-
Notifications
You must be signed in to change notification settings - Fork 365
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: LangGraph integration & example (#734)
* feat: LangGraph example * store messages in server format and use useExternalMessageConverter * add price snapshot tool ui * add purchase stock tool ui use a new algorithm for synching messages * add langgraph package * use langgraph package, clean up code * add changeset * add docs
- Loading branch information
Showing
37 changed files
with
1,488 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@assistant-ui/react-langgraph": patch | ||
--- | ||
|
||
feat: initial release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
--- | ||
title: LangChain LangGraph | ||
--- | ||
|
||
## Overview | ||
|
||
Integration with LangChain's LangGraph server. | ||
|
||
## Requirements | ||
|
||
The state of the graph you are using must have a `messages` key with a list of LangChain-alike messages. | ||
|
||
## Getting Started | ||
|
||
import { Steps, Step } from "fumadocs-ui/components/steps"; | ||
|
||
<Steps> | ||
<Step> | ||
### Create a new assistant-ui project | ||
|
||
```sh | ||
npx assistant-ui@latest create my-app | ||
``` | ||
|
||
You can delete `/app/api/chat/route.ts` from the template, as it is not needed for this example. | ||
|
||
</Step> | ||
<Step> | ||
|
||
### Install dependencies | ||
|
||
```sh npm2yarn | ||
npm install @assistant-ui/react @assistant-ui/react-langgraph @langchain/langgraph-sdk | ||
``` | ||
|
||
</Step> | ||
<Step> | ||
|
||
### Setup helper functions | ||
|
||
<Callout type="warn"> | ||
This example connects to the LangGraph server directly from the browser. For | ||
production use-cases, you should use a proxy server to connect to the | ||
LangGraph server in order to not expose your API key. | ||
</Callout> | ||
|
||
```tsx twoslash include chatApi title="@/lib/chatApi.ts" | ||
// @filename: /lib/chatApi.ts | ||
|
||
// ---cut--- | ||
import { Client } from "@langchain/langgraph-sdk"; | ||
import { LangChainMessage } from "@assistant-ui/react-langgraph"; | ||
|
||
const createClient = () => { | ||
const apiUrl = "https://localhost:8123/api"; | ||
return new Client({ | ||
apiUrl, | ||
}); | ||
}; | ||
|
||
export const createThread = async () => { | ||
const client = createClient(); | ||
return client.threads.create(); | ||
}; | ||
|
||
export const sendMessage = async (params: { | ||
threadId: string; | ||
assistantId: string; | ||
message: LangChainMessage; | ||
}) => { | ||
const client = createClient(); | ||
return client.runs.stream(params.threadId, params.assistantId, { | ||
input: { | ||
messages: [params.message], | ||
}, | ||
streamMode: "messages", | ||
}); | ||
}; | ||
``` | ||
|
||
</Step> | ||
<Step> | ||
|
||
### Define a `MyRuntimeProvider` component | ||
|
||
```tsx twoslash include MyRuntimeProvider title="@/app/MyRuntimeProvider.tsx" | ||
// @filename: /app/MyRuntimeProvider.tsx | ||
// @include: chatApi | ||
|
||
// ---cut--- | ||
"use client"; | ||
|
||
import { useRef } from "react"; | ||
import { AssistantRuntimeProvider } from "@assistant-ui/react"; | ||
import { useLangChainLangGraphRuntime } from "@assistant-ui/react-langgraph"; | ||
import { createThread, sendMessage } from "@/lib/chatApi"; | ||
|
||
export function MyRuntimeProvider({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
const threadIdRef = useRef<string | undefined>(); | ||
const assistant = useLangChainLangGraphRuntime({ | ||
threadId: threadIdRef.current, | ||
stream: async (message) => { | ||
if (!threadIdRef.current) { | ||
const { thread_id } = await createThread(); | ||
threadIdRef.current = thread_id; | ||
} | ||
const threadId = threadIdRef.current; | ||
return sendMessage({ | ||
threadId, | ||
assistantId: process.env["NEXT_PUBLIC_LANGGRAPH_GRAPH_ID"]!, | ||
message, | ||
}); | ||
}, | ||
}); | ||
|
||
return ( | ||
<AssistantRuntimeProvider runtime={runtime}> | ||
{children} | ||
</AssistantRuntimeProvider> | ||
); | ||
} | ||
``` | ||
|
||
</Step> | ||
<Step> | ||
|
||
### Wrap your app in `MyRuntimeProvider` | ||
|
||
```tsx twoslash title="@/app/layout.tsx" {1,11,17} | ||
// @include: MyRuntimeProvider | ||
// @filename: /app/layout.tsx | ||
// ---cut--- | ||
import { MyRuntimeProvider } from "@/app/MyRuntimeProvider"; | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
return ( | ||
<MyRuntimeProvider> | ||
<html lang="en"> | ||
<body>{children}</body> | ||
</html> | ||
</MyRuntimeProvider> | ||
); | ||
} | ||
``` | ||
|
||
</Step> | ||
</Steps> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "next/core-web-vitals" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.vercel |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
"use client"; | ||
|
||
import { AssistantRuntimeProvider } from "@assistant-ui/react"; | ||
import { | ||
useLangChainLangGraphRuntime, | ||
LangChainMessage, | ||
} from "@assistant-ui/react-langgraph"; | ||
import { useRef } from "react"; | ||
import { createThread, sendMessage } from "@/lib/chatApi"; | ||
|
||
export function MyRuntimeProvider({ | ||
children, | ||
}: Readonly<{ | ||
children: React.ReactNode; | ||
}>) { | ||
const threadIdRef = useRef<string | undefined>(); | ||
const runtime = useLangChainLangGraphRuntime({ | ||
threadId: threadIdRef.current, | ||
stream: async (message) => { | ||
if (!threadIdRef.current) { | ||
const { thread_id } = await createThread(); | ||
threadIdRef.current = thread_id; | ||
} | ||
const threadId = threadIdRef.current; | ||
return sendMessage({ | ||
threadId, | ||
assistantId: process.env["NEXT_PUBLIC_LANGGRAPH_GRAPH_ID"] as string, | ||
message, | ||
model: "openai", | ||
userId: "", | ||
systemInstructions: "", | ||
}); | ||
}, | ||
}); | ||
|
||
return ( | ||
<AssistantRuntimeProvider runtime={runtime}> | ||
{children} | ||
</AssistantRuntimeProvider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { NextRequest, NextResponse } from "next/server"; | ||
|
||
export const runtime = "edge"; | ||
|
||
function getCorsHeaders() { | ||
return { | ||
"Access-Control-Allow-Origin": "*", | ||
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", | ||
"Access-Control-Allow-Headers": "*", | ||
}; | ||
} | ||
|
||
async function handleRequest(req: NextRequest, method: string) { | ||
try { | ||
const path = req.nextUrl.pathname.replace(/^\/?api\//, ""); | ||
const url = new URL(req.url); | ||
const searchParams = new URLSearchParams(url.search); | ||
searchParams.delete("_path"); | ||
searchParams.delete("nxtP_path"); | ||
const queryString = searchParams.toString() | ||
? `?${searchParams.toString()}` | ||
: ""; | ||
|
||
const options: RequestInit = { | ||
method, | ||
headers: { | ||
"x-api-key": process.env["LANGCHAIN_API_KEY"] || "", | ||
}, | ||
}; | ||
|
||
if (["POST", "PUT", "PATCH"].includes(method)) { | ||
options.body = await req.text(); | ||
} | ||
|
||
const res = await fetch( | ||
`${process.env["LANGGRAPH_API_URL"]}/${path}${queryString}`, | ||
options, | ||
); | ||
|
||
return new NextResponse(res.body, { | ||
status: res.status, | ||
statusText: res.statusText, | ||
headers: { | ||
...res.headers, | ||
...getCorsHeaders(), | ||
}, | ||
}); | ||
} catch (e: any) { | ||
return NextResponse.json({ error: e.message }, { status: e.status ?? 500 }); | ||
} | ||
} | ||
|
||
export const GET = (req: NextRequest) => handleRequest(req, "GET"); | ||
export const POST = (req: NextRequest) => handleRequest(req, "POST"); | ||
export const PUT = (req: NextRequest) => handleRequest(req, "PUT"); | ||
export const PATCH = (req: NextRequest) => handleRequest(req, "PATCH"); | ||
export const DELETE = (req: NextRequest) => handleRequest(req, "DELETE"); | ||
|
||
// Add a new OPTIONS handler | ||
export const OPTIONS = () => { | ||
return new NextResponse(null, { | ||
status: 204, | ||
headers: { | ||
...getCorsHeaders(), | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; | ||
|
||
@layer base { | ||
:root { | ||
--background: 0 0% 100%; | ||
--foreground: 240 10% 3.9%; | ||
|
||
--card: 0 0% 100%; | ||
--card-foreground: 240 10% 3.9%; | ||
|
||
--popover: 0 0% 100%; | ||
--popover-foreground: 240 10% 3.9%; | ||
|
||
--primary: 240 5.9% 10%; | ||
--primary-foreground: 0 0% 98%; | ||
|
||
--secondary: 240 4.8% 95.9%; | ||
--secondary-foreground: 240 5.9% 10%; | ||
|
||
--muted: 240 4.8% 95.9%; | ||
--muted-foreground: 240 3.8% 46.1%; | ||
|
||
--accent: 240 4.8% 95.9%; | ||
--accent-foreground: 240 5.9% 10%; | ||
|
||
--destructive: 0 84.2% 60.2%; | ||
--destructive-foreground: 0 0% 98%; | ||
|
||
--border: 240 5.9% 90%; | ||
--input: 240 5.9% 90%; | ||
--ring: 240 10% 3.9%; | ||
|
||
--radius: 0.5rem; | ||
} | ||
|
||
.dark { | ||
--background: 240 10% 3.9%; | ||
--foreground: 0 0% 98%; | ||
|
||
--card: 240 10% 3.9%; | ||
--card-foreground: 0 0% 98%; | ||
|
||
--popover: 240 10% 3.9%; | ||
--popover-foreground: 0 0% 98%; | ||
|
||
--primary: 0 0% 98%; | ||
--primary-foreground: 240 5.9% 10%; | ||
|
||
--secondary: 240 3.7% 15.9%; | ||
--secondary-foreground: 0 0% 98%; | ||
|
||
--muted: 240 3.7% 15.9%; | ||
--muted-foreground: 240 5% 64.9%; | ||
|
||
--accent: 240 3.7% 15.9%; | ||
--accent-foreground: 0 0% 98%; | ||
|
||
--destructive: 0 62.8% 30.6%; | ||
--destructive-foreground: 0 0% 98%; | ||
|
||
--border: 240 3.7% 15.9%; | ||
--input: 240 3.7% 15.9%; | ||
--ring: 240 4.9% 83.9%; | ||
} | ||
} | ||
|
||
@layer base { | ||
* { | ||
@apply border-border; | ||
} | ||
body { | ||
@apply bg-background text-foreground; | ||
} | ||
} |
Oops, something went wrong.