Skip to content

Commit

Permalink
feat: LangGraph integration & example (#734)
Browse files Browse the repository at this point in the history
* 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
Yonom authored Sep 6, 2024
1 parent 49f04d5 commit 5c1ca35
Show file tree
Hide file tree
Showing 37 changed files with 1,488 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/three-crabs-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@assistant-ui/react-langgraph": patch
---

feat: initial release
155 changes: 155 additions & 0 deletions apps/docs/content/docs/runtimes/langgraph.mdx
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>
4 changes: 3 additions & 1 deletion apps/docs/content/docs/runtimes/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"pages": [
"pick-a-runtime",
"vercel-ai-sdk",
"langgraph",
"langserve",
"external-store",
"custom-rest"
"custom-rest",
"..."
]
}
2 changes: 2 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
"@ai-sdk/provider": "^0.0.22",
"@assistant-ui/react": "workspace:^",
"@assistant-ui/react-ai-sdk": "workspace:^",
"@assistant-ui/react-langgraph": "workspace:^",
"@assistant-ui/react-markdown": "workspace:^",
"@assistant-ui/react-syntax-highlighter": "workspace:^",
"@assistant-ui/tsconfig": "workspace:^",
"@langchain/core": "^0.2.31",
"@langchain/langgraph-sdk": "^0.0.8",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
Expand Down
3 changes: 3 additions & 0 deletions examples/with-langgraph/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
1 change: 1 addition & 0 deletions examples/with-langgraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vercel
41 changes: 41 additions & 0 deletions examples/with-langgraph/app/MyRuntimeProvider.tsx
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>
);
}
67 changes: 67 additions & 0 deletions examples/with-langgraph/app/api/[..._path]/route.ts
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(),
},
});
};
76 changes: 76 additions & 0 deletions examples/with-langgraph/app/globals.css
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;
}
}
Loading

0 comments on commit 5c1ca35

Please sign in to comment.