Initial commit

This commit is contained in:
William FH
2024-08-25 22:45:29 -07:00
committed by William Fu-Hinthorn
commit 8c5ef919eb
18 changed files with 3184 additions and 0 deletions
View File
+19
View File
@@ -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
+21
View File
@@ -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.
+24
View File
@@ -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
```
+15
View File
@@ -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"],
};
+9
View File
@@ -0,0 +1,9 @@
{
"node_version": "20",
"dockerfile_lines": [],
"dependencies": ["."],
"graphs": {
"agent": "./react_agent/graph.ts:graph"
},
"env": ".env"
}
+36
View File
@@ -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"
}
}
+84
View File
@@ -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: [],
});
View File
View File
+28
View File
@@ -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",
),
};
}
+52
View File
@@ -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;
+88
View File
@@ -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];
+14
View File
@@ -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();
}
}
+4
View File
@@ -0,0 +1,4 @@
import { describe, it } from "@jest/globals";
describe("Researcher", () => {
it("Simple runthrough", async () => {}, 100_000);
});
+4
View File
@@ -0,0 +1,4 @@
import { describe, it } from "@jest/globals";
describe("Routers", () => {
it("Test route", async () => {}, 100_000);
});
+24
View File
@@ -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"]
}
+2762
View File
File diff suppressed because it is too large Load Diff