├── .gitignore ├── README.md ├── human_in_the_loop ├── .env.example ├── .gitignore ├── README.md ├── langgraph.json ├── package.json ├── src │ ├── dynamic_breakpoints.ts │ ├── human_in_the_loop.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── intro ├── .env.example ├── .gitignore ├── README.md ├── langgraph.json ├── package.json ├── src │ └── index.ts ├── tsconfig.json └── yarn.lock ├── stockbroker ├── README.md ├── backend │ ├── .env.example │ ├── .gitignore │ ├── langgraph.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── tools.ts │ │ └── types.ts │ └── tsconfig.json ├── frontend │ ├── .env.example │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierrc │ ├── components.json │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── logo.jpeg │ │ ├── next.svg │ │ └── vercel.svg │ ├── src │ │ ├── app │ │ │ ├── api │ │ │ │ └── [..._path] │ │ │ │ │ └── route.ts │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── APIDocsAlert.tsx │ │ │ ├── Alert.tsx │ │ │ ├── ChatInterface.tsx │ │ │ ├── HomeComponent.tsx │ │ │ ├── InputArea.tsx │ │ │ ├── Interrupted.tsx │ │ │ ├── Message.tsx │ │ │ ├── MessageList.tsx │ │ │ ├── Settings.tsx │ │ │ ├── SkeletonMessage.tsx │ │ │ └── ToolCall.tsx │ │ ├── constants.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── chatApi.ts │ │ │ ├── cookies.ts │ │ │ ├── shadcnUtils.ts │ │ │ └── streamHandler.ts │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vercel.json ├── package.json ├── turbo.json └── yarn.lock ├── streaming_messages ├── .env.example ├── .gitignore ├── README.md ├── langgraph.json ├── package.json ├── src │ ├── langgraph_sdk │ │ ├── stream_events.ts │ │ ├── stream_messages.ts │ │ ├── stream_updates.ts │ │ └── stream_values.ts │ ├── runnable │ │ ├── graph.ts │ │ ├── stream_events.ts │ │ ├── stream_messages.ts │ │ ├── stream_updates.ts │ │ └── stream_values.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock └── streaming_messages_frontend ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── logo.jpeg ├── next.svg └── vercel.svg ├── src ├── app │ ├── api │ │ └── [..._path] │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── ChatInterface.tsx │ ├── HomeComponent.tsx │ ├── InputArea.tsx │ ├── Interrupted.tsx │ ├── Message.tsx │ ├── MessageList.tsx │ ├── Settings.tsx │ ├── SkeletonMessage.tsx │ └── ToolCall.tsx ├── constants.ts ├── types.ts └── utils │ ├── chatApi.ts │ ├── cookies.ts │ └── streamHandler.ts ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .node_modules/ 3 | built/* 4 | tests/cases/rwc/* 5 | tests/cases/perf/* 6 | !tests/cases/webharness/compilerToString.js 7 | test-args.txt 8 | ~*.docx 9 | \#*\# 10 | .\#* 11 | tests/baselines/local/* 12 | tests/baselines/local.old/* 13 | tests/services/baselines/local/* 14 | tests/baselines/prototyping/local/* 15 | tests/baselines/rwc/* 16 | tests/baselines/reference/projectOutput/* 17 | tests/baselines/local/projectOutput/* 18 | tests/baselines/reference/testresults.tap 19 | tests/services/baselines/prototyping/local/* 20 | tests/services/browser/typescriptServices.js 21 | src/harness/*.js 22 | src/compiler/diagnosticInformationMap.generated.ts 23 | src/compiler/diagnosticMessages.generated.json 24 | src/parser/diagnosticInformationMap.generated.ts 25 | src/parser/diagnosticMessages.generated.json 26 | rwc-report.html 27 | *.swp 28 | build.json 29 | *.actual 30 | tests/webTestServer.js 31 | tests/webTestServer.js.map 32 | tests/webhost/*.d.ts 33 | tests/webhost/webtsc.js 34 | tests/cases/**/*.js 35 | tests/cases/**/*.js.map 36 | *.config 37 | scripts/eslint/built/ 38 | scripts/debug.bat 39 | scripts/run.bat 40 | scripts/**/*.js 41 | scripts/**/*.js.map 42 | coverage/ 43 | internal/ 44 | **/.DS_Store 45 | .settings 46 | **/.vs 47 | **/.vscode/* 48 | !**/.vscode/tasks.json 49 | !**/.vscode/settings.template.json 50 | !**/.vscode/launch.template.json 51 | !**/.vscode/extensions.json 52 | !tests/cases/projects/projectOption/**/node_modules 53 | !tests/cases/projects/NodeModulesSearch/**/* 54 | !tests/baselines/reference/project/nodeModules*/**/* 55 | .idea 56 | yarn-error.log 57 | .parallelperf.* 58 | tests/baselines/reference/dt 59 | .failed-tests 60 | TEST-results.xml 61 | package-lock.json 62 | .eslintcache 63 | *v8.log 64 | /lib/ 65 | dist 66 | .turbo 67 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LangGraph.js Examples 2 | 3 | This repository contains a series of example TypeScript projects which implement LangGraph.js agents. 4 | Each directory focuses on a different problem which LangGraph.js aims to solve/enable solutions for. 5 | 6 | ## Prerequisites 7 | 8 | The following projects all use [LangSmith](https://smith.langchain.com/), LangGraph [Studio](https://github.com/langchain-ai/langgraph-studio) and [Cloud](https://langchain-ai.github.io/langgraph/cloud/), as well as the [LangGraph.js](https://langchain-ai.github.io/langgraphjs/) and [LangChain.js](https://js.langchain.com/v0.2/docs/introduction/) libraries. 9 | 10 | Before jumping into any of the projects, you should create a LangSmith account [here](https://smith.langchain.com/), and download the latest LangGraph Studio version [here](https://github.com/langchain-ai/langgraph-studio/releases/latest). 11 | 12 | Running LangGraph Studio locally requires [Docker](https://www.docker.com/), so ensure you have it installed _and_ running before starting the Studio (I personally use [OrbStack](https://orbstack.dev/) to manage my Docker containers, which is free to use for personal use). 13 | 14 | ## Projects 15 | 16 | - [Intro](./intro/README.md) - Introduction to LangGraph.js, Studio, and Cloud. 17 | - [Human in the Loop](./human_in_the_loop/README.md) - Introduction to Human in the Loop (HITL) concepts. 18 | - [Stockbroker](./stockbroker/README.md) - A full stack stockbroker & financial analyst app, with HITL for purchasing stocks. 19 | - Streaming Messages ([Examples](./streaming_messages/README.md), [Frontend](./streaming_messages_frontend/README.md)) - Next.js web app connected to a LangGraph Cloud deployment to show off different message streaming types. 20 | -------------------------------------------------------------------------------- /human_in_the_loop/.env.example: -------------------------------------------------------------------------------- 1 | # ------------------LangSmith tracing------------------ 2 | LANGCHAIN_API_KEY= 3 | LANGCHAIN_TRACING_V2=true 4 | LANGCHAIN_CALLBACKS_BACKGROUND=true 5 | # ----------------------------------------------------- 6 | 7 | OPENAI_API_KEY= 8 | -------------------------------------------------------------------------------- /human_in_the_loop/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | .turbo 4 | .env 5 | pdfs/** 6 | src/generated/** 7 | # LangGraph API 8 | .langgraph_api 9 | -------------------------------------------------------------------------------- /human_in_the_loop/README.md: -------------------------------------------------------------------------------- 1 | # Human in the Loop 2 | 3 | The code for the human in the loop (HITL) conceptual video can be found in this directory. 4 | This directory contains two graphs, located inside the [`dynamic_breakpoints.ts`](./src/dynamic_breakpoints.ts) and [`human_in_the_loop.ts`](./src/human_in_the_loop.ts) files. 5 | 6 | The dynamic breakpoints graph is not set in the LangGraph config, as it's meant to be a demonstration of how to run it programmatically. 7 | Because of this, there is also a `main` function inside the file which contains the logic necessary to invoke the graph, update the state, and then re-invoke the graph, carrying on where it left off. 8 | 9 | ## [YouTube Video](https://www.youtube.com/watch?v=gm-WaPTFQqM) 10 | 11 | ## Setup 12 | 13 | To setup the HITL project, install the dependencies: 14 | 15 | ```bash 16 | yarn install 17 | ``` 18 | 19 | ## Environment variables 20 | 21 | The HITL project only requires an OpenAI API key to run. Sign up here: 22 | 23 | - OpenAI: https://platform.openai.com/signup 24 | 25 | Once you have your API keys, create a `.env` file in this directory and add the following: 26 | 27 | ```bash 28 | OPENAI_API_KEY=YOUR_API_KEY 29 | ``` 30 | 31 | ## Running the dynamic breakpoints graph 32 | 33 | To run the dynamic breakpoints graph, which is not setup to run via LangGraph Studio or Cloud by default --this can be easily changed by adding a new graph to the `graphs` field of the LangGraph config file, commenting out the `main` function, and the `checkpointer` passed to the graph where `.compile({})` is called-- you only need to run a single script: 34 | 35 | ```bash 36 | yarn start:dynamic_breakpoints 37 | ``` 38 | 39 | This should output roughly the following to the terminal: 40 | 41 |
42 | Show terminal output 43 | 44 | ```txt 45 | Event: agent 46 | 47 | 48 | ---INTERRUPTING GRAPH TO UPDATE STATE--- 49 | 50 | 51 | ---refundAuthorized value before state update--- undefined 52 | ---refundAuthorized value after state update--- true 53 | 54 | ---CONTINUING GRAPH AFTER STATE UPDATE--- 55 | 56 | 57 | Event: tools 58 | { role: 'tool', content: 'Successfully processed refund for 123' } 59 | 60 | Event: agent 61 | { 62 | role: 'ai', 63 | content: 'Your refund for order no. 123 has been successfully processed. If you have any other questions or need further assistance, feel free to ask!' 64 | } 65 | ``` 66 | 67 |
68 | 69 | ## LangGraph Config 70 | 71 | The LangGraph configuration file for the HITL project is located inside [`langgraph.json`](langgraph.json). This file defines the HITL graph implemented in the project: `human_in_the_loop`. 72 | -------------------------------------------------------------------------------- /human_in_the_loop/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_version": "20", 3 | "dockerfile_lines": [], 4 | "dependencies": ["."], 5 | "graphs": { 6 | "human_in_the_loop": "./src/human_in_the_loop.ts:graph" 7 | }, 8 | "env": ".env" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /human_in_the_loop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "human-in-the-loop", 3 | "description": "LangGraph.js examples of human in the loop graphs.", 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "yarn tsc --project tsconfig.json --outDir dist", 8 | "start:hitl": "tsx --experimental-wasm-modules -r dotenv/config src/human_in_the_loop.ts", 9 | "start:dynamic_breakpoints": "tsx --experimental-wasm-modules -r dotenv/config src/dynamic_breakpoints.ts", 10 | "lint": "eslint src", 11 | "lint:fix": "yarn lint --fix", 12 | "format": "prettier --write \"**/*.ts\"", 13 | "format:check": "prettier --check \"**/*.ts\"" 14 | }, 15 | "author": "Brace Sproul", 16 | "license": "MIT", 17 | "dependencies": { 18 | "@langchain/community": "^0.3.34", 19 | "@langchain/core": "^0.3.42", 20 | "@langchain/langgraph": "^0.2.54", 21 | "@langchain/openai": "^0.4.4", 22 | "zod": "^3.23.8" 23 | }, 24 | "devDependencies": { 25 | "@tsconfig/recommended": "^1.0.2", 26 | "@typescript-eslint/eslint-plugin": "^5.51.0", 27 | "@typescript-eslint/parser": "^5.51.0", 28 | "dotenv": "^16.0.3", 29 | "eslint": "^8.33.0", 30 | "eslint-config-airbnb-base": "^15.0.0", 31 | "eslint-config-prettier": "^8.6.0", 32 | "eslint-plugin-import": "^2.27.5", 33 | "eslint-plugin-prettier": "^4.2.1", 34 | "eslint-plugin-unused-imports": "^3.0.0", 35 | "prettier": "^2.8.3", 36 | "tsx": "^3.12.3", 37 | "typescript": "^5.0.0" 38 | }, 39 | "resolutions": { 40 | "@langchain/core": "0.2.31" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /human_in_the_loop/src/dynamic_breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { 2 | END, 3 | START, 4 | StateGraph, 5 | MessagesAnnotation, 6 | MemorySaver, 7 | Annotation, 8 | NodeInterrupt, 9 | } from "@langchain/langgraph"; 10 | import { type AIMessage } from "@langchain/core/messages"; 11 | import { ChatOpenAI } from "@langchain/openai"; 12 | import { z } from "zod"; 13 | import { tool } from "@langchain/core/tools"; 14 | import { logEvent } from "utils.js"; 15 | 16 | const GraphAnnotation = Annotation.Root({ 17 | ...MessagesAnnotation.spec, 18 | /** 19 | * Whether or not permission has been granted to refund the user. 20 | */ 21 | refundAuthorized: Annotation(), 22 | }); 23 | 24 | const llm = new ChatOpenAI({ 25 | model: "gpt-4o", 26 | temperature: 0, 27 | }); 28 | 29 | const processRefundTool = tool( 30 | (input) => { 31 | return `Successfully processed refund for ${input.productId}`; 32 | }, 33 | { 34 | name: "process_refund", 35 | description: "Process a refund for a given product ID.", 36 | schema: z.object({ 37 | productId: z.string().describe("The ID of the product to be refunded."), 38 | }), 39 | } 40 | ); 41 | 42 | const tools = [processRefundTool]; 43 | 44 | const callTool = async (state: typeof GraphAnnotation.State) => { 45 | const { messages, refundAuthorized } = state; 46 | if (!refundAuthorized) { 47 | throw new NodeInterrupt("Permission to refund is required."); 48 | } 49 | 50 | const lastMessage = messages[messages.length - 1]; 51 | // Cast here since `tool_calls` does not exist on `BaseMessage` 52 | const messageCastAI = lastMessage as AIMessage; 53 | if (messageCastAI._getType() !== "ai" || !messageCastAI.tool_calls?.length) { 54 | throw new Error("No tools were called."); 55 | } 56 | const toolCall = messageCastAI.tool_calls[0]; 57 | 58 | // Invoke the tool to process the refund 59 | const refundResult = await processRefundTool.invoke(toolCall); 60 | 61 | return { 62 | messages: refundResult, 63 | }; 64 | }; 65 | 66 | const callModel = async (state: typeof GraphAnnotation.State) => { 67 | const { messages } = state; 68 | 69 | const llmWithTools = llm.bindTools(tools); 70 | const result = await llmWithTools.invoke(messages); 71 | return { messages: [result] }; 72 | }; 73 | 74 | const shouldContinue = (state: typeof GraphAnnotation.State) => { 75 | const { messages } = state; 76 | 77 | const lastMessage = messages[messages.length - 1]; 78 | // Cast here since `tool_calls` does not exist on `BaseMessage` 79 | const messageCastAI = lastMessage as AIMessage; 80 | if (messageCastAI._getType() !== "ai" || !messageCastAI.tool_calls?.length) { 81 | // LLM did not call any tools, or it's not an AI message, so we should end. 82 | return END; 83 | } 84 | 85 | // Tools are provided, so we should continue. 86 | return "tools"; 87 | }; 88 | 89 | const workflow = new StateGraph(GraphAnnotation) 90 | .addNode("agent", callModel) 91 | .addEdge(START, "agent") 92 | .addNode("tools", callTool) 93 | .addEdge("tools", "agent") 94 | .addConditionalEdges("agent", shouldContinue, ["tools", END]); 95 | 96 | export const graph = workflow.compile({ 97 | checkpointer: new MemorySaver(), 98 | }); 99 | 100 | async function main() { 101 | const config = { 102 | configurable: { thread_id: "refunder_dynamic" }, 103 | streamMode: "updates" as const, 104 | }; 105 | const input = { 106 | messages: [ 107 | { 108 | role: "user", 109 | content: "Can I have a refund for my purchase? Order no. 123", 110 | }, 111 | ], 112 | }; 113 | 114 | for await (const event of await graph.stream(input, config)) { 115 | const key = Object.keys(event)[0]; 116 | if (key) { 117 | console.log(`Event: ${key}\n`); 118 | } 119 | } 120 | 121 | console.log("\n---INTERRUPTING GRAPH TO UPDATE STATE---\n\n"); 122 | 123 | console.log( 124 | "---refundAuthorized value before state update---", 125 | (await graph.getState(config)).values.refundAuthorized 126 | ); 127 | 128 | await graph.updateState(config, { refundAuthorized: true }); 129 | 130 | console.log( 131 | "---refundAuthorized value after state update---", 132 | (await graph.getState(config)).values.refundAuthorized 133 | ); 134 | 135 | console.log("\n---CONTINUING GRAPH AFTER STATE UPDATE---\n\n"); 136 | 137 | for await (const event of await graph.stream(null, config)) { 138 | // Log the event to the terminal 139 | logEvent(event); 140 | } 141 | } 142 | 143 | main(); 144 | -------------------------------------------------------------------------------- /human_in_the_loop/src/human_in_the_loop.ts: -------------------------------------------------------------------------------- 1 | import { 2 | END, 3 | START, 4 | StateGraph, 5 | MessagesAnnotation, 6 | MemorySaver, 7 | Annotation, 8 | } from "@langchain/langgraph"; 9 | import { type AIMessage } from "@langchain/core/messages"; 10 | import { ChatOpenAI } from "@langchain/openai"; 11 | import { z } from "zod"; 12 | import { tool } from "@langchain/core/tools"; 13 | import { logEvent } from "utils.js"; 14 | 15 | const GraphAnnotation = Annotation.Root({ 16 | ...MessagesAnnotation.spec, 17 | /** 18 | * Whether or not permission has been granted to refund the user. 19 | */ 20 | refundAuthorized: Annotation(), 21 | }); 22 | 23 | const llm = new ChatOpenAI({ 24 | model: "gpt-4o", 25 | temperature: 0, 26 | }); 27 | 28 | const processRefundTool = tool( 29 | (input) => { 30 | return `Successfully processed refund for ${input.productId}`; 31 | }, 32 | { 33 | name: "process_refund", 34 | description: "Process a refund for a given product ID.", 35 | schema: z.object({ 36 | productId: z.string().describe("The ID of the product to be refunded."), 37 | }), 38 | } 39 | ); 40 | 41 | const tools = [processRefundTool]; 42 | 43 | const callTool = async (state: typeof GraphAnnotation.State) => { 44 | const { messages, refundAuthorized } = state; 45 | if (!refundAuthorized) { 46 | throw new Error("Permission to refund is required."); 47 | } 48 | 49 | const lastMessage = messages[messages.length - 1]; 50 | // Cast here since `tool_calls` does not exist on `BaseMessage` 51 | const messageCastAI = lastMessage as AIMessage; 52 | if (messageCastAI._getType() !== "ai" || !messageCastAI.tool_calls?.length) { 53 | throw new Error("No tools were called."); 54 | } 55 | const toolCall = messageCastAI.tool_calls[0]; 56 | 57 | // Invoke the tool to process the refund 58 | const refundResult = await processRefundTool.invoke(toolCall); 59 | 60 | return { 61 | messages: refundResult, 62 | }; 63 | }; 64 | 65 | const callModel = async (state: typeof GraphAnnotation.State) => { 66 | const { messages } = state; 67 | 68 | const llmWithTools = llm.bindTools(tools); 69 | const result = await llmWithTools.invoke(messages); 70 | return { messages: [result] }; 71 | }; 72 | 73 | const shouldContinue = (state: typeof GraphAnnotation.State) => { 74 | const { messages } = state; 75 | 76 | const lastMessage = messages[messages.length - 1]; 77 | // Cast here since `tool_calls` does not exist on `BaseMessage` 78 | const messageCastAI = lastMessage as AIMessage; 79 | if (messageCastAI._getType() !== "ai" || !messageCastAI.tool_calls?.length) { 80 | // LLM did not call any tools, or it's not an AI message, so we should end. 81 | return END; 82 | } 83 | 84 | // Tools are provided, so we should continue. 85 | return "tools"; 86 | }; 87 | 88 | const workflow = new StateGraph(GraphAnnotation) 89 | .addNode("agent", callModel) 90 | .addEdge(START, "agent") 91 | .addNode("tools", callTool) 92 | .addEdge("tools", "agent") 93 | .addConditionalEdges("agent", shouldContinue, ["tools", END]); 94 | 95 | export const graph = workflow.compile({ 96 | // Uncomment below to run programmatically. 97 | // checkpointer: new MemorySaver(), 98 | interruptBefore: ["tools"], 99 | }); 100 | 101 | // async function main() { 102 | // const config = { 103 | // configurable: { thread_id: "refunder" }, 104 | // streamMode: "updates" as const, 105 | // }; 106 | // const input = { 107 | // messages: [ 108 | // { 109 | // role: "user", 110 | // content: "Can I have a refund for my purchase? Order no. 123", 111 | // }, 112 | // ], 113 | // }; 114 | 115 | // for await (const event of await graph.stream(input, config)) { 116 | // const key = Object.keys(event)[0]; 117 | // if (key) { 118 | // console.log(`Event: ${key}\n`); 119 | // } 120 | // } 121 | 122 | // console.log("\n---INTERRUPTING GRAPH TO UPDATE STATE---\n\n"); 123 | 124 | // console.log( 125 | // "---refundAuthorized value before state update---", 126 | // (await graph.getState(config)).values.refundAuthorized 127 | // ); 128 | 129 | // await graph.updateState(config, { refundAuthorized: true }); 130 | 131 | // console.log( 132 | // "---refundAuthorized value after state update---", 133 | // (await graph.getState(config)).values.refundAuthorized 134 | // ); 135 | 136 | // console.log("\n---CONTINUING GRAPH AFTER STATE UPDATE---\n\n"); 137 | 138 | // for await (const event of await graph.stream(null, config)) { 139 | // // Log the event to the terminal 140 | // logEvent(event); 141 | // } 142 | // } 143 | 144 | // main(); 145 | -------------------------------------------------------------------------------- /human_in_the_loop/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const logEvent = (event: Record) => { 2 | const key = Object.keys(event)[0]; 3 | if (key) { 4 | console.log(`Event: ${key}`); 5 | if (Array.isArray(event[key].messages)) { 6 | const lastMsg = event[key].messages[event[key].messages.length - 1]; 7 | console.log( 8 | { 9 | role: lastMsg._getType(), 10 | content: lastMsg.content, 11 | }, 12 | "\n" 13 | ); 14 | } else { 15 | console.log( 16 | { 17 | role: event[key].messages._getType(), 18 | content: event[key].messages.content, 19 | }, 20 | "\n" 21 | ); 22 | } 23 | } 24 | }; 25 | 26 | export const logStateUpdate = (newBuildup: string, newPunchline: string) => { 27 | console.log( 28 | "Updating state to:", 29 | { 30 | buildup: newBuildup, 31 | punchline: newPunchline, 32 | }, 33 | "\n" 34 | ); 35 | }; 36 | 37 | export const logLastMessageToolCalls = (lastMessage: Record) => { 38 | console.log("Last message tool calls:", { 39 | name: lastMessage.tool_calls[0].name, 40 | args: lastMessage.tool_calls[0].args, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /human_in_the_loop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": [ 6 | "ES2021", 7 | "ES2022.Object", 8 | "DOM" 9 | ], 10 | "target": "ES2021", 11 | "module": "nodenext", 12 | "sourceMap": true, 13 | "allowSyntheticDefaultImports": true, 14 | "baseUrl": "./src", 15 | "declaration": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedParameters": true, 19 | "useDefineForClassFields": true, 20 | "strictPropertyInitialization": false 21 | }, 22 | "exclude": [ 23 | "node_modules/", 24 | "dist/", 25 | "tests/" 26 | ], 27 | "include": [ 28 | "./src" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /intro/.env.example: -------------------------------------------------------------------------------- 1 | # ------------------LangSmith tracing------------------ 2 | LANGCHAIN_API_KEY= 3 | LANGCHAIN_TRACING_V2=true 4 | LANGCHAIN_CALLBACKS_BACKGROUND=true 5 | # ----------------------------------------------------- 6 | 7 | TAVILY_API_KEY= 8 | OPENAI_API_KEY= 9 | -------------------------------------------------------------------------------- /intro/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | .turbo 4 | .env 5 | pdfs/** 6 | src/generated/** 7 | .langgraph_api -------------------------------------------------------------------------------- /intro/README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | This directory contains a simple LangGraph Graph, built for the introduction video in the LangGraph.js video series. 4 | This directory contains a single graph, located inside the `index.ts` file. 5 | 6 | ## [YouTube Video](https://www.youtube.com/watch?v=Qu8BYTnh3K0) 7 | 8 | ## Setup 9 | 10 | To setup the intro project, install the dependencies: 11 | 12 | ```bash 13 | yarn install 14 | ``` 15 | 16 | ## Environment variables 17 | 18 | The intro project requires Tavily and OpenAI API keys to run. Sign up here: 19 | 20 | - OpenAI: https://platform.openai.com/signup 21 | - Tavily: https://tavily.com/ 22 | 23 | Once you have your API keys, create a `.env` file in this directory and add the following: 24 | 25 | ```bash 26 | TAVILY_API_KEY=YOUR_API_KEY 27 | OPENAI_API_KEY=YOUR_API_KEY 28 | ``` 29 | 30 | ## LangGraph Config 31 | 32 | The LangGraph configuration file for the intro project is located inside [`langgraph.json`](langgraph.json). This file defines the single graph implemented in the project: `simple_agent`. -------------------------------------------------------------------------------- /intro/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_version": "20", 3 | "dockerfile_lines": [], 4 | "dependencies": ["."], 5 | "graphs": { 6 | "simple_agent": "./src/index.ts:graph" 7 | }, 8 | "env": "../.env" 9 | } -------------------------------------------------------------------------------- /intro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "financial-expert", 3 | "description": "Financial expert LangGraph.js example", 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "yarn tsc --project tsconfig.json --outDir dist", 8 | "start": "tsx --experimental-wasm-modules -r dotenv/config src/index.ts", 9 | "lint": "eslint src", 10 | "lint:fix": "yarn lint --fix", 11 | "format": "prettier --write \"**/*.ts\"", 12 | "format:check": "prettier --check \"**/*.ts\"" 13 | }, 14 | "author": "Brace Sproul", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@langchain/community": "^0.3.34", 18 | "@langchain/core": "^0.3.42", 19 | "@langchain/langgraph": "^0.2.54", 20 | "@langchain/openai": "^0.4.4", 21 | "zod": "^3.23.8" 22 | }, 23 | "devDependencies": { 24 | "@tsconfig/recommended": "^1.0.2", 25 | "@typescript-eslint/eslint-plugin": "^5.51.0", 26 | "@typescript-eslint/parser": "^5.51.0", 27 | "dotenv": "^16.0.3", 28 | "eslint": "^8.33.0", 29 | "eslint-config-airbnb-base": "^15.0.0", 30 | "eslint-config-prettier": "^8.6.0", 31 | "eslint-plugin-import": "^2.27.5", 32 | "eslint-plugin-prettier": "^4.2.1", 33 | "eslint-plugin-unused-imports": "^3.0.0", 34 | "prettier": "^2.8.3", 35 | "tsx": "^3.12.3", 36 | "typescript": "^5.0.0" 37 | }, 38 | "resolutions": { 39 | "@langchain/core": "0.2.31" 40 | } 41 | } -------------------------------------------------------------------------------- /intro/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ToolNode } from "@langchain/langgraph/prebuilt"; 2 | import { 3 | END, 4 | MessagesAnnotation, 5 | START, 6 | StateGraph, 7 | } from "@langchain/langgraph"; 8 | import { AIMessage, BaseMessage } from "@langchain/core/messages"; 9 | import { ChatOpenAI } from "@langchain/openai"; 10 | import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; 11 | 12 | const llm = new ChatOpenAI({ 13 | model: "gpt-4o", 14 | temperature: 0, 15 | }); 16 | 17 | const webSearchTool = new TavilySearchResults({ 18 | maxResults: 4, 19 | }); 20 | const tools = [webSearchTool]; 21 | 22 | const toolNode = new ToolNode(tools); 23 | 24 | const callModel = async (state: typeof MessagesAnnotation.State) => { 25 | const { messages } = state; 26 | 27 | const llmWithTools = llm.bindTools(tools); 28 | const result = await llmWithTools.invoke(messages); 29 | return { messages: [result] }; 30 | }; 31 | 32 | const shouldContinue = (state: typeof MessagesAnnotation.State) => { 33 | const { messages } = state; 34 | 35 | const lastMessage = messages[messages.length - 1]; 36 | if ( 37 | lastMessage._getType() !== "ai" || 38 | !(lastMessage as AIMessage).tool_calls?.length 39 | ) { 40 | // LLM did not call any tools, or it's not an AI message, so we should end. 41 | return END; 42 | } 43 | return "tools"; 44 | }; 45 | 46 | /** 47 | * MessagesAnnotation is a pre-built state annotation imported from @langchain/langgraph. 48 | * It is the same as the following annotation: 49 | * 50 | * ```typescript 51 | * const MessagesAnnotation = Annotation.Root({ 52 | * messages: Annotation({ 53 | * reducer: messagesStateReducer, 54 | * default: () => [systemMessage], 55 | * }), 56 | * }); 57 | * ``` 58 | */ 59 | const workflow = new StateGraph(MessagesAnnotation) 60 | .addNode("agent", callModel) 61 | .addEdge(START, "agent") 62 | .addNode("tools", toolNode) 63 | .addEdge("tools", "agent") 64 | .addConditionalEdges("agent", shouldContinue, ["tools", END]); 65 | 66 | export const graph = workflow.compile({ 67 | // The LangGraph Studio/Cloud API will automatically add a checkpointer 68 | // only uncomment if running locally 69 | // checkpointer: new MemorySaver(), 70 | }); 71 | -------------------------------------------------------------------------------- /intro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": [ 6 | "ES2021", 7 | "ES2022.Object", 8 | "DOM" 9 | ], 10 | "target": "ES2021", 11 | "module": "nodenext", 12 | "sourceMap": true, 13 | "strict": true, 14 | "allowSyntheticDefaultImports": true, 15 | "baseUrl": "./src", 16 | "declaration": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUnusedParameters": true, 20 | "useDefineForClassFields": true, 21 | "strictPropertyInitialization": false 22 | }, 23 | "exclude": [ 24 | "node_modules/", 25 | "dist/", 26 | "tests/" 27 | ], 28 | "include": [ 29 | "./src" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /stockbroker/README.md: -------------------------------------------------------------------------------- 1 | # Stockbroker Human in the Loop 2 | 3 | The code for the Stockbroker Human in the Loop video can be found in this directory. It's setup as a monorepo-style project, with `frontend` and `backend` directories. 4 | The `frontend` directory contains a Next.js application which allows you to interact with the Stockbroker agent via a chat interface. 5 | The backend contains a LangGraph agent which powers the core functionality of the stockbroker. 6 | 7 | ## Deployment 8 | 9 | The stockbroker agent is publicly accessible through two interfaces: 10 | 11 | 1. API: 12 | > The Cloud API for the stockbroker agent is publicly accessible at the following base URL: `https://stockbrokeragent-bracesprouls-projects.vercel.app/api` 13 | > 14 | > You can find the REST documentation for the stockbroker agent [here](https://stockbrokeragent-bracesprouls-projects.vercel.app/api/docs). 15 | > 16 | > *Note* The rest documentation displays a "base URL" which is not exactly correct. To hit the API, you'll need to append `/api` to the end of the base URL listed. 17 | 18 | 2. Web-based Chat Interface: 19 | > To go along with the API, we've also deployed this web-based chat interface for the stockbroker agent. 20 | > 21 | > You can access, and interact with it [here](https://stockbrokeragent-bracesprouls-projects.vercel.app). 22 | 23 | ## [YouTube Video](https://youtu.be/td7qNK8_H-0) 24 | 25 | ## Setup 26 | 27 | To setup the stockbroker, install dependencies from the root of the monorepo: 28 | 29 | ```bash 30 | yarn install 31 | ``` 32 | 33 | This will install all dependencies required by both the frontend and backend projects. You can also run shared commands from the root of the project: 34 | 35 | ```bash 36 | yarn format 37 | 38 | yarn build 39 | ``` 40 | 41 | ## Environment variables 42 | 43 | ### Backend 44 | 45 | The backend requires Financial Datasets AI, Tavily and OpenAI API keys to run. Sign up here: 46 | 47 | - Financial Datasets AI: https://financialdatasets.ai/ 48 | - Tavily: https://tavily.com/ 49 | - OpenAI: https://platform.openai.com/signup 50 | 51 | Once you have your API keys, create a `.env` file in the [`./backend`](`./backend`) directory and add the following: 52 | 53 | ```bash 54 | FINANCIAL_DATASETS_API_KEY=YOUR_API_KEY 55 | TAVILY_API_KEY=YOUR_API_KEY 56 | OPENAI_API_KEY=YOUR_API_KEY 57 | ``` 58 | 59 | ### Frontend 60 | 61 | The frontend requires the production, or local deployment of your agent, along with a LangSmith API key (if calling the production endpoint), and finally the name of the agent to interact with (in this case `stockbroker`). 62 | 63 | For local development, you can find the API endpoint in the bottom left of LangGraph Studio, which defaults to `http://localhost:51497`. You can find the production URL in the deployment page of your LangGraph cloud deployment. 64 | 65 | Then, set the variables in a `.env` file inside [`./frontend`](./frontend): 66 | 67 | ```bash 68 | # Only required for production deployments 69 | # LANGCHAIN_API_KEY=YOUR_API_KEY 70 | LANGGRAPH_API_URL=http://localhost:51497 71 | NEXT_PUBLIC_API_URL=http://localhost:3000/api # Or your production URL + /api 72 | NEXT_PUBLIC_LANGGRAPH_GRAPH_ID=stockbroker 73 | ``` 74 | 75 | ## LangGraph Config 76 | 77 | The LangGraph configuration file for the stockbroker project is located inside [`./backend/langgraph.json`](./backend/langgraph.json). This file defines the stockbroker graph implemented in the project: `stockbroker`. 78 | -------------------------------------------------------------------------------- /stockbroker/backend/.env.example: -------------------------------------------------------------------------------- 1 | # ------------------LangSmith tracing------------------ 2 | LANGCHAIN_API_KEY= 3 | LANGCHAIN_TRACING_V2=true 4 | LANGCHAIN_CALLBACKS_BACKGROUND=true 5 | # ----------------------------------------------------- 6 | 7 | FINANCIAL_DATASETS_API_KEY= 8 | TAVILY_API_KEY= 9 | OPENAI_API_KEY= 10 | -------------------------------------------------------------------------------- /stockbroker/backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | .turbo 4 | .env 5 | pdfs/** 6 | src/generated/** 7 | # LangGraph API 8 | .langgraph_api 9 | -------------------------------------------------------------------------------- /stockbroker/backend/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "node_version": "20", 3 | "dockerfile_lines": [], 4 | "dependencies": ["."], 5 | "graphs": { 6 | "stockbroker": "./src/index.ts:graph" 7 | }, 8 | "env": ".env" 9 | } -------------------------------------------------------------------------------- /stockbroker/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "financial-expert", 3 | "description": "Financial expert LangGraph.js example", 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "build": "yarn tsc --project tsconfig.json --outDir dist", 8 | "start": "tsx --experimental-wasm-modules -r dotenv/config src/index.ts", 9 | "format": "prettier --write \"**/*.ts\"", 10 | "format:check": "prettier --check \"**/*.ts\"" 11 | }, 12 | "author": "Brace Sproul", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@langchain/community": "^0.3.34", 16 | "@langchain/core": "^0.3.42", 17 | "@langchain/langgraph": "^0.2.54", 18 | "@langchain/openai": "^0.4.4", 19 | "zod": "^3.23.8" 20 | }, 21 | "devDependencies": { 22 | "@tsconfig/recommended": "^1.0.2", 23 | "dotenv": "^16.0.3", 24 | "prettier": "^2.8.3", 25 | "tsx": "^4.19.0", 26 | "typescript": "^5.0.0" 27 | } 28 | } -------------------------------------------------------------------------------- /stockbroker/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ToolNode } from "@langchain/langgraph/prebuilt"; 2 | import { 3 | Annotation, 4 | END, 5 | START, 6 | StateGraph, 7 | NodeInterrupt, 8 | MessagesAnnotation, 9 | } from "@langchain/langgraph"; 10 | import { BaseMessage, type AIMessage } from "@langchain/core/messages"; 11 | import { ChatOpenAI } from "@langchain/openai"; 12 | import { 13 | priceSnapshotTool, 14 | StockPurchase, 15 | ALL_TOOLS_LIST, 16 | webSearchTool, 17 | } from "tools.js"; 18 | import { z } from "zod"; 19 | 20 | const GraphAnnotation = Annotation.Root({ 21 | ...MessagesAnnotation.spec, 22 | requestedStockPurchaseDetails: Annotation, 23 | purchaseConfirmed: Annotation, 24 | }); 25 | 26 | const llm = new ChatOpenAI({ 27 | model: "gpt-4o", 28 | temperature: 0, 29 | }); 30 | 31 | const toolNode = new ToolNode(ALL_TOOLS_LIST); 32 | 33 | const callModel = async (state: typeof GraphAnnotation.State) => { 34 | const { messages } = state; 35 | 36 | const systemMessage = { 37 | role: "system", 38 | content: 39 | "You're an expert financial analyst, tasked with answering the users questions " + 40 | "about a given company or companies. You do not have up to date information on " + 41 | "the companies, so you much call tools when answering users questions. " + 42 | "All financial data tools require a company ticker to be passed in as a parameter. If you " + 43 | "do not know the ticker, you should use the web search tool to find it.", 44 | }; 45 | 46 | const llmWithTools = llm.bindTools(ALL_TOOLS_LIST); 47 | const result = await llmWithTools.invoke([systemMessage, ...messages]); 48 | return { messages: result }; 49 | }; 50 | 51 | const shouldContinue = (state: typeof GraphAnnotation.State) => { 52 | const { messages, requestedStockPurchaseDetails } = state; 53 | 54 | const lastMessage = messages[messages.length - 1]; 55 | 56 | // Cast here since `tool_calls` does not exist on `BaseMessage` 57 | const messageCastAI = lastMessage as AIMessage; 58 | if (messageCastAI._getType() !== "ai" || !messageCastAI.tool_calls?.length) { 59 | // LLM did not call any tools, or it's not an AI message, so we should end. 60 | return END; 61 | } 62 | 63 | // If `requestedStockPurchaseDetails` is present, we want to execute the purchase 64 | if (requestedStockPurchaseDetails) { 65 | return "execute_purchase"; 66 | } 67 | 68 | const { tool_calls } = messageCastAI; 69 | if (!tool_calls?.length) { 70 | throw new Error( 71 | "Expected tool_calls to be an array with at least one element" 72 | ); 73 | } 74 | 75 | return tool_calls.map((tc) => { 76 | if (tc.name === "purchase_stock") { 77 | // The user is trying to purchase a stock, route to the verify purchase node. 78 | return "prepare_purchase_details"; 79 | } else { 80 | return "tools"; 81 | } 82 | }); 83 | }; 84 | 85 | const findCompanyName = async (companyName: string) => { 86 | // Use the web search tool to find the ticker symbol for the company. 87 | const searchResults: string = await webSearchTool.invoke( 88 | `What is the ticker symbol for ${companyName}?` 89 | ); 90 | const llmWithTickerOutput = llm.withStructuredOutput( 91 | z 92 | .object({ 93 | ticker: z.string().describe("The ticker symbol of the company"), 94 | }) 95 | .describe( 96 | `Extract the ticker symbol of ${companyName} from the provided context.` 97 | ), 98 | { name: "extract_ticker" } 99 | ); 100 | const extractedTicker = await llmWithTickerOutput.invoke([ 101 | { 102 | role: "user", 103 | content: `Given the following search results, extract the ticker symbol for ${companyName}:\n${searchResults}`, 104 | }, 105 | ]); 106 | 107 | return extractedTicker.ticker; 108 | }; 109 | 110 | const preparePurchaseDetails = async (state: typeof GraphAnnotation.State) => { 111 | const { messages } = state; 112 | const lastMessage = messages[messages.length - 1]; 113 | if (lastMessage._getType() !== "ai") { 114 | throw new Error("Expected the last message to be an AI message"); 115 | } 116 | 117 | // Cast here since `tool_calls` does not exist on `BaseMessage` 118 | const messageCastAI = lastMessage as AIMessage; 119 | const purchaseStockTool = messageCastAI.tool_calls?.find( 120 | (tc) => tc.name === "purchase_stock" 121 | ); 122 | if (!purchaseStockTool) { 123 | throw new Error( 124 | "Expected the last AI message to have a purchase_stock tool call" 125 | ); 126 | } 127 | let { maxPurchasePrice, companyName, ticker } = purchaseStockTool.args; 128 | 129 | if (!ticker) { 130 | if (!companyName) { 131 | // The user did not provide the ticker or the company name. 132 | // Ask the user for the missing information. Also, if the 133 | // last message had a tool call we need to add a tool message 134 | // to the messages array. 135 | const toolMessages = messageCastAI.tool_calls?.map((tc) => { 136 | return { 137 | role: "tool", 138 | content: `Please provide the missing information for the ${tc.name} tool.`, 139 | id: tc.id, 140 | }; 141 | }); 142 | 143 | return { 144 | messages: [ 145 | ...(toolMessages ?? []), 146 | { 147 | role: "assistant", 148 | content: 149 | "Please provide either the company ticker or the company name to purchase stock.", 150 | }, 151 | ], 152 | }; 153 | } else { 154 | // The user did not provide the ticker, but did provide the company name. 155 | // Call the `findCompanyName` tool to get the ticker. 156 | ticker = await findCompanyName(purchaseStockTool.args.companyName); 157 | } 158 | } 159 | 160 | if (!maxPurchasePrice) { 161 | // If `maxPurchasePrice` is not defined, default to the current price. 162 | const priceSnapshot = await priceSnapshotTool.invoke({ ticker }); 163 | maxPurchasePrice = priceSnapshot.snapshot.price; 164 | } 165 | 166 | // Now we have the final ticker, we can return the purchase information. 167 | return { 168 | requestedStockPurchaseDetails: { 169 | ticker, 170 | quantity: purchaseStockTool.args.quantity ?? 1, // Default to one if not provided. 171 | maxPurchasePrice, 172 | }, 173 | }; 174 | }; 175 | 176 | const executePurchase = async (state: typeof GraphAnnotation.State) => { 177 | const { purchaseConfirmed, requestedStockPurchaseDetails } = state; 178 | if (!requestedStockPurchaseDetails) { 179 | throw new Error("Expected requestedStockPurchaseDetails to be present"); 180 | } 181 | if (!purchaseConfirmed) { 182 | // Interrupt the node to request permission to execute the purchase. 183 | throw new NodeInterrupt("Please confirm the purchase before executing."); 184 | } 185 | 186 | const { ticker, quantity, maxPurchasePrice } = requestedStockPurchaseDetails; 187 | // Execute the purchase. In this demo we'll just return a success message. 188 | return { 189 | messages: [ 190 | { 191 | role: "assistant", 192 | content: 193 | `Successfully purchased ${quantity} share(s) of ` + 194 | `${ticker} at $${maxPurchasePrice}/share.`, 195 | }, 196 | ], 197 | }; 198 | }; 199 | 200 | const workflow = new StateGraph(GraphAnnotation) 201 | .addNode("agent", callModel) 202 | .addEdge(START, "agent") 203 | .addNode("tools", toolNode) 204 | .addNode("prepare_purchase_details", preparePurchaseDetails) 205 | .addNode("execute_purchase", executePurchase) 206 | .addEdge("prepare_purchase_details", "execute_purchase") 207 | .addEdge("execute_purchase", END) 208 | .addEdge("tools", "agent") 209 | .addConditionalEdges("agent", shouldContinue, [ 210 | "tools", 211 | END, 212 | "prepare_purchase_details", 213 | "execute_purchase", 214 | ]); 215 | 216 | export const graph = workflow.compile({ 217 | // The LangGraph Studio/Cloud API will automatically add a checkpointer 218 | // only uncomment if running locally 219 | // checkpointer: new MemorySaver(), 220 | }); 221 | -------------------------------------------------------------------------------- /stockbroker/backend/src/tools.ts: -------------------------------------------------------------------------------- 1 | import { TavilySearchResults } from "@langchain/community/tools/tavily_search"; 2 | import { tool } from "@langchain/core/tools"; 3 | import { 4 | IncomeStatementsResponse, 5 | BalanceSheetsResponse, 6 | CashFlowStatementsResponse, 7 | CompanyFactsResponse, 8 | SnapshotResponse, 9 | } from "types.js"; 10 | import { z } from "zod"; 11 | 12 | export async function callFinancialDatasetAPI< 13 | Output extends Record = Record 14 | >(fields: { 15 | endpoint: string; 16 | params: Record; 17 | }): Promise { 18 | if (!process.env.FINANCIAL_DATASETS_API_KEY) { 19 | throw new Error("FINANCIAL_DATASETS_API_KEY is not set"); 20 | } 21 | 22 | const baseURL = "https://api.financialdatasets.ai"; 23 | const queryParams = new URLSearchParams(fields.params).toString(); 24 | const url = `${baseURL}${fields.endpoint}?${queryParams}`; 25 | const response = await fetch(url, { 26 | method: "GET", 27 | headers: { 28 | "X-API-KEY": process.env.FINANCIAL_DATASETS_API_KEY, 29 | }, 30 | }); 31 | 32 | if (!response.ok) { 33 | let res: string; 34 | try { 35 | res = JSON.stringify(await response.json(), null, 2); 36 | } catch (_) { 37 | try { 38 | res = await response.text(); 39 | } catch (_) { 40 | res = response.statusText; 41 | } 42 | } 43 | throw new Error( 44 | `Failed to fetch data from ${fields.endpoint}.\nResponse: ${res}` 45 | ); 46 | } 47 | const data = await response.json(); 48 | return data; 49 | } 50 | 51 | const incomeStatementsTool = tool( 52 | async (input) => { 53 | try { 54 | const data = await callFinancialDatasetAPI({ 55 | endpoint: "/financials/income-statements", 56 | params: { 57 | ticker: input.ticker, 58 | period: input.period ?? "annual", 59 | limit: input.limit.toString() ?? "5", 60 | }, 61 | }); 62 | return JSON.stringify(data, null); 63 | } catch (e: any) { 64 | console.warn("Error fetching income statements", e.message); 65 | return `An error occurred while fetching income statements: ${e.message}`; 66 | } 67 | }, 68 | { 69 | name: "income_statements", 70 | description: 71 | "Retrieves income statements for a specified company, showing detailed financial performance over a chosen time period. The output includes key metrics such as revenue, expenses, profits, and per-share data. Specifically, it provides: ticker, date, period type, revenue, cost of revenue, gross profit, operating expenses, income figures (operating, net, EBIT), tax expenses, earnings per share (basic and diluted), dividends per share, and share count information.", 72 | schema: z.object({ 73 | ticker: z.string().describe("The ticker of the stock. Example: 'AAPL'"), 74 | period: z 75 | .enum(["annual", "quarterly", "ttm"]) 76 | .describe("The time period of the income statement. Example: 'annual'") 77 | .optional() 78 | .default("annual"), 79 | limit: z 80 | .number() 81 | .int() 82 | .positive() 83 | .describe("The number of income statements to return. Example: 5") 84 | .optional() 85 | .default(5), 86 | }), 87 | } 88 | ); 89 | 90 | const balanceSheetsTool = tool( 91 | async (input) => { 92 | try { 93 | const data = await callFinancialDatasetAPI({ 94 | endpoint: "/financials/balance-sheets", 95 | params: { 96 | ticker: input.ticker, 97 | period: input.period ?? "annual", 98 | limit: input.limit.toString() ?? "5", 99 | }, 100 | }); 101 | return JSON.stringify(data, null); 102 | } catch (e: any) { 103 | console.warn("Error fetching balance sheets", e.message); 104 | return `An error occurred while fetching balance sheets: ${e.message}`; 105 | } 106 | }, 107 | { 108 | name: "balance_sheets", 109 | description: 110 | "Fetches balance sheets for a given company, providing a snapshot of its financial position at specific points in time. The output includes detailed information on assets (total, current, non-current), liabilities (total, current, non-current), and shareholders' equity. Specific data points include cash and equivalents, inventory, investments, property/plant/equipment, goodwill, debt, payables, retained earnings, and more. The result is a JSON stringified object containing an array of balance sheets.", 111 | schema: z.object({ 112 | ticker: z.string().describe("The ticker of the stock. Example: 'AAPL'"), 113 | period: z 114 | .enum(["annual", "quarterly", "ttm"]) 115 | .describe("The time period of the balance sheet. Example: 'annual'") 116 | .optional() 117 | .default("annual"), 118 | limit: z 119 | .number() 120 | .int() 121 | .positive() 122 | .describe("The number of balance sheets to return. Example: 5") 123 | .optional() 124 | .default(5), 125 | }), 126 | } 127 | ); 128 | 129 | const cashFlowStatementsTool = tool( 130 | async (input) => { 131 | try { 132 | const data = await callFinancialDatasetAPI({ 133 | endpoint: "/financials/cash-flow-statements", 134 | params: { 135 | ticker: input.ticker, 136 | period: input.period ?? "annual", 137 | limit: input.limit.toString() ?? "5", 138 | }, 139 | }); 140 | return JSON.stringify(data, null); 141 | } catch (e: any) { 142 | console.warn("Error fetching cash flow statements", e.message); 143 | return `An error occurred while fetching cash flow statements: ${e.message}`; 144 | } 145 | }, 146 | { 147 | name: "cash_flow_statements", 148 | description: 149 | "Obtains cash flow statements for a company, detailing the inflows and outflows of cash from operating, investing, and financing activities. The result is a JSON stringified object containing an array of cash flow statements. Each statement includes: ticker, date, report period, net cash flows from operations/investing/financing, depreciation and amortization, share-based compensation, capital expenditure, business and investment acquisitions/disposals, debt and equity issuances/repayments, dividends, change in cash and equivalents, and effect of exchange rate changes.", 150 | schema: z.object({ 151 | ticker: z.string().describe("The ticker of the stock. Example: 'AAPL'"), 152 | period: z 153 | .enum(["annual", "quarterly", "ttm"]) 154 | .describe("The period of the cash flow statement. Example: 'annual'") 155 | .optional() 156 | .default("annual"), 157 | limit: z 158 | .number() 159 | .int() 160 | .positive() 161 | .describe("The number of cash flow statements to return. Example: 5") 162 | .optional() 163 | .default(5), 164 | }), 165 | } 166 | ); 167 | 168 | const companyFactsTool = tool( 169 | async (input) => { 170 | try { 171 | const data = await callFinancialDatasetAPI({ 172 | endpoint: "/company/facts", 173 | params: { 174 | ticker: input.ticker, 175 | }, 176 | }); 177 | return JSON.stringify(data, null); 178 | } catch (e: any) { 179 | console.warn("Error fetching company facts", e.message); 180 | return `An error occurred while fetching company facts: ${e.message}`; 181 | } 182 | }, 183 | { 184 | name: "company_facts", 185 | description: 186 | "Provides key facts and information about a specified company. The result is a JSON stringified object containing details such as: ticker symbol, company name, CIK number, market capitalization, number of employees, SIC code and description, website URL, listing date, and whether the company is currently active.", 187 | schema: z.object({ 188 | ticker: z.string().describe("The ticker of the company. Example: 'AAPL'"), 189 | }), 190 | } 191 | ); 192 | 193 | export const priceSnapshotTool = tool( 194 | async (input) => { 195 | try { 196 | const data = await callFinancialDatasetAPI({ 197 | endpoint: "/prices/snapshot", 198 | params: { 199 | ticker: input.ticker, 200 | }, 201 | }); 202 | return JSON.stringify(data, null); 203 | } catch (e: any) { 204 | console.warn("Error fetching price snapshots", e.message); 205 | return `An error occurred while fetching price snapshots: ${e.message}`; 206 | } 207 | }, 208 | { 209 | name: "price_snapshot", 210 | description: 211 | "Retrieves the current stock price and related market data for a given company. The snapshot includes the current price, ticker symbol, day's change in price and percentage, timestamp of the data, and a nanosecond-precision timestamp. This tool should ALWAYS be called before purchasing a stock to ensure the most up-to-date price is used.", 212 | schema: z.object({ 213 | ticker: z.string().describe("The ticker of the company. Example: 'AAPL'"), 214 | }), 215 | } 216 | ); 217 | 218 | const stockPurchaseSchema = z.object({ 219 | ticker: z.string().describe("The ticker of the stock. Example: 'AAPL'"), 220 | quantity: z 221 | .number() 222 | .int() 223 | .positive() 224 | .describe("The quantity of stock to purchase."), 225 | maxPurchasePrice: z 226 | .number() 227 | .positive() 228 | .describe( 229 | "The max price at which to purchase the stock. Defaults to the current price." 230 | ), 231 | }); 232 | 233 | export type StockPurchase = z.infer; 234 | 235 | const purchaseStockTool = tool( 236 | (input) => { 237 | return ( 238 | `Please confirm that you want to purchase ${input.quantity} shares of ${input.ticker} at ` + 239 | `${ 240 | input.maxPurchasePrice 241 | ? `$${input.maxPurchasePrice} per share` 242 | : "the current price" 243 | }.` 244 | ); 245 | }, 246 | { 247 | name: "purchase_stock", 248 | description: 249 | "This tool should be called when a user wants to purchase a stock.", 250 | schema: z.object({ 251 | ticker: z 252 | .string() 253 | .optional() 254 | .describe("The ticker of the stock. Example: 'AAPL'"), 255 | companyName: z 256 | .string() 257 | .optional() 258 | .describe( 259 | "The name of the company. This field should be populated if you do not know the ticker." 260 | ), 261 | quantity: z 262 | .number() 263 | .int() 264 | .positive() 265 | .optional() 266 | .describe("The quantity of stock to purchase. Defaults to 1."), 267 | maxPurchasePrice: z 268 | .number() 269 | .positive() 270 | .optional() 271 | .describe( 272 | "The max price at which to purchase the stock. Defaults to the current price." 273 | ), 274 | }), 275 | } 276 | ); 277 | 278 | export const webSearchTool = new TavilySearchResults({ 279 | maxResults: 2, 280 | }); 281 | 282 | export const ALL_TOOLS_LIST = [ 283 | incomeStatementsTool, 284 | balanceSheetsTool, 285 | cashFlowStatementsTool, 286 | companyFactsTool, 287 | priceSnapshotTool, 288 | purchaseStockTool, 289 | webSearchTool, 290 | ]; 291 | 292 | export const SIMPLE_TOOLS_LIST = [ 293 | incomeStatementsTool, 294 | balanceSheetsTool, 295 | cashFlowStatementsTool, 296 | companyFactsTool, 297 | priceSnapshotTool, 298 | webSearchTool, 299 | ]; 300 | -------------------------------------------------------------------------------- /stockbroker/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ---------------------------------------------------- 3 | * ------------------ /company/facts ------------------ 4 | * ---------------------------------------------------- 5 | */ 6 | export interface CompanyFacts { 7 | ticker: string; 8 | name: string; 9 | cik: string; 10 | market_cap: number; 11 | number_of_employees: number; 12 | sic_code: string; 13 | sic_description: string; 14 | website_url: string; 15 | listing_date: string; 16 | is_active: boolean; 17 | } 18 | 19 | export interface CompanyFactsResponse { 20 | company_facts: CompanyFacts; 21 | } 22 | 23 | /** 24 | * ---------------------------------------------------- 25 | * ---------- /financials/income-statements ----------- 26 | * ---------------------------------------------------- 27 | */ 28 | export interface IncomeStatement { 29 | ticker: string; 30 | calendar_date: string; 31 | report_period: string; 32 | period: "quarterly" | "ttm" | "annual"; 33 | revenue: number; 34 | cost_of_revenue: number; 35 | gross_profit: number; 36 | operating_expense: number; 37 | selling_general_and_administrative_expenses: number; 38 | research_and_development: number; 39 | operating_income: number; 40 | interest_expense: number; 41 | ebit: number; 42 | income_tax_expense: number; 43 | net_income_discontinued_operations: number; 44 | net_income_non_controlling_interests: number; 45 | net_income: number; 46 | net_income_common_stock: number; 47 | preferred_dividends_impact: number; 48 | consolidated_income: number; 49 | earnings_per_share: number; 50 | earnings_per_share_diluted: number; 51 | dividends_per_common_share: number; 52 | weighted_average_shares: number; 53 | weighted_average_shares_diluted: number; 54 | } 55 | 56 | export interface IncomeStatementsResponse { 57 | income_statements: IncomeStatement[]; 58 | } 59 | 60 | /** 61 | * ---------------------------------------------------- 62 | * ------------ /financials/balance-sheets ------------ 63 | * ---------------------------------------------------- 64 | */ 65 | export interface BalanceSheet { 66 | ticker: string; 67 | calendar_date: string; 68 | report_period: string; 69 | period: "quarterly" | "ttm" | "annual"; 70 | total_assets: number; 71 | current_assets: number; 72 | cash_and_equivalents: number; 73 | inventory: number; 74 | current_investments: number; 75 | trade_and_non_trade_receivables: number; 76 | non_current_assets: number; 77 | property_plant_and_equipment: number; 78 | goodwill_and_intangible_assets: number; 79 | investments: number; 80 | non_current_investments: number; 81 | outstanding_shares: number; 82 | tax_assets: number; 83 | total_liabilities: number; 84 | current_liabilities: number; 85 | current_debt: number; 86 | trade_and_non_trade_payables: number; 87 | deferred_revenue: number; 88 | deposit_liabilities: number; 89 | non_current_liabilities: number; 90 | non_current_debt: number; 91 | tax_liabilities: number; 92 | shareholders_equity: number; 93 | retained_earnings: number; 94 | accumulated_other_comprehensive_income: number; 95 | total_debt: number; 96 | } 97 | 98 | export interface BalanceSheetsResponse { 99 | balance_sheets: BalanceSheet[]; 100 | } 101 | 102 | /** 103 | * ---------------------------------------------------- 104 | * --------- /financials/cash-flow-statements --------- 105 | * ---------------------------------------------------- 106 | */ 107 | export interface CashFlowStatement { 108 | ticker: string; 109 | calendar_date: string; 110 | report_period: string; 111 | period: "quarterly" | "ttm" | "annual"; 112 | net_cash_flow_from_operations: number; 113 | depreciation_and_amortization: number; 114 | share_based_compensation: number; 115 | net_cash_flow_from_investing: number; 116 | capital_expenditure: number; 117 | business_acquisitions_and_disposals: number; 118 | investment_acquisitions_and_disposals: number; 119 | net_cash_flow_from_financing: number; 120 | issuance_or_repayment_of_debt_securities: number; 121 | issuance_or_purchase_of_equity_shares: number; 122 | dividends_and_other_cash_distributions: number; 123 | change_in_cash_and_equivalents: number; 124 | effect_of_exchange_rate_changes: number; 125 | } 126 | 127 | export interface CashFlowStatementsResponse { 128 | cash_flow_statements: CashFlowStatement[]; 129 | } 130 | 131 | /** 132 | * ---------------------------------------------------- 133 | * --------- /prices/snapshot --------- 134 | * ---------------------------------------------------- 135 | */ 136 | export interface Snapshot { 137 | price: number; 138 | ticker: string; 139 | day_change: number; 140 | day_change_percent: number; 141 | time: string; 142 | time_nanoseconds: number; 143 | } 144 | 145 | export interface SnapshotResponse { 146 | snapshot: Snapshot; 147 | } 148 | 149 | /* ---------------------------------------------------- */ 150 | -------------------------------------------------------------------------------- /stockbroker/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "lib": [ 6 | "ES2021", 7 | "ES2022.Object", 8 | "DOM" 9 | ], 10 | "target": "ES2021", 11 | "module": "nodenext", 12 | "sourceMap": true, 13 | "allowSyntheticDefaultImports": true, 14 | "baseUrl": "./src", 15 | "declaration": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedParameters": true, 19 | "useDefineForClassFields": true, 20 | "strictPropertyInitialization": false 21 | }, 22 | "exclude": [ 23 | "node_modules/", 24 | "dist/", 25 | "tests/" 26 | ], 27 | "include": [ 28 | "./src" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /stockbroker/frontend/.env.example: -------------------------------------------------------------------------------- 1 | LANGGRAPH_API_URL=http://localhost:8123 2 | NEXT_PUBLIC_API_URL=http://localhost:3000/api 3 | LANGCHAIN_API_KEY=YOUR_API_KEY 4 | NEXT_PUBLIC_LANGGRAPH_GRAPH_ID=YOUR_GRAPH_ID -------------------------------------------------------------------------------- /stockbroker/frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /stockbroker/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | -------------------------------------------------------------------------------- /stockbroker/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "quoteProps": "as-needed", 9 | "jsxSingleQuote": false, 10 | "trailingComma": "es5", 11 | "bracketSpacing": true, 12 | "arrowParens": "always", 13 | "requirePragma": false, 14 | "insertPragma": false, 15 | "proseWrap": "preserve", 16 | "htmlWhitespaceSensitivity": "css", 17 | "vueIndentScriptAndStyle": false, 18 | "endOfLine": "lf" 19 | } 20 | -------------------------------------------------------------------------------- /stockbroker/frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /stockbroker/frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /stockbroker/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streaming_chat_frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --config .prettierrc --write \"src\"" 11 | }, 12 | "dependencies": { 13 | "@langchain/langgraph-sdk": "^0.0.8", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "class-variance-authority": "^0.7.0", 16 | "clsx": "^2.1.1", 17 | "js-cookie": "^3.0.5", 18 | "lucide-react": "^0.438.0", 19 | "next": "14.2.7", 20 | "react": "^18", 21 | "react-dom": "^18", 22 | "react-json-view": "^1.21.3", 23 | "react-markdown": "^9.0.1", 24 | "tailwind-merge": "^2.5.2", 25 | "tailwind-scrollbar-hide": "^1.1.7", 26 | "tailwindcss-animate": "^1.0.7", 27 | "uuid": "^10.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/js-cookie": "^3.0.6", 31 | "@types/node": "^20", 32 | "@types/react": "^18", 33 | "@types/react-dom": "^18", 34 | "@types/uuid": "^10.0.0", 35 | "eslint": "^8", 36 | "eslint-config-next": "14.2.7", 37 | "postcss": "^8", 38 | "prettier": "^3.3.3", 39 | "tailwindcss": "^3.4.1", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /stockbroker/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /stockbroker/frontend/public/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bracesproul/langgraphjs-examples/078212189ce498963cd74ee321c08f4b746fbfdd/stockbroker/frontend/public/logo.jpeg -------------------------------------------------------------------------------- /stockbroker/frontend/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stockbroker/frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/app/api/[..._path]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | export const runtime = "edge"; 4 | 5 | function getCorsHeaders() { 6 | return { 7 | "Access-Control-Allow-Origin": "*", 8 | "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS", 9 | "Access-Control-Allow-Headers": "*", 10 | }; 11 | } 12 | 13 | async function handleRequest(req: NextRequest, method: string) { 14 | try { 15 | const path = req.nextUrl.pathname.replace(/^\/?api\//, ""); 16 | const url = new URL(req.url); 17 | const searchParams = new URLSearchParams(url.search); 18 | searchParams.delete("_path"); 19 | searchParams.delete("nxtP_path"); 20 | const queryString = searchParams.toString() 21 | ? `?${searchParams.toString()}` 22 | : ""; 23 | 24 | const options: RequestInit = { 25 | method, 26 | headers: { 27 | "x-api-key": process.env.LANGCHAIN_API_KEY || "", 28 | }, 29 | }; 30 | 31 | if (["POST", "PUT", "PATCH"].includes(method)) { 32 | options.body = await req.text(); 33 | } 34 | 35 | const res = await fetch( 36 | `${process.env.LANGGRAPH_API_URL}/${path}${queryString}`, 37 | options 38 | ); 39 | 40 | return new NextResponse(res.body, { 41 | status: res.status, 42 | statusText: res.statusText, 43 | headers: { 44 | ...res.headers, 45 | ...getCorsHeaders(), 46 | }, 47 | }); 48 | } catch (e: any) { 49 | return NextResponse.json({ error: e.message }, { status: e.status ?? 500 }); 50 | } 51 | } 52 | 53 | export const GET = (req: NextRequest) => handleRequest(req, "GET"); 54 | export const POST = (req: NextRequest) => handleRequest(req, "POST"); 55 | export const PUT = (req: NextRequest) => handleRequest(req, "PUT"); 56 | export const PATCH = (req: NextRequest) => handleRequest(req, "PATCH"); 57 | export const DELETE = (req: NextRequest) => handleRequest(req, "DELETE"); 58 | 59 | // Add a new OPTIONS handler 60 | export const OPTIONS = () => { 61 | return new NextResponse(null, { 62 | status: 204, 63 | headers: { 64 | ...getCorsHeaders(), 65 | }, 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bracesproul/langgraphjs-examples/078212189ce498963cd74ee321c08f4b746fbfdd/stockbroker/frontend/src/app/favicon.ico -------------------------------------------------------------------------------- /stockbroker/frontend/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | *::-webkit-scrollbar { 19 | display: none; 20 | } 21 | 22 | a { 23 | color: rgb(33, 118, 246); 24 | } 25 | 26 | @layer utilities { 27 | .text-balance { 28 | text-wrap: balance; 29 | } 30 | .no-scrollbar::-webkit-scrollbar { 31 | display: none; 32 | } 33 | 34 | .no-scrollbar { 35 | -ms-overflow-style: none; /* IE and Edge */ 36 | scrollbar-width: none; /* Firefox */ 37 | } 38 | } 39 | 40 | @layer base { 41 | :root { 42 | --background: 0 0% 100%; 43 | --foreground: 0 0% 3.9%; 44 | --card: 0 0% 100%; 45 | --card-foreground: 0 0% 3.9%; 46 | --popover: 0 0% 100%; 47 | --popover-foreground: 0 0% 3.9%; 48 | --primary: 0 0% 9%; 49 | --primary-foreground: 0 0% 98%; 50 | --secondary: 0 0% 96.1%; 51 | --secondary-foreground: 0 0% 9%; 52 | --muted: 0 0% 96.1%; 53 | --muted-foreground: 0 0% 45.1%; 54 | --accent: 0 0% 96.1%; 55 | --accent-foreground: 0 0% 9%; 56 | --destructive: 0 84.2% 60.2%; 57 | --destructive-foreground: 0 0% 98%; 58 | --border: 0 0% 89.8%; 59 | --input: 0 0% 89.8%; 60 | --ring: 0 0% 3.9%; 61 | --chart-1: 12 76% 61%; 62 | --chart-2: 173 58% 39%; 63 | --chart-3: 197 37% 24%; 64 | --chart-4: 43 74% 66%; 65 | --chart-5: 27 87% 67%; 66 | --radius: 0.5rem; 67 | } 68 | .dark { 69 | --background: 0 0% 3.9%; 70 | --foreground: 0 0% 98%; 71 | --card: 0 0% 3.9%; 72 | --card-foreground: 0 0% 98%; 73 | --popover: 0 0% 3.9%; 74 | --popover-foreground: 0 0% 98%; 75 | --primary: 0 0% 98%; 76 | --primary-foreground: 0 0% 9%; 77 | --secondary: 0 0% 14.9%; 78 | --secondary-foreground: 0 0% 98%; 79 | --muted: 0 0% 14.9%; 80 | --muted-foreground: 0 0% 63.9%; 81 | --accent: 0 0% 14.9%; 82 | --accent-foreground: 0 0% 98%; 83 | --destructive: 0 62.8% 30.6%; 84 | --destructive-foreground: 0 0% 98%; 85 | --border: 0 0% 14.9%; 86 | --input: 0 0% 14.9%; 87 | --ring: 0 0% 83.1%; 88 | --chart-1: 220 70% 50%; 89 | --chart-2: 160 60% 45%; 90 | --chart-3: 30 80% 55%; 91 | --chart-4: 280 65% 60%; 92 | --chart-5: 340 75% 55%; 93 | } 94 | } 95 | 96 | @layer base { 97 | * { 98 | @apply border-border; 99 | } 100 | body { 101 | @apply bg-background text-foreground; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Streaming UI chat", 9 | description: "Streaming UI chat", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { APIDocsAlert } from "@/components/APIDocsAlert"; 2 | import ChatInterface from "../components/ChatInterface"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/APIDocsAlert.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { getCookie, setCookie } from "@/utils/cookies"; 5 | import { SEEN_API_TOAST_COOKIE } from "@/constants"; 6 | import { Alert, AlertDescription, AlertTitle } from "@/components/Alert"; 7 | import { Terminal, X, Wifi, Book } from "lucide-react"; 8 | 9 | export function APIDocsAlert() { 10 | const [showAlert, setShowAlert] = useState(false); 11 | const [isClosing, setIsClosing] = useState(false); 12 | 13 | useEffect(() => { 14 | if (showAlert) return; 15 | if (typeof window === "undefined") return; 16 | 17 | const hasClosesApiToast = getCookie(SEEN_API_TOAST_COOKIE); 18 | if (hasClosesApiToast) return; 19 | 20 | setShowAlert(true); 21 | }, []); 22 | 23 | const handleClose = () => { 24 | setIsClosing(true); 25 | setTimeout(() => { 26 | setShowAlert(false); 27 | if (process.env.NODE_ENV === "development") { 28 | console.info( 29 | "API alert closed. Not setting cookie due to development environment." 30 | ); 31 | return; 32 | } 33 | setCookie(SEEN_API_TOAST_COOKIE, "true"); 34 | }, 300); // Match this with the transition duration 35 | }; 36 | 37 | if (!showAlert) { 38 | return null; 39 | } 40 | 41 | return ( 42 | 47 |
48 | 49 | 50 | 51 | Want to use the Stockbroker API? 52 | 53 | 54 | 55 | 62 | 63 |
64 | 65 | 66 | 67 |

