From 4578fc6c3c2b51451a81f50e0e952a07814e273a Mon Sep 17 00:00:00 2001
From: thucpn
Date: Fri, 29 Mar 2024 15:23:50 +0700
Subject: [PATCH 1/6] feat/add-nextjs-llamaindex-agent-example
---
nextjs-llamaindex-agent/.eslintrc.json | 8 +
nextjs-llamaindex-agent/.gitignore | 38 +
nextjs-llamaindex-agent/Dockerfile | 16 +
nextjs-llamaindex-agent/README.md | 38 +
.../app/components/chat-section.tsx | 12 +
.../app/components/header.tsx | 28 +
.../app/components/ui/button.tsx | 56 +
.../app/components/ui/chat/chat-avatar.tsx | 35 +
.../app/components/ui/chat/chat-input.tsx | 57 +
.../app/components/ui/chat/chat-loading.tsx | 9 +
.../app/components/ui/chat/chat-message.tsx | 21 +
.../app/components/ui/chat/chat-messages.tsx | 34 +
.../app/components/ui/chat/codeblock.tsx | 139 +
.../app/components/ui/chat/index.ts | 5 +
.../app/components/ui/chat/markdown.tsx | 59 +
.../ui/chat/use-copy-to-clipboard.tsx | 33 +
.../app/components/ui/input.tsx | 25 +
.../app/components/ui/lib/utils.ts | 6 +
nextjs-llamaindex-agent/app/favicon.ico | Bin 0 -> 15406 bytes
nextjs-llamaindex-agent/app/globals.css | 94 +
nextjs-llamaindex-agent/app/layout.tsx | 28 +
nextjs-llamaindex-agent/app/markdown.css | 23 +
nextjs-llamaindex-agent/app/page.tsx | 11 +
.../rsc/actions/flight-assistant/index.tsx | 100 +
.../rsc/actions/flight-assistant/shared.ts | 14 +
.../app/rsc/actions/flight-assistant/tools.ts | 10 +
.../flight-assistant/ui/flight-card.tsx | 23 +
.../app/rsc/actions/index.ts | 2 +
.../app/rsc/actions/wiki-assistant/index.tsx | 71 +
.../app/rsc/actions/wiki-assistant/shared.ts | 8 +
.../app/rsc/actions/wiki-assistant/tools.ts | 1 +
.../actions/wiki-assistant/ui/flight-card.tsx | 12 +
.../app/rsc/engine/chat.ts | 27 +
.../app/rsc/engine/constants.mjs | 3 +
.../app/rsc/engine/generate.mjs | 46 +
.../app/rsc/engine/index.ts | 32 +
.../app/rsc/engine/loader.mjs | 9 +
nextjs-llamaindex-agent/app/rsc/index.tsx | 12 +
nextjs-llamaindex-agent/app/rsc/type.ts | 18 +
nextjs-llamaindex-agent/config/tools.json | 3 +
nextjs-llamaindex-agent/data/101.pdf | Bin 0 -> 47931 bytes
nextjs-llamaindex-agent/next.config.json | 9 +
nextjs-llamaindex-agent/next.config.mjs | 8 +
nextjs-llamaindex-agent/package.json | 52 +
nextjs-llamaindex-agent/pnpm-lock.yaml | 6200 +++++++++++++++++
nextjs-llamaindex-agent/postcss.config.js | 6 +
nextjs-llamaindex-agent/prettier.config.js | 3 +
nextjs-llamaindex-agent/public/llama.png | Bin 0 -> 36985 bytes
nextjs-llamaindex-agent/tailwind.config.ts | 78 +
nextjs-llamaindex-agent/tsconfig.json | 28 +
nextjs-llamaindex-agent/webpack.config.mjs | 10 +
51 files changed, 7560 insertions(+)
create mode 100644 nextjs-llamaindex-agent/.eslintrc.json
create mode 100644 nextjs-llamaindex-agent/.gitignore
create mode 100644 nextjs-llamaindex-agent/Dockerfile
create mode 100644 nextjs-llamaindex-agent/README.md
create mode 100644 nextjs-llamaindex-agent/app/components/chat-section.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/header.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/button.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/chat-avatar.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/chat-input.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/chat-loading.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/chat-message.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/chat-messages.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/codeblock.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/index.ts
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/markdown.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/chat/use-copy-to-clipboard.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/input.tsx
create mode 100644 nextjs-llamaindex-agent/app/components/ui/lib/utils.ts
create mode 100644 nextjs-llamaindex-agent/app/favicon.ico
create mode 100644 nextjs-llamaindex-agent/app/globals.css
create mode 100644 nextjs-llamaindex-agent/app/layout.tsx
create mode 100644 nextjs-llamaindex-agent/app/markdown.css
create mode 100644 nextjs-llamaindex-agent/app/page.tsx
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/index.tsx
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/shared.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/tools.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/ui/flight-card.tsx
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/index.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/index.tsx
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/shared.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/tools.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/ui/flight-card.tsx
create mode 100644 nextjs-llamaindex-agent/app/rsc/engine/chat.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/engine/constants.mjs
create mode 100644 nextjs-llamaindex-agent/app/rsc/engine/generate.mjs
create mode 100644 nextjs-llamaindex-agent/app/rsc/engine/index.ts
create mode 100644 nextjs-llamaindex-agent/app/rsc/engine/loader.mjs
create mode 100644 nextjs-llamaindex-agent/app/rsc/index.tsx
create mode 100644 nextjs-llamaindex-agent/app/rsc/type.ts
create mode 100644 nextjs-llamaindex-agent/config/tools.json
create mode 100644 nextjs-llamaindex-agent/data/101.pdf
create mode 100644 nextjs-llamaindex-agent/next.config.json
create mode 100644 nextjs-llamaindex-agent/next.config.mjs
create mode 100644 nextjs-llamaindex-agent/package.json
create mode 100644 nextjs-llamaindex-agent/pnpm-lock.yaml
create mode 100644 nextjs-llamaindex-agent/postcss.config.js
create mode 100644 nextjs-llamaindex-agent/prettier.config.js
create mode 100644 nextjs-llamaindex-agent/public/llama.png
create mode 100644 nextjs-llamaindex-agent/tailwind.config.ts
create mode 100644 nextjs-llamaindex-agent/tsconfig.json
create mode 100644 nextjs-llamaindex-agent/webpack.config.mjs
diff --git a/nextjs-llamaindex-agent/.eslintrc.json b/nextjs-llamaindex-agent/.eslintrc.json
new file mode 100644
index 0000000..6b52088
--- /dev/null
+++ b/nextjs-llamaindex-agent/.eslintrc.json
@@ -0,0 +1,8 @@
+{
+ "extends": ["next/core-web-vitals", "prettier"],
+ "rules": {
+ "max-params": ["error", 4],
+ "prefer-const": "error",
+ "no-unused-vars": "warn"
+ }
+}
diff --git a/nextjs-llamaindex-agent/.gitignore b/nextjs-llamaindex-agent/.gitignore
new file mode 100644
index 0000000..920fcdc
--- /dev/null
+++ b/nextjs-llamaindex-agent/.gitignore
@@ -0,0 +1,38 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# 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
+.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+cache/
\ No newline at end of file
diff --git a/nextjs-llamaindex-agent/Dockerfile b/nextjs-llamaindex-agent/Dockerfile
new file mode 100644
index 0000000..5c738ab
--- /dev/null
+++ b/nextjs-llamaindex-agent/Dockerfile
@@ -0,0 +1,16 @@
+FROM node:20-alpine as build
+
+WORKDIR /app
+
+# Install dependencies
+COPY package.json package-lock.* ./
+RUN npm install
+
+# Build the application
+COPY . .
+RUN npm run build
+
+# ====================================
+FROM build as release
+
+CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/nextjs-llamaindex-agent/README.md b/nextjs-llamaindex-agent/README.md
new file mode 100644
index 0000000..a1e287d
--- /dev/null
+++ b/nextjs-llamaindex-agent/README.md
@@ -0,0 +1,38 @@
+This is a [LlamaIndex](https://www.llamaindex.ai/) project using [Next.js](https://nextjs.org/) bootstrapped with [`create-llama`](https://github.com/run-llama/LlamaIndexTS/tree/main/packages/create-llama).
+
+This project was created to demonstrate the use of React Server Components with LlamaIndex Agents.
+
+## Getting Started
+
+First, install the dependencies:
+
+```
+npm install
+```
+
+Second, generate the embeddings of the documents in the `./data` directory (if this folder exists - otherwise, skip this step):
+
+```
+npm run generate
+```
+
+Third, run the development server:
+
+```
+npm run dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about LlamaIndex, take a look at the following resources:
+
+- [LlamaIndex Documentation](https://docs.llamaindex.ai) - learn about LlamaIndex (Python features).
+- [LlamaIndexTS Documentation](https://ts.llamaindex.ai) - learn about LlamaIndex (Typescript features).
+
+You can check out [the LlamaIndexTS GitHub repository](https://github.com/run-llama/LlamaIndexTS) - your feedback and contributions are welcome!
diff --git a/nextjs-llamaindex-agent/app/components/chat-section.tsx b/nextjs-llamaindex-agent/app/components/chat-section.tsx
new file mode 100644
index 0000000..9d7a6b2
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/chat-section.tsx
@@ -0,0 +1,12 @@
+"use client";
+
+import { ChatInput, ChatMessages } from "./ui/chat";
+
+export default function ChatSection() {
+ return (
+
+
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/header.tsx b/nextjs-llamaindex-agent/app/components/header.tsx
new file mode 100644
index 0000000..2b0e488
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/header.tsx
@@ -0,0 +1,28 @@
+import Image from "next/image";
+
+export default function Header() {
+ return (
+
+
+ Get started by editing
+ app/page.tsx
+
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/button.tsx b/nextjs-llamaindex-agent/app/components/ui/button.tsx
new file mode 100644
index 0000000..662b040
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "./lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button, buttonVariants };
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/chat-avatar.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/chat-avatar.tsx
new file mode 100644
index 0000000..a4a4853
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/chat-avatar.tsx
@@ -0,0 +1,35 @@
+import { Blend, User2 } from "lucide-react";
+import Image from "next/image";
+
+type AvatarType = "user" | "function" | "assistant";
+
+export default function ChatAvatar({ role = "user" }: { role: AvatarType }) {
+ if (role === "user") {
+ return (
+
+
+
+ );
+ }
+
+ if (role === "function") {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/chat-input.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/chat-input.tsx
new file mode 100644
index 0000000..a01c401
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/chat-input.tsx
@@ -0,0 +1,57 @@
+import { AI } from "@/app/rsc";
+import { UIStateItem } from "@/app/rsc/type";
+import { useAIState, useActions, useUIState } from "ai/rsc";
+import { nanoid } from "nanoid";
+import { FormEvent, useState } from "react";
+import { Button } from "../button";
+import { Input } from "../input";
+import ChatMessage from "./chat-message";
+
+export default function ChatInput() {
+ const [inputValue, setInputValue] = useState("");
+ const [aiState] = useAIState();
+ const [messages, setMessages] = useUIState();
+ const { submitUserMessage } = useActions();
+ console.log({ aiState, messages });
+
+ const onSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ if (!inputValue) return;
+
+ // Add user message to UI state
+ setMessages((currentMessages) => [
+ ...currentMessages,
+ {
+ id: nanoid(),
+ display: ,
+ },
+ ]);
+
+ // Submit and get response message
+ const responseMessage: UIStateItem = await submitUserMessage(inputValue);
+ setMessages((currentMessages) => [...currentMessages, responseMessage]);
+
+ setInputValue("");
+ };
+
+ return (
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/chat-loading.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/chat-loading.tsx
new file mode 100644
index 0000000..c975123
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/chat-loading.tsx
@@ -0,0 +1,9 @@
+import { Loader2 } from "lucide-react";
+
+export default function ChatLoading() {
+ return (
+
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/chat-message.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/chat-message.tsx
new file mode 100644
index 0000000..8c80bb7
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/chat-message.tsx
@@ -0,0 +1,21 @@
+import ChatAvatar from "./chat-avatar";
+import Markdown from "./markdown";
+
+export default function ChatMessage({
+ message,
+ role,
+}: {
+ message: string;
+ role: "user" | "function" | "assistant";
+}) {
+ return (
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/chat-messages.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/chat-messages.tsx
new file mode 100644
index 0000000..751c171
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/chat-messages.tsx
@@ -0,0 +1,34 @@
+import { AI } from "@/app/rsc";
+import { useUIState } from "ai/rsc";
+import { Fragment, useEffect, useRef } from "react";
+
+export default function ChatMessages() {
+ const [messages] = useUIState();
+ const scrollableChatContainerRef = useRef(null);
+ const messageLength = messages.length;
+ const lastMessage = messages[messageLength - 1];
+
+ const scrollToBottom = () => {
+ if (scrollableChatContainerRef.current) {
+ scrollableChatContainerRef.current.scrollTop =
+ scrollableChatContainerRef.current.scrollHeight;
+ }
+ };
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messageLength, lastMessage]);
+
+ return (
+
+
+ {messages.map((message) => (
+ {message.display}
+ ))}
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/codeblock.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/codeblock.tsx
new file mode 100644
index 0000000..014a0fc
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/codeblock.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { Check, Copy, Download } from "lucide-react";
+import { FC, memo } from "react";
+import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter";
+import { coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
+
+import { Button } from "../button";
+import { useCopyToClipboard } from "./use-copy-to-clipboard";
+
+// TODO: Remove this when @type/react-syntax-highlighter is updated
+const SyntaxHighlighter = Prism as unknown as FC;
+
+interface Props {
+ language: string;
+ value: string;
+}
+
+interface languageMap {
+ [key: string]: string | undefined;
+}
+
+export const programmingLanguages: languageMap = {
+ javascript: ".js",
+ python: ".py",
+ java: ".java",
+ c: ".c",
+ cpp: ".cpp",
+ "c++": ".cpp",
+ "c#": ".cs",
+ ruby: ".rb",
+ php: ".php",
+ swift: ".swift",
+ "objective-c": ".m",
+ kotlin: ".kt",
+ typescript: ".ts",
+ go: ".go",
+ perl: ".pl",
+ rust: ".rs",
+ scala: ".scala",
+ haskell: ".hs",
+ lua: ".lua",
+ shell: ".sh",
+ sql: ".sql",
+ html: ".html",
+ css: ".css",
+ // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
+};
+
+export const generateRandomString = (length: number, lowercase = false) => {
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXY3456789"; // excluding similar looking characters like Z, 2, I, 1, O, 0
+ let result = "";
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return lowercase ? result.toLowerCase() : result;
+};
+
+const CodeBlock: FC = memo(({ language, value }) => {
+ const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 });
+
+ const downloadAsFile = () => {
+ if (typeof window === "undefined") {
+ return;
+ }
+ const fileExtension = programmingLanguages[language] || ".file";
+ const suggestedFileName = `file-${generateRandomString(
+ 3,
+ true,
+ )}${fileExtension}`;
+ const fileName = window.prompt("Enter file name" || "", suggestedFileName);
+
+ if (!fileName) {
+ // User pressed cancel on prompt.
+ return;
+ }
+
+ const blob = new Blob([value], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.download = fileName;
+ link.href = url;
+ link.style.display = "none";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ const onCopy = () => {
+ if (isCopied) return;
+ copyToClipboard(value);
+ };
+
+ return (
+
+
+
{language}
+
+
+
+
+
+
+ {value}
+
+
+ );
+});
+CodeBlock.displayName = "CodeBlock";
+
+export { CodeBlock };
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/index.ts b/nextjs-llamaindex-agent/app/components/ui/chat/index.ts
new file mode 100644
index 0000000..112ef39
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/index.ts
@@ -0,0 +1,5 @@
+import ChatInput from "./chat-input";
+import ChatMessages from "./chat-messages";
+
+export { type ChatHandler } from "./chat.interface";
+export { ChatInput, ChatMessages };
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/markdown.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/markdown.tsx
new file mode 100644
index 0000000..82d2f29
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/markdown.tsx
@@ -0,0 +1,59 @@
+import { FC, memo } from "react";
+import ReactMarkdown, { Options } from "react-markdown";
+import remarkGfm from "remark-gfm";
+import remarkMath from "remark-math";
+
+import { CodeBlock } from "./codeblock";
+
+const MemoizedReactMarkdown: FC = memo(
+ ReactMarkdown,
+ (prevProps, nextProps) =>
+ prevProps.children === nextProps.children &&
+ prevProps.className === nextProps.className,
+);
+
+export default function Markdown({ content }: { content: string }) {
+ return (
+ {children}
;
+ },
+ code({ inline, className, children, ...props }) {
+ if (children.length) {
+ if (children[0] == "▍") {
+ return (
+ ▍
+ );
+ }
+
+ children[0] = (children[0] as string).replace("`▍`", "▍");
+ }
+
+ const match = /language-(\w+)/.exec(className || "");
+
+ if (inline) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ );
+ },
+ }}
+ >
+ {content}
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/chat/use-copy-to-clipboard.tsx b/nextjs-llamaindex-agent/app/components/ui/chat/use-copy-to-clipboard.tsx
new file mode 100644
index 0000000..e011d69
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/chat/use-copy-to-clipboard.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import * as React from "react";
+
+export interface useCopyToClipboardProps {
+ timeout?: number;
+}
+
+export function useCopyToClipboard({
+ timeout = 2000,
+}: useCopyToClipboardProps) {
+ const [isCopied, setIsCopied] = React.useState(false);
+
+ const copyToClipboard = (value: string) => {
+ if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
+ return;
+ }
+
+ if (!value) {
+ return;
+ }
+
+ navigator.clipboard.writeText(value).then(() => {
+ setIsCopied(true);
+
+ setTimeout(() => {
+ setIsCopied(false);
+ }, timeout);
+ });
+ };
+
+ return { isCopied, copyToClipboard };
+}
diff --git a/nextjs-llamaindex-agent/app/components/ui/input.tsx b/nextjs-llamaindex-agent/app/components/ui/input.tsx
new file mode 100644
index 0000000..edfa129
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/input.tsx
@@ -0,0 +1,25 @@
+import * as React from "react";
+
+import { cn } from "./lib/utils";
+
+export interface InputProps
+ extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/nextjs-llamaindex-agent/app/components/ui/lib/utils.ts b/nextjs-llamaindex-agent/app/components/ui/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/components/ui/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/nextjs-llamaindex-agent/app/favicon.ico b/nextjs-llamaindex-agent/app/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..a1eaef62f2dfa895f1bbffc6595bb53d9604963e
GIT binary patch
literal 15406
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
KfA0a-1OEqKLE94m
literal 0
HcmV?d00001
diff --git a/nextjs-llamaindex-agent/app/globals.css b/nextjs-llamaindex-agent/app/globals.css
new file mode 100644
index 0000000..09b85ed
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 47.4% 11.2%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 47.4% 11.2%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 100% 50%;
+ --destructive-foreground: 210 40% 98%;
+
+ --ring: 215 20.2% 65.1%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 224 71% 4%;
+ --foreground: 213 31% 91%;
+
+ --muted: 223 47% 11%;
+ --muted-foreground: 215.4 16.3% 56.9%;
+
+ --accent: 216 34% 17%;
+ --accent-foreground: 210 40% 98%;
+
+ --popover: 224 71% 4%;
+ --popover-foreground: 215 20.2% 65.1%;
+
+ --border: 216 34% 17%;
+ --input: 216 34% 17%;
+
+ --card: 224 71% 4%;
+ --card-foreground: 213 31% 91%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 1.2%;
+
+ --secondary: 222.2 47.4% 11.2%;
+ --secondary-foreground: 210 40% 98%;
+
+ --destructive: 0 63% 31%;
+ --destructive-foreground: 210 40% 98%;
+
+ --ring: 216 34% 17%;
+
+ --radius: 0.5rem;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ font-feature-settings:
+ "rlig" 1,
+ "calt" 1;
+ }
+ .background-gradient {
+ background-color: #fff;
+ background-image: radial-gradient(
+ at 21% 11%,
+ rgba(186, 186, 233, 0.53) 0,
+ transparent 50%
+ ),
+ radial-gradient(at 85% 0, hsla(46, 57%, 78%, 0.52) 0, transparent 50%),
+ radial-gradient(at 91% 36%, rgba(194, 213, 255, 0.68) 0, transparent 50%),
+ radial-gradient(at 8% 40%, rgba(251, 218, 239, 0.46) 0, transparent 50%);
+ }
+}
diff --git a/nextjs-llamaindex-agent/app/layout.tsx b/nextjs-llamaindex-agent/app/layout.tsx
new file mode 100644
index 0000000..2cfaba8
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/layout.tsx
@@ -0,0 +1,28 @@
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+import "./markdown.css";
+import { AI } from "@/app/rsc";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Create Llama App",
+ description: "Generated by create-llama",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/nextjs-llamaindex-agent/app/markdown.css b/nextjs-llamaindex-agent/app/markdown.css
new file mode 100644
index 0000000..a843eeb
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/markdown.css
@@ -0,0 +1,23 @@
+/* Custom CSS for chat message markdown */
+.custom-markdown ul {
+ list-style-type: disc;
+ margin-left: 20px;
+}
+
+.custom-markdown ol {
+ list-style-type: decimal;
+ margin-left: 20px;
+}
+
+.custom-markdown li {
+ margin-bottom: 5px;
+}
+
+.custom-markdown ol ol {
+ list-style: lower-alpha;
+}
+
+.custom-markdown ul ul,
+.custom-markdown ol ol {
+ margin-left: 20px;
+}
diff --git a/nextjs-llamaindex-agent/app/page.tsx b/nextjs-llamaindex-agent/app/page.tsx
new file mode 100644
index 0000000..ef00262
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/page.tsx
@@ -0,0 +1,11 @@
+import Header from "@/app/components/header";
+import ChatSection from "./components/chat-section";
+
+export default function Home() {
+ return (
+
+
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/index.tsx b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/index.tsx
new file mode 100644
index 0000000..b558277
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/index.tsx
@@ -0,0 +1,100 @@
+import ChatLoading from "@/app/components/ui/chat/chat-loading";
+import ChatMessage from "@/app/components/ui/chat/chat-message";
+import { getMutableAIState, render } from "ai/rsc";
+import { nanoid } from "nanoid";
+import { OpenAI } from "openai";
+import { z } from "zod";
+import { type AI } from "../../index";
+import { Message, UIStateItem } from "../../type";
+import { PROMPT } from "./shared";
+import FlightCard from "./ui/flight-card";
+import { getFlightInfo } from "./tools";
+
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY,
+});
+
+export async function askFlightInformation(
+ userInput: string,
+): Promise {
+ "use server";
+ const aiState = getMutableAIState();
+
+ // Update the AI state with the new user message.
+ const newUserMessage: Message = {
+ id: nanoid(),
+ role: "user",
+ content: userInput,
+ };
+ aiState.update({
+ ...aiState.get(),
+ messages: [...aiState.get().messages, newUserMessage],
+ });
+
+ // Creates a generated, streamable UI.
+ const ui = render({
+ model: "gpt-4-0125-preview",
+ provider: openai,
+ initial: ,
+ messages: [
+ { role: "system", content: PROMPT },
+ ...aiState.get().messages.map((message: any) => ({
+ role: message.role,
+ content: message.content,
+ name: message.name,
+ })),
+ ],
+ tools: {
+ get_flight_info: {
+ description: "Get the information for a flight",
+ parameters: z
+ .object({
+ flightNumber: z.string().describe("the number of the flight"),
+ })
+ .required(),
+ render: async function* ({ flightNumber }) {
+ // Show a spinner on the client while we wait for the response.
+ yield ;
+
+ // Fetch the flight information from an external API.
+ const flightInfo = await getFlightInfo(flightNumber);
+
+ // Update the final AI state.
+ const toolResponseMessage: Message = {
+ id: nanoid(),
+ role: "function",
+ name: "get_flight_info",
+ content: JSON.stringify(flightInfo),
+ };
+ aiState.done({
+ ...aiState.get(),
+ messages: [...aiState.get().messages, toolResponseMessage],
+ });
+
+ // Return the flight card to the client.
+ return ;
+ },
+ },
+ },
+ // In case, tools have not been used, fallback to response from the LLM.
+ text: ({ content, done }) => {
+ if (done) {
+ const assistantResponseMessage: Message = {
+ id: nanoid(),
+ role: "assistant",
+ content,
+ };
+ aiState.done({
+ ...aiState.get(),
+ messages: [...aiState.get().messages, assistantResponseMessage],
+ });
+ }
+ return ;
+ },
+ });
+
+ return {
+ id: nanoid(),
+ display: ui,
+ };
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/shared.ts b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/shared.ts
new file mode 100644
index 0000000..c5d21c3
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/shared.ts
@@ -0,0 +1,14 @@
+export const PROMPT = `\
+You are a flight assistant bot and you can help users with flight information.
+You can also help users with other flight-related queries.
+
+If the user requests information about a flight, call \`get_flight_info\` to show the flight information.
+
+Besides that, you can also chat with users and do some calculations if needed.
+`;
+
+export interface FlightInfo {
+ flightNumber: string;
+ departure: string;
+ arrival: string;
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/tools.ts b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/tools.ts
new file mode 100644
index 0000000..cea9088
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/tools.ts
@@ -0,0 +1,10 @@
+import { FlightInfo } from "./shared";
+
+export async function getFlightInfo(flightNumber: string): Promise {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ return {
+ flightNumber,
+ departure: "New York",
+ arrival: "San Francisco",
+ };
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/ui/flight-card.tsx b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/ui/flight-card.tsx
new file mode 100644
index 0000000..39badea
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/flight-assistant/ui/flight-card.tsx
@@ -0,0 +1,23 @@
+import ChatAvatar from "@/app/components/ui/chat/chat-avatar";
+import { Plane } from "lucide-react";
+import { FlightInfo } from "../shared";
+
+export default function FlightCard({ flightInfo }: { flightInfo: FlightInfo }) {
+ return (
+
+
+
+
+
+
+
Flight Number: {flightInfo.flightNumber}
+
Departure: {flightInfo.departure}
+
Arrival: {flightInfo.arrival}
+
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/index.ts b/nextjs-llamaindex-agent/app/rsc/actions/index.ts
new file mode 100644
index 0000000..a751fe1
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/index.ts
@@ -0,0 +1,2 @@
+export * from "./flight-assistant";
+export * from "./wiki-assistant";
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/index.tsx b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/index.tsx
new file mode 100644
index 0000000..db4489a
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/index.tsx
@@ -0,0 +1,71 @@
+import ChatLoading from "@/app/components/ui/chat/chat-loading";
+import ChatMessage from "@/app/components/ui/chat/chat-message";
+import { trimStartOfStreamHelper } from "ai";
+import { createStreamableUI, getMutableAIState } from "ai/rsc";
+import { MessageType, OpenAI } from "llamaindex";
+import { nanoid } from "nanoid";
+import { createChatEngine } from "../../engine/chat";
+import { type AI } from "../../index";
+import { Message, UIStateItem } from "../../type";
+import { MAX_TOKEN, runAsyncFnWithoutBlocking } from "./shared";
+
+const llm = new OpenAI({
+ model: (process.env.MODEL as any) ?? "gpt-3.5-turbo",
+ maxTokens: MAX_TOKEN,
+});
+
+export async function submitUserMessage(
+ userInput: string,
+): Promise {
+ "use server";
+ const aiState = getMutableAIState();
+
+ // Update the AI state with the new user message.
+ const newUserMessage: Message = {
+ id: nanoid(),
+ role: "user",
+ content: userInput,
+ };
+ aiState.update({
+ ...aiState.get(),
+ messages: [...aiState.get().messages, newUserMessage],
+ });
+
+ // Init the UI stream with a loading component
+ const uiStream = createStreamableUI();
+
+ // Create a ChatEngine instance that use LlamaIndex's Tools
+ const chatEngine = await createChatEngine(llm);
+
+ // Calling LlamaIndex's ChatEngine to get a streamed response
+ const response = await chatEngine.chat({
+ message: userInput,
+ chatHistory: aiState.get().messages.map((m) => ({
+ role: m.role as MessageType,
+ content: m.content,
+ })),
+ stream: true,
+ });
+
+ // Update the UI stream with the response
+ const it = response.response[Symbol.asyncIterator]();
+ const trimStartOfStream = trimStartOfStreamHelper();
+ runAsyncFnWithoutBlocking(async () => {
+ // TODO: if result come from a tool, it's not text plain and should be handled differently
+ let result = "";
+ while (true) {
+ const { value, done } = await it.next();
+ if (done) break;
+ const text = trimStartOfStream(value.response ?? "");
+ if (text) {
+ result += text;
+ uiStream.update();
+ }
+ }
+ });
+
+ return {
+ id: nanoid(),
+ display: uiStream.value,
+ };
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/shared.ts b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/shared.ts
new file mode 100644
index 0000000..95b5f54
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/shared.ts
@@ -0,0 +1,8 @@
+export const MAX_TOKEN = 512;
+
+export const runAsyncFnWithoutBlocking = (
+ // eslint-disable-next-line no-unused-vars
+ fn: (...args: any) => Promise,
+) => {
+ fn();
+};
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/tools.ts b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/tools.ts
new file mode 100644
index 0000000..7483224
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/tools.ts
@@ -0,0 +1 @@
+/** You can put other wiki assistant functions here, eg. call an external API,... */
diff --git a/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/ui/flight-card.tsx b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/ui/flight-card.tsx
new file mode 100644
index 0000000..2da8d8d
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/actions/wiki-assistant/ui/flight-card.tsx
@@ -0,0 +1,12 @@
+import ChatAvatar from "@/app/components/ui/chat/chat-avatar";
+
+export default function WikiSummaryCard() {
+ return (
+
+
+
+ Implement later a card that displays the summary of a wiki article.
+
+
+ );
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/engine/chat.ts b/nextjs-llamaindex-agent/app/rsc/engine/chat.ts
new file mode 100644
index 0000000..b18bba6
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/engine/chat.ts
@@ -0,0 +1,27 @@
+// import config from "@/config/tools.json";
+import { OpenAI, OpenAIAgent, QueryEngineTool } from "llamaindex";
+import { STORAGE_CACHE_DIR } from "./constants.mjs";
+import { getDataSource } from "./index";
+
+export async function createChatEngine(llm: OpenAI) {
+ const index = await getDataSource(llm);
+ const queryEngine = index.asQueryEngine();
+ const queryEngineTool = new QueryEngineTool({
+ queryEngine: queryEngine,
+ metadata: {
+ name: "data_query_engine",
+ description: `A query engine for documents in storage folder: ${STORAGE_CACHE_DIR}`,
+ },
+ });
+
+ // TODO: Use ToolFactory when it's available in LlamaIndex release
+ // const externalTools = await ToolFactory.createTools(config);
+
+ const agent = new OpenAIAgent({
+ tools: [queryEngineTool],
+ verbose: true,
+ llm,
+ });
+
+ return agent;
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/engine/constants.mjs b/nextjs-llamaindex-agent/app/rsc/engine/constants.mjs
new file mode 100644
index 0000000..42a8664
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/engine/constants.mjs
@@ -0,0 +1,3 @@
+export const STORAGE_CACHE_DIR = "./cache";
+export const CHUNK_SIZE = 512;
+export const CHUNK_OVERLAP = 20;
diff --git a/nextjs-llamaindex-agent/app/rsc/engine/generate.mjs b/nextjs-llamaindex-agent/app/rsc/engine/generate.mjs
new file mode 100644
index 0000000..5b3987c
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/engine/generate.mjs
@@ -0,0 +1,46 @@
+import {
+ serviceContextFromDefaults,
+ storageContextFromDefaults,
+ VectorStoreIndex,
+} from "llamaindex";
+
+import * as dotenv from "dotenv";
+
+import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
+import { getDocuments } from "./loader.mjs";
+
+// Load environment variables from local .env file
+dotenv.config();
+
+async function getRuntime(func) {
+ const start = Date.now();
+ await func();
+ const end = Date.now();
+ return end - start;
+}
+
+async function generateDatasource(serviceContext) {
+ console.log(`Generating storage context...`);
+ // Split documents, create embeddings and store them in the storage context
+ const ms = await getRuntime(async () => {
+ const storageContext = await storageContextFromDefaults({
+ persistDir: STORAGE_CACHE_DIR,
+ });
+ const documents = await getDocuments();
+ await VectorStoreIndex.fromDocuments(documents, {
+ storageContext,
+ serviceContext,
+ });
+ });
+ console.log(`Storage context successfully generated in ${ms / 1000}s.`);
+}
+
+(async () => {
+ const serviceContext = serviceContextFromDefaults({
+ chunkSize: CHUNK_SIZE,
+ chunkOverlap: CHUNK_OVERLAP,
+ });
+
+ await generateDatasource(serviceContext);
+ console.log("Finished generating storage.");
+})();
diff --git a/nextjs-llamaindex-agent/app/rsc/engine/index.ts b/nextjs-llamaindex-agent/app/rsc/engine/index.ts
new file mode 100644
index 0000000..528d605
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/engine/index.ts
@@ -0,0 +1,32 @@
+import {
+ LLM,
+ serviceContextFromDefaults,
+ SimpleDocumentStore,
+ storageContextFromDefaults,
+ VectorStoreIndex,
+} from "llamaindex";
+import { CHUNK_OVERLAP, CHUNK_SIZE, STORAGE_CACHE_DIR } from "./constants.mjs";
+
+export async function getDataSource(llm: LLM) {
+ const serviceContext = serviceContextFromDefaults({
+ llm,
+ chunkSize: CHUNK_SIZE,
+ chunkOverlap: CHUNK_OVERLAP,
+ });
+ const storageContext = await storageContextFromDefaults({
+ persistDir: `${STORAGE_CACHE_DIR}`,
+ });
+
+ const numberOfDocs = Object.keys(
+ (storageContext.docStore as SimpleDocumentStore).toDict(),
+ ).length;
+ if (numberOfDocs === 0) {
+ throw new Error(
+ `StorageContext is empty - call 'npm run generate' to generate the storage first`,
+ );
+ }
+ return await VectorStoreIndex.init({
+ storageContext,
+ serviceContext,
+ });
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/engine/loader.mjs b/nextjs-llamaindex-agent/app/rsc/engine/loader.mjs
new file mode 100644
index 0000000..3039f34
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/engine/loader.mjs
@@ -0,0 +1,9 @@
+import { SimpleDirectoryReader } from "llamaindex";
+
+export const DATA_DIR = "./data";
+
+export async function getDocuments() {
+ return await new SimpleDirectoryReader().loadData({
+ directoryPath: DATA_DIR,
+ });
+}
diff --git a/nextjs-llamaindex-agent/app/rsc/index.tsx b/nextjs-llamaindex-agent/app/rsc/index.tsx
new file mode 100644
index 0000000..3239856
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/index.tsx
@@ -0,0 +1,12 @@
+import { createAI } from "ai/rsc";
+import { nanoid } from "nanoid";
+import { submitUserMessage } from "./actions/";
+import { AIState, UIState } from "./type";
+
+export const AI = createAI({
+ actions: {
+ submitUserMessage,
+ },
+ initialAIState: { chatId: nanoid(), messages: [] },
+ initialUIState: [],
+});
diff --git a/nextjs-llamaindex-agent/app/rsc/type.ts b/nextjs-llamaindex-agent/app/rsc/type.ts
new file mode 100644
index 0000000..8c4237c
--- /dev/null
+++ b/nextjs-llamaindex-agent/app/rsc/type.ts
@@ -0,0 +1,18 @@
+export type Message = {
+ role: "user" | "assistant" | "system" | "function" | "data" | "tool";
+ content: string;
+ id: string;
+ name?: string;
+};
+
+export type AIState = {
+ chatId: string;
+ messages: Message[];
+};
+
+export type UIStateItem = {
+ id: string;
+ display: React.ReactNode;
+};
+
+export type UIState = UIStateItem[];
diff --git a/nextjs-llamaindex-agent/config/tools.json b/nextjs-llamaindex-agent/config/tools.json
new file mode 100644
index 0000000..6fc6dc3
--- /dev/null
+++ b/nextjs-llamaindex-agent/config/tools.json
@@ -0,0 +1,3 @@
+{
+ "wikipedia.WikipediaToolSpec": {}
+}
\ No newline at end of file
diff --git a/nextjs-llamaindex-agent/data/101.pdf b/nextjs-llamaindex-agent/data/101.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..ae5acffd5398b7c59e2df9e6dead2d99128b719c
GIT binary patch
literal 47931
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