diff --git a/nextjs-edgedb-ai/README.md b/nextjs-edgedb-ai/README.md index 5550ab6..6adeecd 100644 --- a/nextjs-edgedb-ai/README.md +++ b/nextjs-edgedb-ai/README.md @@ -1,17 +1,60 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). +# Chat App with EdgeDB AI and Vercel AI SDK + +This is an example project built with [Next.js](https://nextjs.org/) to showcase the use of [EdgeDB's AI](https://docs.edgedb.com/ai) features. The application demonstrates a chat app that allows users to query a digital library of imaginary books. Users can ask about books, their authors, and related details, leveraging the EdgeDB database to store and retrieve the data, and embeddings. For the LLM models, you can use any of the `OpenAI`, `Mistral`, or `Anthropic` models that EdgeDB AI supports. ## Getting Started -First, run the development server: +- Install dependencies: + + ```bash + npm install + ``` + +- Initialize EdgeDB project: + + ```bash + edgedb project init + edgedb migrate + ``` + + Check the schema file to see how the **deferred semantic similarity index** is defined for the Book type. This index enables the generation of embeddings for a provided expression, which are then used for retrieving relevant context from the database when users ask questions. + +- Seed the database with books and authors: + + ```bash + npm run seed + ``` + +- Start the development server: + + ```bash + npm run dev + ``` + +## Features + +### Chat Route (/) + +Users can have a conversation with EdgeDB AI about books. The conversation history is preserved. +Some example questions: + +- What Ariadne writes about? +- Where is she from? +- What is the book "Whispers of the Forgotten City" about? + +### Completion route (/completion) + +A standalone query without persistent chat history. Each question is independent. + +### Function calling + +EdgeDB AI extension supports function calling. In this project we defined `getCountry` tool which is utilized in both the chat and completion routes. +This tool retrieves an author's country of origin from the database. For example, if a user asks, **"Where is Ariadne from?"**, the getCountry tool should be invoked. +The `processStream` function is responsible for parsing response chunks. When a tool call response is detected, the function executes the corresponding tool, updates the messages array with the tool's results, and provides the updated array back to the AI. + +**NOTE**: It is advisable to create a system query in a way that ensures it is aware of the available tools and understands when to call each tool. We achieved this in the seed script by updating the `builtin::rag-default` prompt. However, you can also update this prompt using the EdgeDB UI or via the REPL. Additionally, you can create a new prompt for this purpose using the UI or the REPL with an `INSERT` query. -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +### Feedback and Contributions -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Feel free to fork this project, suggest improvements, or raise issues. +This project is a simple starting point for exploring how EdgeDB can integrate with Vercel AI SDK. diff --git a/nextjs-edgedb-ai/app/api/chat/route.ts b/nextjs-edgedb-ai/app/api/chat/route.ts index 7e23222..e0c9bdd 100644 --- a/nextjs-edgedb-ai/app/api/chat/route.ts +++ b/nextjs-edgedb-ai/app/api/chat/route.ts @@ -1,13 +1,14 @@ import { createClient } from "edgedb"; +import { createAI } from "@edgedb/ai"; import { - createAI, - EdgeDBAssistantMessage, - EdgeDBToolMessage, -} from "@edgedb/ai"; + countryTool, + getCountry, + generateToolMessages, +} from "../getCountryTool"; export const dynamic = "force-dynamic"; -export const client = createClient({ tlsSecurity: "insecure" }); +export const client = createClient(); const gpt4Ai = createAI(client, { model: "gpt-4-turbo-preview", @@ -51,7 +52,7 @@ export async function POST(req: Request) { // we only have one tool so using if statement is fine // if there are more tools, some toolMap object can be used if (call.name === "getCountry") { - let res = await getCountry(JSON.parse(call.args)); + let res = await getCountry(client, JSON.parse(call.args).author); toolResults.push(res); } } @@ -61,8 +62,7 @@ export async function POST(req: Request) { ...initialMessages, ...generateToolMessages(toolCalls, toolResults), ]; - console.log("new messages", newMessages); - console.log("--"); + // recursively process the stream with the updated messages await processStream(newMessages, controller); } @@ -90,70 +90,3 @@ export async function POST(req: Request) { }, }); } - -const countryTool = { - type: "function", - function: { - name: "getCountry", - description: "Get the country of the author", - parameters: { - type: "object", - properties: { - author: { - type: "string", - description: "Author name to get the country for.", - }, - }, - required: ["author"], - }, - }, -}; - -async function getCountry({ author }: { author: string }) { - const res: { name: string; country: string } | null = - await client.querySingle( - ` - select Author { name, country } - filter .name=$author;`, - { author } - ); - - return res?.country - ? res - : { - ...res, - country: `There is no available data on the country of origin for ${author}.`, - }; -} - -function generateToolMessages(toolCalls: any[], results: any[]) { - let messages: (EdgeDBAssistantMessage | EdgeDBToolMessage)[] = []; - - toolCalls.forEach((call, i) => { - messages.push( - ...[ - { - role: "assistant" as const, - content: "", - tool_calls: [ - { - id: call.id, - type: "function" as const, - function: { - name: call.name, - arguments: call.args, - }, - }, - ], - }, - { - role: "tool" as const, - content: JSON.stringify(results[i]), - tool_call_id: call.id, - }, - ] - ); - }); - - return messages; -} diff --git a/nextjs-edgedb-ai/app/api/completion/route.ts b/nextjs-edgedb-ai/app/api/completion/route.ts index 6a6bffb..a947743 100644 --- a/nextjs-edgedb-ai/app/api/completion/route.ts +++ b/nextjs-edgedb-ai/app/api/completion/route.ts @@ -1,9 +1,14 @@ import { createClient } from "edgedb"; -import { createAI } from "@edgedb/ai"; +import { createAI, EdgeDBMessage, EdgeDBUserMessage } from "@edgedb/ai"; +import { + countryTool, + getCountry, + generateToolMessages, +} from "../getCountryTool"; export const dynamic = "force-dynamic"; -export const client = createClient({ tlsSecurity: "insecure" }); +export const client = createClient(); const gpt4Ai = createAI(client, { model: "gpt-4-turbo-preview", @@ -11,23 +16,81 @@ const gpt4Ai = createAI(client, { const booksAi = gpt4Ai.withContext({ query: "Book" }); -// tool calls aren't handled, check the chat route for tool calls handling +// we handle tools calls here too export async function POST(req: Request) { const { prompt } = await req.json(); + + async function processStream( + prompt: string | EdgeDBMessage[], + controller: ReadableStreamDefaultController + ) { + let toolCalls: any[] = []; + + // if the AI response finishes with a tool call, + // next prompt will be an array of messages that includes tool results + const isStrPrompt = typeof prompt == "string"; + + for await (const chunk of booksAi.streamRag({ + ...(isStrPrompt ? { prompt } : { messages: prompt }), + tools: [countryTool], + })) { + // if it is content_block_delta text chunk (type==text_delta) enqueue the text, if it is + // content_block_delta tool call chunk (type==tool_call_delta) add it to the toolCalls list + if (chunk.type === "content_block_delta") { + if (chunk.delta.type === "text_delta") { + controller.enqueue(new TextEncoder().encode(chunk.delta.text)); + } else { + toolCalls[toolCalls.length - 1].args += chunk.delta.args; + } + } else if ( + chunk.type === "content_block_start" && + chunk.content_block.type === "tool_use" + ) { + toolCalls.push(chunk.content_block); + } + } + + // call the tool function for every tool call and get results + if (toolCalls.length > 0) { + let toolResults: any[] = []; + for (const call of toolCalls) { + // we only have one tool so using if statement is fine + // if there are more tools, some toolMap object can be used + if (call.name === "getCountry") { + let res = await getCountry(client, JSON.parse(call.args).author); + toolResults.push(res); + } + } + + // add tool messages to messages + const initialMessages = isStrPrompt + ? [ + { + role: "user" as const, + content: [{ type: "text" as const, text: prompt }], + }, + ] + : prompt; + + const newMessages = [ + ...initialMessages, + ...generateToolMessages(toolCalls, toolResults), + ]; + + // recursively process the stream with the updated messages + await processStream(newMessages, controller); + } + } + const stream = new ReadableStream({ async start(controller) { try { - // stream data from booksAi and enqueue each text chunk as plain text - for await (const chunk of booksAi.streamRag({ - prompt, - })) { - if (chunk.type == "content_block_delta") { - controller.enqueue(new TextEncoder().encode(chunk.delta.text)); - } - } + // start processing the initial stream + await processStream(prompt, controller); } catch (error) { - console.error("Error streaming data:", error); - controller.error(error); + const errMsg = error instanceof Error ? error.message : error; + console.error("Error streaming data:\n", errMsg); + controller.error(errMsg); } finally { controller.close(); } @@ -38,7 +101,6 @@ export async function POST(req: Request) { headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache", - "Transfer-Encoding": "chunked", }, }); } diff --git a/nextjs-edgedb-ai/app/api/getCountryTool.ts b/nextjs-edgedb-ai/app/api/getCountryTool.ts new file mode 100644 index 0000000..3207dc6 --- /dev/null +++ b/nextjs-edgedb-ai/app/api/getCountryTool.ts @@ -0,0 +1,69 @@ +import { EdgeDBAssistantMessage, EdgeDBToolMessage } from "@edgedb/ai"; +import type { Client } from "edgedb"; + +export const countryTool = { + type: "function", + function: { + name: "getCountry", + description: "Get the country of the author", + parameters: { + type: "object", + properties: { + author: { + type: "string", + description: "Author name to get the country for.", + }, + }, + required: ["author"], + }, + }, +}; + +export async function getCountry(client: Client, author: string) { + const res: { name: string; country: string } | null = + await client.querySingle( + ` + select Author { name, country } + filter .name=$author;`, + { author } + ); + + return res?.country + ? res + : { + ...res, + country: `There is no available data on the country of origin for ${author}.`, + }; +} + +export function generateToolMessages(toolCalls: any[], results: any[]) { + let messages: (EdgeDBAssistantMessage | EdgeDBToolMessage)[] = []; + + toolCalls.forEach((call, i) => { + messages.push( + ...[ + { + role: "assistant" as const, + content: "", + tool_calls: [ + { + id: call.id, + type: "function" as const, + function: { + name: call.name, + arguments: call.args, + }, + }, + ], + }, + { + role: "tool" as const, + content: JSON.stringify(results[i]), + tool_call_id: call.id, + }, + ] + ); + }); + + return messages; +} diff --git a/nextjs-vercel-ai-provider/README.md b/nextjs-vercel-ai-provider/README.md index 0d9db87..f403d26 100644 --- a/nextjs-vercel-ai-provider/README.md +++ b/nextjs-vercel-ai-provider/README.md @@ -1,6 +1,6 @@ # Chat App with EdgeDB AI and Vercel AI SDK -This is an example project built with [Next.js](https://nextjs.org/) to showcase the use of [EdgeDB's AI](https://docs.edgedb.com/ai) features with the [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction). The [Edgedb provider for Vercel AI](https://docs.edgedb.com/ai/vercel-ai-provider) is used. The application demonstrates a chat app that allows users to query a digital library of imaginary books. Users can ask about books, their authors, and related details, leveraging the EdgeDB database to store and retrieve the data, and embeddings. +This is an example project built with [Next.js](https://nextjs.org/) to showcase the use of [EdgeDB's AI](https://docs.edgedb.com/ai) features with the [Vercel AI SDK](https://sdk.vercel.ai/docs/introduction). The [Edgedb provider for Vercel AI](https://docs.edgedb.com/ai/vercel-ai-provider) is used. The application demonstrates a chat app that allows users to query a digital library of imaginary books. Users can ask about books, their authors, and related details, leveraging the EdgeDB database to store and retrieve the data, and embeddings. For the LLM models, you can use any of the `OpenAI`, `Mistral`, or `Anthropic` models that EdgeDB AI supports. ## Getting Started @@ -33,26 +33,28 @@ This is an example project built with [Next.js](https://nextjs.org/) to showcase ## Features -### Chat Route (/): +### Chat Route (/) -Users can have a conversation with EdgeDB AI about books The conversation history is preserved. +Users can have a conversation with EdgeDB AI about books. The conversation history is preserved. Some example questions: - What Ariadne writes about? - Where is she from? -- What is the book Whispers of the Forgotten City about? +- What is the book "Whispers of the Forgotten City" about? -### Completion route (/completion): +### Completion route (/completion) -A standalone query interface without persistent chat history. Each question is independent. +A standalone query without persistent chat history. Each question is independent. -### Function calling: +### Function calling EdgeDB AI extension supports function calling. Inside this project we defined `getCountry` tool that is used in both chat and completion routes. -This tool retrieves the author's country of origin from the database. If u ask the question **Where is Ariadne from?**, this tool will be used. +This tool retrieves the author's country of origin from the database. If a user asks the question **Where is Ariadne from?**, this tool will be used. In both routes the Vercel AI SDK executes the tools and provides results back to the EdgeDB AI. Users can of course choose to execute the tools on the client side. -### Supplementary Folders: +**NOTE**: It is advisable to create a system query in a way that ensures it is aware of the available tools and understands when to call each tool. We achieved this in the seed script by updating the `builtin::rag-default` prompt. However, you can also update this prompt using the EdgeDB UI or via the REPL. Additionally, you can create a new prompt for this purpose using the UI or the REPL with an `INSERT` query. + +### Supplementary Folders - `generateTextExamples` @@ -63,3 +65,8 @@ For queries that requires tool calls, there are examples demonstrating two scena - The Vercel AI SDK runs the tools and provides the results to the AI. - The application runs the tools client-side and supplies the results to the AI. + +### Feedback and Contributions + +Feel free to fork this project, suggest improvements, or raise issues. +This project is a simple starting point for exploring how EdgeDB can integrate with Vercel AI SDK.