68 | Click{" "} 69 | 74 | here 75 | {" "} 76 | to read the Stockbroker docs. 77 |

78 |
79 | 80 | 81 |

82 | {" "} 83 | Or, click{" "} 84 | 89 | here 90 | {" "} 91 | to read the REST API docs. 92 |

93 |
94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/utils/shadcnUtils"; 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = "Alert"; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = "AlertTitle"; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = "AlertDescription"; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/ChatInterface.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef } from "react"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | import MessageList from "./MessageList"; 6 | import InputArea from "./InputArea"; 7 | import HomeComponent from "./HomeComponent"; 8 | import Settings from "./Settings"; 9 | import { Message, Model } from "../types"; 10 | import { handleStreamEvent } from "../utils/streamHandler"; 11 | import { 12 | createAssistant, 13 | createThread, 14 | getThreadState, 15 | sendMessage, 16 | } from "../utils/chatApi"; 17 | import { ASSISTANT_ID_COOKIE } from "@/constants"; 18 | import { getCookie, setCookie } from "@/utils/cookies"; 19 | import { ThreadState } from "@langchain/langgraph-sdk"; 20 | import { GraphInterrupt } from "./Interrupted"; 21 | 22 | export default function ChatInterface() { 23 | const [messages, setMessages] = useState([]); 24 | const [threadId, setThreadId] = useState(null); 25 | const [assistantId, setAssistantId] = useState(null); 26 | const [model, setModel] = useState("gpt-4o-mini" as Model); 27 | const [userId, setUserId] = useState(""); 28 | const [systemInstructions, setSystemInstructions] = useState(""); 29 | const [isLoading, setIsLoading] = useState(false); 30 | const [threadState, setThreadState] = 31 | useState>>(); 32 | const [graphInterrupted, setGraphInterrupted] = useState(false); 33 | const [allowNullMessage, setAllowNullMessage] = useState(false); 34 | 35 | const messageListRef = useRef(null); 36 | 37 | useEffect(() => { 38 | if (typeof window === "undefined") return; 39 | 40 | const initializeChat = async () => { 41 | let assistantId = getCookie(ASSISTANT_ID_COOKIE); 42 | 43 | if (!assistantId) { 44 | const assistant = await createAssistant( 45 | process.env.NEXT_PUBLIC_LANGGRAPH_GRAPH_ID as string 46 | ); 47 | assistantId = assistant.assistant_id as string; 48 | setCookie(ASSISTANT_ID_COOKIE, assistantId); 49 | setAssistantId(assistantId); 50 | // Use the assistant ID as the user ID. 51 | setUserId(assistantId); 52 | } else { 53 | setUserId(assistantId); 54 | } 55 | 56 | const { thread_id } = await createThread(); 57 | setThreadId(thread_id); 58 | setAssistantId(assistantId); 59 | }; 60 | 61 | initializeChat(); 62 | }, []); 63 | 64 | useEffect(() => { 65 | if (messageListRef.current) { 66 | messageListRef.current.scrollTop = messageListRef.current.scrollHeight; 67 | } 68 | }, [messages]); 69 | 70 | const handleSendMessage = async (message: string | null) => { 71 | if (message !== null) { 72 | setMessages([ 73 | ...messages, 74 | { text: message, sender: "user", id: uuidv4() }, 75 | ]); 76 | } 77 | 78 | if (!threadId) { 79 | console.error("Thread ID is not available"); 80 | return; 81 | } 82 | if (!assistantId) { 83 | console.error("Assistant ID is not available"); 84 | return; 85 | } 86 | 87 | try { 88 | setIsLoading(true); 89 | setThreadState(undefined); 90 | setGraphInterrupted(false); 91 | setAllowNullMessage(false); 92 | const response = await sendMessage({ 93 | threadId, 94 | assistantId, 95 | message, 96 | model, 97 | userId, 98 | systemInstructions, 99 | }); 100 | 101 | for await (const chunk of response) { 102 | handleStreamEvent(chunk, setMessages); 103 | } 104 | 105 | // Fetch the current state of the thread 106 | const currentState = await getThreadState(threadId); 107 | setThreadState(currentState); 108 | if (currentState.next.length) { 109 | setGraphInterrupted(true); 110 | } 111 | setIsLoading(false); 112 | } catch (error) { 113 | console.error("Error streaming messages:", error); 114 | setIsLoading(false); 115 | } 116 | }; 117 | 118 | return ( 119 |
120 | 126 | {messages.length === 0 ? ( 127 | 128 | ) : ( 129 |
130 | 131 | {!!graphInterrupted && !!threadState && !!threadId ? ( 132 |
133 | 138 |
139 | ) : null} 140 | {allowNullMessage && ( 141 |
142 | 149 |
150 | )} 151 |
152 | )} 153 | 157 |
158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/HomeComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | const exampleMessages = [ 5 | "What're some facts about Google?", 6 | "How much revenue did Apple make last year?", 7 | "Is McDonald's profitable?", 8 | "What's the current stock price of Tesla?", 9 | ]; 10 | 11 | const HomeComponent: React.FC<{ 12 | onMessageSelect: (message: string) => void; 13 | }> = ({ onMessageSelect }) => { 14 | return ( 15 |
16 | StreamChat 23 |
24 | {exampleMessages.map((message, index) => ( 25 |
onMessageSelect(message)} 29 | > 30 | {message} 31 |
32 | ))} 33 |
34 |
35 | ); 36 | }; 37 | 38 | export default HomeComponent; 39 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/InputArea.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function InputArea({ 4 | onSendMessage, 5 | disabled, 6 | }: { 7 | onSendMessage: (message: string) => void; 8 | disabled: boolean; 9 | }) { 10 | const [input, setInput] = useState(""); 11 | 12 | const handleSubmit = (e: React.FormEvent) => { 13 | if (disabled) return; 14 | e.preventDefault(); 15 | if (input.trim()) { 16 | onSendMessage(input); 17 | setInput(""); 18 | } 19 | }; 20 | 21 | return ( 22 |
23 |
24 | setInput(e.target.value)} 28 | className=" h-full w-full rounded-[30px] px-10 focus:outline-none bg-[#2f2f2f] placeholder-white text-white" 29 | placeholder="Message StreamChat" 30 | /> 31 | 56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/Interrupted.tsx: -------------------------------------------------------------------------------- 1 | import { updateState } from "@/utils/chatApi"; 2 | import { ThreadState } from "@langchain/langgraph-sdk"; 3 | import { useState } from "react"; 4 | 5 | export interface GraphInterruptProps { 6 | threadId: string; 7 | state: ThreadState>; 8 | setAllowNullMessage: (value: boolean) => void; 9 | } 10 | 11 | // The JSON to update state with if the user confirms the purchase. 12 | const CONFIRM_PURCHASE = { 13 | purchaseConfirmed: true, 14 | }; 15 | // The name of the node to update the state as 16 | const PREPARE_PURCHASE_DETAILS_NODE = "prepare_purchase_details"; 17 | 18 | export function GraphInterrupt(props: GraphInterruptProps) { 19 | const [confirmed, setConfirmed] = useState(false); 20 | const [disabled, setDisabled] = useState(false); 21 | const [stateUpdated, setStateUpdated] = useState(false); 22 | 23 | async function callUpdateState() { 24 | setDisabled(true); 25 | await updateState(props.threadId, { 26 | newState: CONFIRM_PURCHASE, 27 | asNode: PREPARE_PURCHASE_DETAILS_NODE, 28 | }); 29 | setDisabled(false); 30 | setStateUpdated(true); 31 | props.setAllowNullMessage(true); 32 | } 33 | 34 | if (stateUpdated) { 35 | return ( 36 |
37 |

State updated.

38 |
39 | ); 40 | } 41 | 42 | return ( 43 |
44 |

45 | Graph interrupted. Next: {props.state.next} 46 |

47 |
48 | 60 | 61 | 72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import Markdown from "react-markdown"; 2 | import ToolCall from "./ToolCall"; 3 | import { ToolCall as ToolCallType } from "../types"; 4 | import { useState, useEffect } from "react"; 5 | 6 | export default function Message({ 7 | text, 8 | sender, 9 | toolCalls, 10 | }: { 11 | text: string; 12 | sender: string; 13 | toolCalls?: ToolCallType[]; 14 | }) { 15 | const isBot = sender === "ai"; 16 | const [isVisible, setIsVisible] = useState(false); 17 | 18 | useEffect(() => { 19 | setIsVisible(true); 20 | }, []); 21 | 22 | return ( 23 |
30 | {isBot && ( 31 | Bot Icon 37 | )} 38 |
45 | {toolCalls && 46 | toolCalls.length > 0 && 47 | toolCalls.map((toolCall) => ( 48 | 49 | ))} 50 | {isBot ? {text} : text} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import Message from "./Message"; 2 | import SkeletonMessage from "./SkeletonMessage"; 3 | import { Message as MessageType } from "../types"; 4 | 5 | export default function MessageList({ 6 | messages, 7 | isLoading, 8 | }: { 9 | messages: MessageType[]; 10 | isLoading: boolean; 11 | }) { 12 | return ( 13 |
14 | {messages.map((message, index) => ( 15 |
16 | 21 |
22 | ))} 23 | {isLoading && } 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /stockbroker/frontend/src/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export type Model = "gpt-4o" | "haiku" | "gpt-4o-mini" | "sonnet-3.5"; 4 | 5 | interface SettingsProps { 6 | onModelChange: (model: Model) => void; 7 | onSystemInstructionsChange: (instructions: string) => void; 8 | currentModel: Model; 9 | currentSystemInstructions: string; 10 | } 11 | 12 | export default function Settings({ 13 | onModelChange, 14 | onSystemInstructionsChange, 15 | currentModel, 16 | currentSystemInstructions, 17 | }: SettingsProps) { 18 | const [isOpen, setIsOpen] = useState(false); 19 | const models: Model[] = ["gpt-4o", "haiku", "gpt-4o-mini", "sonnet-3.5"]; 20 | 21 | return ( 22 |
23 | 29 | {isOpen && ( 30 |
31 |

Model

32 | {models.map((model) => ( 33 | 47 | ))} 48 |

System Instructions

49 |