From 1ade380bd73424f1a11639daa0c338b1b9092dea Mon Sep 17 00:00:00 2001 From: Matt Williams Date: Thu, 7 Dec 2023 11:48:25 -0800 Subject: [PATCH] Simple chat example for typescript Signed-off-by: Matt Williams --- examples/typescript-simplechat/client.ts | 78 +++++++++++++++++++++ examples/typescript-simplechat/package.json | 1 + examples/typescript-simplechat/readme.md | 31 ++++++++ 3 files changed, 110 insertions(+) create mode 100644 examples/typescript-simplechat/client.ts create mode 100644 examples/typescript-simplechat/package.json create mode 100644 examples/typescript-simplechat/readme.md diff --git a/examples/typescript-simplechat/client.ts b/examples/typescript-simplechat/client.ts new file mode 100644 index 00000000..7e37fe30 --- /dev/null +++ b/examples/typescript-simplechat/client.ts @@ -0,0 +1,78 @@ +import * as readline from "readline"; + +const model = "llama2"; +type Message = { + role: "assistant" | "user" | "system"; + content: string; +} +const messages: Message[] = [{ + role: "system", + content: "You are a helpful AI agent." +}] + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +async function chat(messages: Message[]): Promise { + const body = { + model: model, + messages: messages + } + + const response = await fetch("http://localhost:11434/api/chat", { + method: "POST", + body: JSON.stringify(body) + }) + + const reader = response.body?.getReader() + if (!reader) { + throw new Error("Failed to read response body") + } + const content: string[] = [] + while (true) { + const { done, value } = await reader.read() + if (done) { + break; + } + const rawjson = new TextDecoder().decode(value); + const json = JSON.parse(rawjson) + + if (json.done === false) { + process.stdout.write(json.message.content); + content.push(json.message.content) + // messages.push({role: "system", content: text}) + } + + } + return { role: "assistant", content: content.join("") }; +} + +async function askQuestion(): Promise { + return new Promise((resolve) => { + rl.question("\n\nAsk a question: (press enter alone to quit)\n\n", async (user_input) => { + if (user_input.trim() === "") { + rl.close(); + console.log("Thankyou. Goodbye.\n") + console.log("=======\nHere is the message history that was used in this conversation.\n=======\n") + messages.forEach(message => { + console.log(message) + }) + resolve(); + } else { + console.log(); + messages.push({ role: "user", content: user_input }); + messages.push(await chat(messages)); + await askQuestion(); // Ask the next question + } + }); + }); +} + +async function main() { + await askQuestion(); + +} + +main(); \ No newline at end of file diff --git a/examples/typescript-simplechat/package.json b/examples/typescript-simplechat/package.json new file mode 100644 index 00000000..4ee1647d --- /dev/null +++ b/examples/typescript-simplechat/package.json @@ -0,0 +1 @@ +{ "dependencies": { "@types/node": "^20.10.4", "prompt-sync": "^4.2.0", "readline": "^1.3.0" } } \ No newline at end of file diff --git a/examples/typescript-simplechat/readme.md b/examples/typescript-simplechat/readme.md new file mode 100644 index 00000000..ea61bd8a --- /dev/null +++ b/examples/typescript-simplechat/readme.md @@ -0,0 +1,31 @@ +# Simple Chat Example + +The **chat** endpoint is one of two ways to generate text from an LLM with Ollama. At a high level you provide the endpoint an array of objects with a role and content specified. Then with each output and prompt, you add more of those role/content objects, which builds up the history. + +## Review the Code + +You can see in the **chat** function that actually calling the endpoint is done simply with: + +```typescript +const body = { + model: model, + messages: messages +} + +const response = await fetch("http://localhost:11434/api/chat", { + method: "POST", + body: JSON.stringify(body) +}) +``` + +With the **generate** endpoint, you need to provide a `prompt`. But with **chat**, you provide `messages`. And the resulting stream of responses includes a `message` object with a `content` field. + +The final JSON object doesn't provide the full content, so you will need to build the content yourself. In this example, **chat** takes the full array of messages and outputs the resulting message from this call of the chat endpoint. + +In the **askQuestion** function, we collect `user_input` and add it as a message to our messages and that is passed to the chat function. When the LLM is done responding the output is added as another message to the messages array. + +At the end, you will see a printout of all the messages. + +## Next Steps + +In this example, all generations are kept. You might want to experiment with summarizing everything older than 10 conversations to enable longer history with less context being used.