Skip to content

Commit

Permalink
Update ai & provider readmes, add getCountryTool file
Browse files Browse the repository at this point in the history
  • Loading branch information
diksipav committed Nov 21, 2024
1 parent 00955e6 commit ff7a746
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 110 deletions.
67 changes: 55 additions & 12 deletions nextjs-edgedb-ai/README.md
Original file line number Diff line number Diff line change
@@ -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.
83 changes: 8 additions & 75 deletions nextjs-edgedb-ai/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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=<str>$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;
}
90 changes: 76 additions & 14 deletions nextjs-edgedb-ai/app/api/completion/route.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,96 @@
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",
});

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();
}
Expand All @@ -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",
},
});
}
69 changes: 69 additions & 0 deletions nextjs-edgedb-ai/app/api/getCountryTool.ts
Original file line number Diff line number Diff line change
@@ -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=<str>$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;
}
Loading

0 comments on commit ff7a746

Please sign in to comment.