mirror of
https://github.com/langchain-ai/fullstack-chat-server.git
synced 2026-07-01 00:08:16 -04:00
Initial commit
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
index.cjs
|
||||
index.js
|
||||
index.d.ts
|
||||
node_modules
|
||||
dist
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
.turbo
|
||||
**/.turbo
|
||||
**/.eslintcache
|
||||
|
||||
.env
|
||||
.ipynb_checkpoints
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,24 @@
|
||||
# LangGraph Starter Template
|
||||
|
||||
This repo offers the basic code structure to get started building LangGraph workflows in JavaScript within LangGraph studio.
|
||||
|
||||
## Repo Structure
|
||||
|
||||
```txt
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
├── jest.config.js # Test configuration
|
||||
├── .env # Define environment variables. Can copy over from .env.example
|
||||
├── langgraph.json # LangGraph studio configuration
|
||||
├── my-app
|
||||
│ ├── graph.ts # Graph / workflow definition
|
||||
│ └── index.ts
|
||||
├── package.json # Define the project dependencies
|
||||
├── tests # Add any tests you'd like here
|
||||
│ ├── integration
|
||||
│ │ └── graph.int.test.ts
|
||||
│ └── unit
|
||||
│ └── graph.test.ts
|
||||
├── tsconfig.json # Typescript-specific configuration
|
||||
└── yarn.lock
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./react_agent/graph.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "example-graph",
|
||||
"version": "0.0.1",
|
||||
"description": "A starter template for creating a LangGraph workflow.",
|
||||
"main": "graph.ts",
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
|
||||
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^8.15.0",
|
||||
"@langchain/community": "^0.2.31",
|
||||
"@langchain/langgraph": "^0.1.1",
|
||||
"langchain": "^0.2.17",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"@langchain/core": "^0.2.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@langchain/openai": "^0.2.7",
|
||||
"@tsconfig/recommended": "^1.0.7",
|
||||
"@types/jest": "^29.5.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { AIMessage } from "@langchain/core/messages";
|
||||
import { ChatPromptTemplate } from "@langchain/core/prompts";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { ensureConfigurable } from "./utils/configuration.js";
|
||||
import { StateT, State } from "./utils/state.js";
|
||||
import { TOOLS } from "./utils/tools.js";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
// Define the function that calls the model
|
||||
async function callModel(
|
||||
state: StateT,
|
||||
config: RunnableConfig,
|
||||
): Promise<{ messages: AIMessage[] }> {
|
||||
/**Call the LLM powering our "agent".**/
|
||||
const configuration = ensureConfigurable(config);
|
||||
// Feel free to customize the prompt, model, and other logic!
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
["system", configuration.systemPrompt],
|
||||
["placeholder", "{messages}"],
|
||||
]);
|
||||
const model = (await initChatModel(configuration.modelName)).bindTools(TOOLS);
|
||||
|
||||
const messageValue = await prompt.invoke(
|
||||
{ ...state, system_time: new Date().toISOString() },
|
||||
config,
|
||||
);
|
||||
const response: AIMessage = await model.invoke(messageValue, config);
|
||||
if (state.is_last_step && response.tool_calls) {
|
||||
return {
|
||||
messages: [
|
||||
new AIMessage({
|
||||
id: response.id,
|
||||
content:
|
||||
"Sorry, I could not find an answer to your question in the specified number of steps.",
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
// We return a list, because this will get added to the existing list
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
// Define the function that determines whether to continue or not
|
||||
function routeModelOutput(state: StateT): string {
|
||||
const messages = state.messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
// If the LLM is invoking tools, route there.
|
||||
if ((lastMessage as AIMessage)?.tool_calls?.length || 0 > 0) {
|
||||
return "tools";
|
||||
}
|
||||
// Otherwise end the graph.
|
||||
else {
|
||||
return "__end__";
|
||||
}
|
||||
}
|
||||
|
||||
// Define a new graph
|
||||
const workflow = new StateGraph(State)
|
||||
// Define the two nodes we will cycle between
|
||||
.addNode("callModel", callModel)
|
||||
.addNode("tools", new ToolNode<{ messages: BaseMessage[] }>(TOOLS))
|
||||
// Set the entrypoint as `callModel`
|
||||
// This means that this node is the first one called
|
||||
.addEdge("__start__", "callModel")
|
||||
.addConditionalEdges(
|
||||
// First, we define the edges' source node. We use `callModel`.
|
||||
// This means these are the edges taken after the `callModel` node is called.
|
||||
"callModel",
|
||||
// Next, we pass in the function that will determine the sink node(s), which
|
||||
// will be called after the source node is called.
|
||||
routeModelOutput,
|
||||
)
|
||||
// This means that after `tools` is called, `callModel` node is called next.
|
||||
.addEdge("tools", "callModel");
|
||||
|
||||
// Finally, we compile it!
|
||||
// This compiles it into a graph you can invoke and deploy.
|
||||
export const graph = workflow.compile({
|
||||
interruptBefore: [], // if you want to update the state before calling the tools
|
||||
interruptAfter: [],
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Define the configurable parameters for the agent.
|
||||
*/
|
||||
|
||||
export interface Configuration {
|
||||
systemPrompt: string;
|
||||
modelName: string;
|
||||
scraperToolModelName: string;
|
||||
}
|
||||
|
||||
export function ensureConfigurable(config: any): Configuration {
|
||||
/**
|
||||
* Ensure the defaults are populated.
|
||||
*/
|
||||
const configurable = config.get("configurable") || {};
|
||||
return {
|
||||
systemPrompt:
|
||||
configurable.get(
|
||||
"systemPrompt",
|
||||
"You are a helpful AI assistant.\nSystem time: {systemTime}",
|
||||
) || "",
|
||||
modelName: configurable.get("modelName", "claude-3-5-sonnet-20240620"),
|
||||
scraperToolModelName: configurable.get(
|
||||
"scraperToolModelName",
|
||||
"accounts/fireworks/models/firefunction-v2",
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { messagesStateReducer } from "@langchain/langgraph";
|
||||
|
||||
// This is the primary state of your agent, where you can store any information
|
||||
export const State = Annotation.Root({
|
||||
/**
|
||||
* Messages track the primary execution state of the agent.
|
||||
|
||||
Typically accumulates a pattern of:
|
||||
|
||||
1. HumanMessage - user input
|
||||
2. AIMessage with .tool_calls - agent picking tool(s) to use to collect
|
||||
information
|
||||
3. ToolMessage(s) - the responses (or errors) from the executed tools
|
||||
|
||||
(... repeat steps 2 and 3 as needed ...)
|
||||
4. AIMessage without .tool_calls - agent responding in unstructured
|
||||
format to the user.
|
||||
|
||||
5. HumanMessage - user responds with the next conversational turn.
|
||||
|
||||
(... repeat steps 2-5 as needed ... )
|
||||
|
||||
Merges two lists of messages, updating existing messages by ID.
|
||||
|
||||
By default, this ensures the state is "append-only", unless the
|
||||
new message has the same ID as an existing message.
|
||||
|
||||
Returns:
|
||||
A new list of messages with the messages from \`right\` merged into \`left\`.
|
||||
If a message in \`right\` has the same ID as a message in \`left\`, the
|
||||
message from \`right\` will replace the message from \`left\`.`
|
||||
*/
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: messagesStateReducer,
|
||||
default: () => [],
|
||||
}),
|
||||
/**
|
||||
* Set to 'true' if the step is recursion_limit - 1 (meaning it's the last step before the graph will raise an error)
|
||||
*
|
||||
* This is a 'managed' variable (meaning it is managed by the state machine rather than your code).
|
||||
*/
|
||||
is_last_step: Annotation<boolean>({
|
||||
reducer: (existing: boolean, newValue: boolean) => newValue ?? existing,
|
||||
default: () => false,
|
||||
}),
|
||||
// Feel free to add additional attributes to your state as needed.
|
||||
// Common examples include retrieved documents, extracted entities, API connections, etc.
|
||||
});
|
||||
|
||||
export type StateT = typeof State.State;
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ensureConfigurable } from "./configuration.js";
|
||||
import { ensureConfig } from "@langchain/core/runnables";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { getMessageText } from "./utils.js";
|
||||
import { DynamicStructuredTool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
const scrapeWebpage = new DynamicStructuredTool({
|
||||
name: "scrapeWebpage",
|
||||
description:
|
||||
"Scrape the given webpage and return a summary of text based on the instructions.",
|
||||
schema: z.object({
|
||||
url: z.string().describe("The URL of the webpage to scrape."),
|
||||
instructions: z
|
||||
.string()
|
||||
.describe(
|
||||
"The instructions to give to the scraper. An LLM will be used to respond using the instructions and the scraped text.",
|
||||
),
|
||||
}),
|
||||
func: async ({ url, instructions }): Promise<string> => {
|
||||
const response = await fetch(url);
|
||||
const webText = await response.text();
|
||||
const config = ensureConfig();
|
||||
const configuration = ensureConfigurable(config);
|
||||
const model = await initChatModel(configuration.modelName);
|
||||
const responseMsg = await model.invoke(
|
||||
[
|
||||
[
|
||||
"system",
|
||||
`You are a helpful web scraper AI assistant. You are working in extractive Q&A mode, meaning you refrain from making overly abstractive responses.
|
||||
Respond to the user's instructions.
|
||||
Based on the provided webpage. If you are unable to answer the question, let the user know. Do not guess.
|
||||
Provide citations and direct quotes when possible.
|
||||
|
||||
<webpage_text>
|
||||
${webText}
|
||||
</webpage_text>
|
||||
|
||||
System time: ${new Date().toISOString()}`,
|
||||
],
|
||||
["user", instructions],
|
||||
],
|
||||
config,
|
||||
);
|
||||
return getMessageText(responseMsg);
|
||||
},
|
||||
});
|
||||
|
||||
// Note, in a real use case, you'd want to use a more robust search API.
|
||||
const searchDuckduckgo = new DynamicStructuredTool({
|
||||
name: "searchDuckduckgo",
|
||||
description:
|
||||
"Search DuckDuckGo for the given query and return the JSON response. Results are limited, as this is the free public API.",
|
||||
schema: z.object({
|
||||
query: z.string().describe("The search query to send to DuckDuckGo"),
|
||||
}),
|
||||
func: async ({ query }): Promise<any> => {
|
||||
const response = await fetch(
|
||||
`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json`,
|
||||
);
|
||||
const result = await response.json();
|
||||
|
||||
delete result.meta;
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
const searchWikipedia = new DynamicStructuredTool({
|
||||
name: "searchWikipedia",
|
||||
description:
|
||||
"Search Wikipedia for the given query and return the JSON response.",
|
||||
schema: z.object({
|
||||
query: z.string().describe("The search query to send to Wikipedia"),
|
||||
}),
|
||||
func: async ({ query }): Promise<any> => {
|
||||
const url = "https://en.wikipedia.org/w/api.php";
|
||||
const params = new URLSearchParams({
|
||||
action: "query",
|
||||
list: "search",
|
||||
srsearch: query,
|
||||
format: "json",
|
||||
});
|
||||
const response = await fetch(`${url}?${params}`);
|
||||
return await response.json();
|
||||
},
|
||||
});
|
||||
|
||||
export const TOOLS = [scrapeWebpage, searchDuckduckgo, searchWikipedia];
|
||||
@@ -0,0 +1,14 @@
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
export function getMessageText(msg: BaseMessage): string {
|
||||
/**Get the text content of a message. */
|
||||
const content = msg.content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
} else {
|
||||
const txts = (content as any[]).map((c) =>
|
||||
typeof c === "string" ? c : c.text || "",
|
||||
);
|
||||
return txts.join("").trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { describe, it } from "@jest/globals";
|
||||
describe("Researcher", () => {
|
||||
it("Simple runthrough", async () => {}, 100_000);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { describe, it } from "@jest/globals";
|
||||
describe("Routers", () => {
|
||||
it("Test route", async () => {}, 100_000);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "@tsconfig/recommended",
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": ["ES2021", "ES2022.Object", "DOM"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"useDefineForClassFields": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"strictFunctionTypes": false,
|
||||
"outDir": "dist",
|
||||
"types": ["jest", "node"],
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.js"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user