Initial commit
This commit is contained in:
3
agent/.gitignore
vendored
Normal file
3
agent/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# LangGraph API
|
||||
.langgraph_api
|
||||
dist
|
||||
89
agent/agent.tsx
Normal file
89
agent/agent.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
StateGraph,
|
||||
MessagesAnnotation,
|
||||
START,
|
||||
Annotation,
|
||||
} from "@langchain/langgraph";
|
||||
import { SystemMessage } from "@langchain/core/messages";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { typedUi } from "@langchain/langgraph-sdk/react-ui/server";
|
||||
import { uiMessageReducer } from "@langchain/langgraph-sdk/react-ui/types";
|
||||
import type ComponentMap from "./ui";
|
||||
import { z, ZodTypeAny } from "zod";
|
||||
|
||||
// const llm = new ChatOllama({ model: "deepseek-r1" });
|
||||
const llm = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 });
|
||||
|
||||
interface ToolCall {
|
||||
name: string;
|
||||
args: Record<string, any>;
|
||||
id?: string;
|
||||
type?: "tool_call";
|
||||
}
|
||||
|
||||
function findToolCall<Name extends string>(name: Name) {
|
||||
return <Args extends ZodTypeAny>(
|
||||
x: ToolCall
|
||||
): x is { name: Name; args: z.infer<Args> } => x.name === name;
|
||||
}
|
||||
|
||||
const builder = new StateGraph(
|
||||
Annotation.Root({
|
||||
messages: MessagesAnnotation.spec["messages"],
|
||||
ui: Annotation({ default: () => [], reducer: uiMessageReducer }),
|
||||
timestamp: Annotation<number>,
|
||||
})
|
||||
)
|
||||
.addNode("agent", async (state, config) => {
|
||||
const ui = typedUi<typeof ComponentMap>(config);
|
||||
|
||||
// const result = ui.interrupt("react-component", {
|
||||
// instruction: "Hello world",
|
||||
// });
|
||||
|
||||
// // throw new Error("Random error");
|
||||
// // stream custom events
|
||||
// for (let count = 0; count < 10; count++) config.writer?.({ count });
|
||||
|
||||
// How do I properly assign
|
||||
const stockbrokerSchema = z.object({ company: z.string() });
|
||||
const message = await llm
|
||||
.bindTools([
|
||||
{
|
||||
name: "stockbroker",
|
||||
description: "A tool to get the stock price of a company",
|
||||
schema: stockbrokerSchema,
|
||||
},
|
||||
])
|
||||
.invoke([
|
||||
new SystemMessage(
|
||||
"You are a stockbroker agent that uses tools to get the stock price of a company"
|
||||
),
|
||||
...state.messages,
|
||||
]);
|
||||
|
||||
const stockbrokerToolCall = message.tool_calls?.find(
|
||||
findToolCall("stockbroker")<typeof stockbrokerSchema>
|
||||
);
|
||||
|
||||
if (stockbrokerToolCall) {
|
||||
const instruction = `The stock price of ${
|
||||
stockbrokerToolCall.args.company
|
||||
} is ${Math.random() * 100}`;
|
||||
|
||||
ui.write("react-component", { instruction, logo: "hey" });
|
||||
}
|
||||
|
||||
return { messages: message, ui: ui.collect, timestamp: Date.now() };
|
||||
})
|
||||
.addEdge(START, "agent");
|
||||
|
||||
export const graph = builder.compile();
|
||||
|
||||
// event handler of evetns ˇtypes)
|
||||
// event handler for specific node -> handle node
|
||||
|
||||
// TODO:
|
||||
// - Send run ID & additional metadata for the client to properly use messages (maybe we even have a config)
|
||||
// - Store that run ID in messages
|
||||
1
agent/ui.css
Normal file
1
agent/ui.css
Normal file
@@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
57
agent/ui.tsx
Normal file
57
agent/ui.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import "./ui.css";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
|
||||
import { useState } from "react";
|
||||
|
||||
function ReactComponent(props: { instruction: string; logo: string }) {
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
// useStream should be able to be infered from context
|
||||
const thread = useStream<{ messages: Message[] }, { messages: Message[] }>({
|
||||
assistantId: "assistant_123",
|
||||
apiUrl: "http://localhost:3123",
|
||||
});
|
||||
|
||||
const aiTool = thread.messages
|
||||
.slice()
|
||||
.reverse()
|
||||
.find(
|
||||
(message): message is AIMessage =>
|
||||
message.type === "ai" && !!message.tool_calls?.length
|
||||
);
|
||||
|
||||
const toolCallId = aiTool?.tool_calls?.[0]?.id;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border border-solid border-slate-500 p-4 rounded-md">
|
||||
Request: {props.instruction}
|
||||
<button className="text-left" onClick={() => setCounter(counter + 1)}>
|
||||
Click me
|
||||
</button>
|
||||
<p>Counter: {counter}</p>
|
||||
{toolCallId && (
|
||||
<button
|
||||
className="text-left"
|
||||
onClick={() => {
|
||||
thread.submit({
|
||||
messages: [
|
||||
{
|
||||
type: "tool",
|
||||
tool_call_id: toolCallId!,
|
||||
name: "stockbroker",
|
||||
content: "hey",
|
||||
},
|
||||
{ type: "human", content: `Buy ${counter}` },
|
||||
],
|
||||
});
|
||||
}}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ComponentMap = { "react-component": ReactComponent } as const;
|
||||
export default ComponentMap;
|
||||
Reference in New Issue
Block a user