Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EdgeDB AI and Provider example chat apps #132

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
769863a
Create basic gpt-like interface
diksipav Apr 25, 2024
7a7a056
Add server api route & update schema
diksipav Apr 30, 2024
0fc1ea2
Use eventsource-parser to correctly parse stream data
diksipav Apr 30, 2024
0ad615b
Add eventsource-parser to package-lock.json
scotttrinh May 1, 2024
ed9fbfe
Avoid failing at runtime
scotttrinh May 1, 2024
3133980
Use normal client config (via environment)
scotttrinh May 1, 2024
22227cb
`streamRag` already returns a Response
scotttrinh May 1, 2024
5fe05be
Use `ai` SDK for chat functionality
scotttrinh May 6, 2024
94c9a5f
Update UI message colors
diksipav May 8, 2024
d59bad8
Add .env file and envConfig.ts to load vars
diksipav May 10, 2024
e387af4
Add nextjs-ai-new: edgedb ai provider + vercel ai sdk
diksipav Oct 17, 2024
99fbc59
Use edgedb-ai-sdk provider
diksipav Oct 17, 2024
4076f3f
Merge branch 'main' into add-nextjs-ai-example
diksipav Oct 17, 2024
22a30f4
Remove unused yarn files
diksipav Oct 17, 2024
f04f805
nextjs-ai-new: add generate & stream test examples, update ui
diksipav Nov 6, 2024
d19b710
Remove unused imports from nextjs-ai example
diksipav Nov 6, 2024
c9ae7e5
Rename and polish ai examples
diksipav Nov 20, 2024
534c86e
Refactor & delete unused code
diksipav Nov 20, 2024
00955e6
Update default prompt in the seed script
diksipav Nov 20, 2024
ee7d7a4
Update ai & provider readmes, add getCountryTool file
diksipav Nov 21, 2024
225dc68
Polish ai & provider readmes
diksipav Nov 21, 2024
0a32321
Use vercel ai sdk 4 in ai examples
diksipav Nov 21, 2024
080cd83
Add digital library example app
diksipav Nov 26, 2024
02df33c
Update digital-lib ui to match vercel ui more
diksipav Jan 6, 2025
738a8a2
Make digital-lib UI responsive
diksipav Jan 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions digital-library/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
72 changes: 72 additions & 0 deletions digital-library/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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](https://docs.edgedb.com/ai/vercel-ai-provider) for Vercel AI 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

- 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. 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 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.

**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`

- `streamTextExamples`

These folders contain examples of how to use `generateText` or `streamText` for basic queries or queries that include tool calls.
For queries that requires tool calls, there are examples demonstrating two scenarios:

- 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.
39 changes: 39 additions & 0 deletions digital-library/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { streamText, tool } from "ai";
import { z } from "zod";
import { edgedb } from "../../../../../edgedb-js/packages/vercel-ai-provider/dist";
import { getCountry } from "@/app/utils";

export async function POST(req: Request) {
const requestData = await req.json();

const { bookIds, messages } = requestData;

const context = {
query: bookIds.length
? "select Book filter .id in array_unpack(<array<uuid>>$bookIds)"
: "Book",
variables: bookIds ? { bookIds } : undefined,
};

const model = (await edgedb).languageModel("gpt-4-turbo", {
context,
});

const result = streamText({
model,
messages,
tools: {
country: tool({
description: "Get the country of the author",
parameters: z.object({
author: z.string().describe("Author name to get the country for"),
}),
execute: getCountry,
}),
},
// Required to be > 1 in order for the Vercel SDK to execute tools.
maxSteps: 5,
});

return result.toDataStreamResponse();
}
31 changes: 31 additions & 0 deletions digital-library/app/booksProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import {
useState,
createContext,
type SetStateAction,
type Dispatch,
} from "react";

interface BookContextType {
bookIds: string[];
setBookIds: Dispatch<SetStateAction<string[]>>;
}

export const BooksContext = createContext<BookContextType | undefined>(
undefined
);

export default function BooksProvider({
children,
}: {
children: React.ReactNode;
}) {
const [bookIds, setBookIds] = useState<string[]>([]);

return (
<BooksContext.Provider value={{ bookIds, setBookIds }}>
{children}
</BooksContext.Provider>
);
}
70 changes: 70 additions & 0 deletions digital-library/app/components/bookCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import { useState, useContext } from "react";
import { BooksContext } from "../booksProvider";

interface BookCardProps {
id: string;
title: string;
author: string;
summary: string;
}

export default function BookCard({
id,
title,
author,
summary,
}: BookCardProps) {
const bookIds = useContext(BooksContext)?.bookIds;
const setBookIds = useContext(BooksContext)?.setBookIds;

const [isActive, setIsActive] = useState(false);

function handleClick() {
if (bookIds && setBookIds) {
if (isActive) {
setBookIds(bookIds?.filter((bookId) => bookId !== id));
} else {
setBookIds([...bookIds, id]);
}
}

setIsActive(!isActive);
}

return (
<div className="relative bg-elem rounded-md mb-3 p-3 pt-2">
<p>{title}</p>
<p className="text-sm mb-1">- {author}</p>
<p
className="overflow-hidden text-ellipsis text-xs"
style={{
display: "-webkit-box",
WebkitBoxOrient: "vertical",
WebkitLineClamp: 2,
}}
>
{summary}
</p>
<button
onClick={handleClick}
className={`w-7 h-7 rounded-full border-[1.5px] flex items-center justify-center
absolute -top-2 -right-2 transition-colors duration-200
${
isActive
? "bg-text border-text"
: "bg-transparent border-[#CCCCCC]"
}`}
>
<span
className={`text-lg transition-colors duration-200 ${
isActive ? "text-bg" : "text-[#CCCCCC]"
}`}
>
+
</span>
</button>
</div>
);
}
42 changes: 42 additions & 0 deletions digital-library/app/components/bookCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { useState } from "react";
import BookCard from "./bookCard";
import { SidebarIcon } from "../icons";

export default function BookCards({ books }: { books: any[] }) {
const [isCollapsed, setIsCollapsed] = useState(false);

return (
<>
<aside
className={`pt-14 border-r border-border transition-all duration-300 ease-in-out
bg-bg absolute lg:relative top-0 left-0 z-20 shrink-0 ${
isCollapsed
? "h-fit lg:h-screen border-b rounded-br-lg w-14"
: "h-screen w-[300px] block"
} `}
>
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute top-2 right-2 p-2 text-md rounded-md text-text hover:bg-elem"
aria-label="Toggle Sidebar"
>
<SidebarIcon />
</button>
<div
className={`h-full overflow-y-auto p-3 px-4 pr-5 ${
isCollapsed ? "hidden" : "block"
}`}
>
{books.map((book) => (
<BookCard key={book.id} {...book} />
))}
</div>
</aside>
{!isCollapsed && (
<div className="fixed inset-0 bg-black bg-opacity-60 z-10 lg:hidden" />
)}
</>
);
}
78 changes: 78 additions & 0 deletions digital-library/app/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client";

import { useContext } from "react";
import { useChat } from "ai/react";
import { BooksContext } from "../booksProvider";
import LoadingDots from "./loadingDots";
import { RunIcon } from "../icons";

export default function Chat() {
const bookIds = useContext(BooksContext)?.bookIds;

const { messages, input, handleInputChange, handleSubmit, error, isLoading } =
useChat({
body: {
bookIds,
},
});

return (
<div className="overflow-y-auto flex flex-column gap-4 w-full">
<div className="w-full flex flex-column justify-center pt-8 pb-6">
<div className="w-[688px] px-4 lg:px-6">
{!input && messages.length === 0 && (
<div className="text-center">
<h1 className="mt-4 font-bold">LET'S CHAT!</h1>
<p>Ask me about books and authors.</p>
</div>
)}
{messages.map((m) =>
m.role === "user" ? (
<div className="flex justify-end relative mb-2" key={m.id}>
<div className="text-text p-4 bg-elem max-w-[80%] rounded-full shadow-lg">
{m.content}
</div>
{true && (
<LoadingDots className="absolute -bottom-[20px] left-4" />
)}
</div>
) : (
m.content && (
<div
key={m.id}
className="bg-bg text-text p-4 rounded-xl mb-4 z-10 relative"
>
<div className="opacity-30 text-xs font-semibold">AI</div>
{m.content}
</div>
)
)
)}
{error && (
<div
className="m-auto bg-red-200 text-red-700 px-4 py-2 rounded relative"
role="alert"
>
<span>An error has occurred. </span>
</div>
)}
</div>
</div>
<form
className="absolute bottom-4 left-1/2 transform -translate-x-1/2 px-4 w-full max-w-2xl"
onSubmit={handleSubmit}
>
<input
className="bg-text pl-4 py-3 rounded-xl outline-none w-full text-bg
placeholder:text-placeholder shadow-md"
placeholder="Ask a question..."
onChange={handleInputChange}
value={input}
/>
<button className="absolute right-8 top-3">
<RunIcon className={` ${input ? "text-bg" : "text-placeholder"}`} />
</button>
</form>
</div>
);
}
9 changes: 9 additions & 0 deletions digital-library/app/components/loadingDots.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function LoadingDots({ className }: { className?: string }) {
return (
<span className={`flex items-center gap-1.5 ${className}`}>
<span className="w-1.5 h-1.5 bg-[#333333] rounded-full animate-pulse delay-[0ms]"></span>
<span className="w-1.5 h-1.5 bg-[#333333] rounded-full animate-pulse delay-[200ms]"></span>
<span className="w-1.5 h-1.5 bg-[#333333] rounded-full animate-pulse delay-[400ms]"></span>
</span>
);
}
Binary file added digital-library/app/favicon.ico
Binary file not shown.
Loading