mirror of
https://github.com/langchain-ai/create-agent-chat-app.git
synced 2026-07-01 21:24:02 -04:00
add agents, port script file to ts
This commit is contained in:
@@ -26,8 +26,10 @@
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@tsconfig/recommended": "^1.0.7",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"dotenv": "^16.4.7",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import chalk from "chalk";
|
||||
import { fileURLToPath } from "url";
|
||||
import prompts from "prompts";
|
||||
|
||||
// Get the directory name of the current module
|
||||
const __filename: string = fileURLToPath(import.meta.url);
|
||||
const __dirname: string = path.dirname(__filename);
|
||||
|
||||
interface ProjectAnswers {
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
async function init(): Promise<void> {
|
||||
console.log(`
|
||||
${chalk.green("Welcome to create-agent-chat-app!")}
|
||||
Let's set up your new agent chat application.
|
||||
`);
|
||||
|
||||
// Collect user input
|
||||
const questions: ProjectAnswers = await prompts([
|
||||
{
|
||||
type: "text",
|
||||
name: "projectName",
|
||||
message: "What is the name of your project?",
|
||||
initial: "agent-chat-app",
|
||||
},
|
||||
]);
|
||||
const { projectName } = questions;
|
||||
|
||||
// Create project directory
|
||||
const targetDir: string = path.join(process.cwd(), projectName);
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
console.error(chalk.red(`Error: Directory ${projectName} already exists.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Log the collected values
|
||||
console.log(`Project will be created at: ${chalk.green(targetDir)}\n`);
|
||||
|
||||
// Create the project directory
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
console.log(chalk.yellow("Creating project files..."));
|
||||
|
||||
// Copy all the template files to the target directory
|
||||
const templateDir: string = path.join(__dirname, "template");
|
||||
fs.copySync(templateDir, targetDir);
|
||||
|
||||
// Update package.json with project name
|
||||
const pkgJsonPath: string = path.join(targetDir, "package.json");
|
||||
const pkgJson: Record<string, any> = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
||||
pkgJson.name = projectName;
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
||||
|
||||
console.log(chalk.green("\nSuccess!"));
|
||||
console.log(`
|
||||
Your agent chat app has been created at ${chalk.green(targetDir)}
|
||||
|
||||
To get started:
|
||||
${chalk.cyan(`cd ${projectName}`)}
|
||||
${chalk.cyan("pnpm install")}
|
||||
${chalk.cyan("pnpm dev")}
|
||||
|
||||
This will start a development server at:
|
||||
${chalk.cyan("http://localhost:5173")}
|
||||
`);
|
||||
}
|
||||
|
||||
init().catch((err: Error) => {
|
||||
console.error(chalk.red("Error:"), err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
# LangGraph.js ReAct Memory Agent
|
||||
|
||||
This repo provides a simple example of a ReAct-style agent with a tool to save memories, implemented in JavaScript. This is a straightforward way to allow an agent to persist important information for later use. In this implementation, we save all memories scoped to a configurable `userId`, enabling the bot to learn and remember a user's preferences across different conversational threads.
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
This quickstart will get your memory service deployed on [LangGraph Cloud](https://langchain-ai.github.io/langgraph/cloud/). Once created, you can interact with it from any API.
|
||||
|
||||
Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
|
||||
|
||||
1. Create a `.env` file.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Define required API keys in your `.env` file.
|
||||
|
||||
<!--
|
||||
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
-->
|
||||
|
||||
### Setup Model
|
||||
|
||||
The defaults values for `model` are shown below:
|
||||
|
||||
```yaml
|
||||
model: anthropic/claude-3-5-sonnet-20240620
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Anthropic
|
||||
|
||||
To use Anthropic's chat models:
|
||||
|
||||
1. Sign up for an [Anthropic API key](https://console.anthropic.com/) if you haven't already.
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's chat models:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
<!--
|
||||
End setup instructions
|
||||
-->
|
||||
|
||||
3. Open in LangGraph studio. Navigate to the `memory_agent` graph and have a conversation with it! Try sending some messages saying your name and other things the bot should remember.
|
||||
|
||||
Assuming the bot saved some memories, create a _new_ thread using the `+` icon. Then chat with the bot again - if you've completed your setup correctly, the bot should now have access to the memories you've saved!
|
||||
|
||||
You can review the saved memories by clicking the "memory" button.
|
||||
|
||||

|
||||
|
||||
## How it works
|
||||
|
||||
This chat bot reads from your memory graph's `Store` to easily list extracted memories. If it calls a tool, LangGraph will route to the `store_memory` node to save the information to the store.
|
||||
|
||||
## How to evaluate
|
||||
|
||||
Memory management can be challenging to get right, especially if you add additional tools for the bot to choose between.
|
||||
To tune the frequency and quality of memories your bot is saving, we recommend starting from an evaluation set, adding to it over time as you find and address common errors in your service.
|
||||
|
||||
We have provided a few example evaluation cases in [the test file here](./tests/agent.int.test.ts). As you can see, the metrics themselves don't have to be terribly complicated, especially not at the outset.
|
||||
|
||||
## How to customize
|
||||
|
||||
1. Customize memory content: we've defined a simple memory structure `content: string, context: string` for each memory, but you could structure them in other ways.
|
||||
2. Provide additional tools: the bot will be more useful if you connect it to other functions.
|
||||
3. Select a different model: We default to anthropic/claude-3-5-sonnet-20240620. You can select a compatible chat model using provider/model-name via configuration. Example: openai/gpt-4.
|
||||
4. Customize the prompts: We provide a default prompt in the [prompts.ts](src/memory_agent/prompts.ts) file. You can easily update this via configuration.
|
||||
|
||||
<!--
|
||||
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
{
|
||||
"config_schemas": {
|
||||
"agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"description": "The name of the language model to use for the agent. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
@@ -0,0 +1,21 @@
|
||||
// Define the configurable parameters for the agent
|
||||
|
||||
import { Annotation, LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
import { SYSTEM_PROMPT } from "./prompts.js";
|
||||
|
||||
export const ConfigurationAnnotation = Annotation.Root({
|
||||
userId: Annotation<string>(),
|
||||
model: Annotation<string>(),
|
||||
systemPrompt: Annotation<string>(),
|
||||
});
|
||||
|
||||
export type Configuration = typeof ConfigurationAnnotation.State;
|
||||
|
||||
export function ensureConfiguration(config?: LangGraphRunnableConfig) {
|
||||
const configurable = config?.configurable || {};
|
||||
return {
|
||||
userId: configurable?.userId || "default",
|
||||
model: configurable?.model || "anthropic/claude-3-5-sonnet-20240620",
|
||||
systemPrompt: configurable?.systemPrompt || SYSTEM_PROMPT,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Main graph
|
||||
import {
|
||||
LangGraphRunnableConfig,
|
||||
START,
|
||||
StateGraph,
|
||||
END,
|
||||
} from "@langchain/langgraph";
|
||||
import { BaseMessage, AIMessage } from "@langchain/core/messages";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
import { initializeTools } from "./tools.js";
|
||||
import {
|
||||
ConfigurationAnnotation,
|
||||
ensureConfiguration,
|
||||
} from "./configuration.js";
|
||||
import { GraphAnnotation } from "./state.js";
|
||||
import { getStoreFromConfigOrThrow, splitModelAndProvider } from "./utils.js";
|
||||
|
||||
const llm = await initChatModel();
|
||||
|
||||
async function callModel(
|
||||
state: typeof GraphAnnotation.State,
|
||||
config: LangGraphRunnableConfig,
|
||||
): Promise<{ messages: BaseMessage[] }> {
|
||||
const store = getStoreFromConfigOrThrow(config);
|
||||
const configurable = ensureConfiguration(config);
|
||||
const memories = await store.search(["memories", configurable.userId], {
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
let formatted =
|
||||
memories
|
||||
?.map((mem) => `[${mem.key}]: ${JSON.stringify(mem.value)}`)
|
||||
?.join("\n") || "";
|
||||
if (formatted) {
|
||||
formatted = `\n<memories>\n${formatted}\n</memories>`;
|
||||
}
|
||||
|
||||
const sys = configurable.systemPrompt
|
||||
.replace("{user_info}", formatted)
|
||||
.replace("{time}", new Date().toISOString());
|
||||
|
||||
const tools = initializeTools(config);
|
||||
const boundLLM = llm.bind({
|
||||
tools: tools,
|
||||
tool_choice: "auto",
|
||||
});
|
||||
|
||||
const result = await boundLLM.invoke(
|
||||
[{ role: "system", content: sys }, ...state.messages],
|
||||
{
|
||||
configurable: splitModelAndProvider(configurable.model),
|
||||
},
|
||||
);
|
||||
|
||||
return { messages: [result] };
|
||||
}
|
||||
|
||||
async function storeMemory(
|
||||
state: typeof GraphAnnotation.State,
|
||||
config: LangGraphRunnableConfig,
|
||||
): Promise<{ messages: BaseMessage[] }> {
|
||||
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
|
||||
const toolCalls = lastMessage.tool_calls || [];
|
||||
|
||||
const tools = initializeTools(config);
|
||||
const upsertMemoryTool = tools[0];
|
||||
|
||||
const savedMemories = await Promise.all(
|
||||
toolCalls.map(async (tc) => {
|
||||
return await upsertMemoryTool.invoke(tc);
|
||||
}),
|
||||
);
|
||||
|
||||
return { messages: savedMemories };
|
||||
}
|
||||
|
||||
function routeMessage(
|
||||
state: typeof GraphAnnotation.State,
|
||||
): "store_memory" | typeof END {
|
||||
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
|
||||
if (lastMessage.tool_calls?.length) {
|
||||
return "store_memory";
|
||||
}
|
||||
return END;
|
||||
}
|
||||
|
||||
// Create the graph + all nodes
|
||||
export const builder = new StateGraph(
|
||||
{
|
||||
stateSchema: GraphAnnotation,
|
||||
},
|
||||
ConfigurationAnnotation,
|
||||
)
|
||||
.addNode("call_model", callModel)
|
||||
.addNode("store_memory", storeMemory)
|
||||
.addEdge(START, "call_model")
|
||||
.addConditionalEdges("call_model", routeMessage, {
|
||||
store_memory: "store_memory",
|
||||
[END]: END,
|
||||
})
|
||||
.addEdge("store_memory", "call_model");
|
||||
|
||||
export const graph = builder.compile();
|
||||
graph.name = "MemoryAgent";
|
||||
@@ -0,0 +1,7 @@
|
||||
// Define default prompts
|
||||
|
||||
export const SYSTEM_PROMPT = `You are a helpful and friendly chatbot. Get to know the user! \
|
||||
Ask questions! Be spontaneous!
|
||||
{user_info}
|
||||
|
||||
System Time: {time}`;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import {
|
||||
Annotation,
|
||||
Messages,
|
||||
messagesStateReducer,
|
||||
} from "@langchain/langgraph";
|
||||
|
||||
/**
|
||||
* Main graph state.
|
||||
*/
|
||||
export const GraphAnnotation = Annotation.Root({
|
||||
/**
|
||||
* The messages in the conversation.
|
||||
*/
|
||||
messages: Annotation<BaseMessage[], Messages>({
|
||||
reducer: messagesStateReducer,
|
||||
default: () => [],
|
||||
}),
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 688 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 596 KiB |
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { MemorySaver, InMemoryStore } from "@langchain/langgraph";
|
||||
import { builder } from "../../graph.js";
|
||||
|
||||
describe("Memory Graph", () => {
|
||||
const conversations = [
|
||||
["My name is Alice and I love pizza. Remember this."],
|
||||
[
|
||||
"Hi, I'm Bob and I enjoy playing tennis. Remember this.",
|
||||
"Yes, I also have a pet dog named Max.",
|
||||
"Max is a golden retriever and he's 5 years old. Please remember this too.",
|
||||
],
|
||||
[
|
||||
"Hello, I'm Charlie. I work as a software engineer and I'm passionate about AI. Remember this.",
|
||||
"I specialize in machine learning algorithms and I'm currently working on a project involving natural language processing.",
|
||||
"My main goal is to improve sentiment analysis accuracy in multi-lingual texts. It's challenging but exciting.",
|
||||
"We've made some progress using transformer models, but we're still working on handling context and idioms across languages.",
|
||||
"Chinese and English have been the most challenging pair so far due to their vast differences in structure and cultural contexts.",
|
||||
],
|
||||
];
|
||||
|
||||
it.each(
|
||||
conversations.map((conversation, index) => [
|
||||
["short", "medium", "long"][index],
|
||||
conversation,
|
||||
]),
|
||||
)(
|
||||
"should store memories for %s conversation",
|
||||
async (_, conversation) => {
|
||||
const memStore = new InMemoryStore();
|
||||
const graph = builder.compile({
|
||||
store: memStore,
|
||||
checkpointer: new MemorySaver(),
|
||||
});
|
||||
const userId = "test-user";
|
||||
for (const content of conversation) {
|
||||
await graph.invoke(
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: [{ type: "text", text: content }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
configurable: {
|
||||
userId,
|
||||
thread_id: "thread",
|
||||
model: "gpt-4o-mini",
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const namespace = ["memories", userId];
|
||||
const memories = await memStore.search(namespace);
|
||||
expect(memories.length).toBeGreaterThan(0);
|
||||
|
||||
const badNamespace = ["memories", "wrong-user"];
|
||||
const badMemories = await memStore.search(badNamespace);
|
||||
expect(badMemories.length).toBe(0);
|
||||
},
|
||||
30000,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { ensureConfiguration } from "../../configuration.js";
|
||||
|
||||
describe("Configuration", () => {
|
||||
it("should initialize configuration from an empty object", () => {
|
||||
const emptyConfig = {};
|
||||
const result = ensureConfiguration(emptyConfig);
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe("object");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { graph } from "../../graph.js";
|
||||
|
||||
describe("Memory Graph", () => {
|
||||
it("should initialize and compile the graph", () => {
|
||||
expect(graph).toBeDefined();
|
||||
expect(graph.name).toBe("MemoryAgent");
|
||||
});
|
||||
|
||||
// TODO: Add more test cases for individual nodes, routing logic, tool integration, and output validation
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
import { ensureConfiguration } from "./configuration.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
import { getStoreFromConfigOrThrow } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Initialize tools within a function so that they have access to the current
|
||||
* state and config at runtime.
|
||||
*/
|
||||
export function initializeTools(config?: LangGraphRunnableConfig) {
|
||||
/**
|
||||
* Upsert a memory in the database.
|
||||
* @param content The main content of the memory.
|
||||
* @param context Additional context for the memory.
|
||||
* @param memoryId Optional ID to overwrite an existing memory.
|
||||
* @returns A string confirming the memory storage.
|
||||
*/
|
||||
async function upsertMemory(opts: {
|
||||
content: string;
|
||||
context: string;
|
||||
memoryId?: string;
|
||||
}): Promise<string> {
|
||||
const { content, context, memoryId } = opts;
|
||||
if (!config || !config.store) {
|
||||
throw new Error("Config or store not provided");
|
||||
}
|
||||
|
||||
const configurable = ensureConfiguration(config);
|
||||
const memId = memoryId || uuidv4();
|
||||
const store = getStoreFromConfigOrThrow(config);
|
||||
|
||||
await store.put(["memories", configurable.userId], memId, {
|
||||
content,
|
||||
context,
|
||||
});
|
||||
|
||||
return `Stored memory ${memId}`;
|
||||
}
|
||||
|
||||
const upsertMemoryTool = tool(upsertMemory, {
|
||||
name: "upsertMemory",
|
||||
description:
|
||||
"Upsert a memory in the database. If a memory conflicts with an existing one, \
|
||||
update the existing one by passing in the memory_id instead of creating a duplicate. \
|
||||
If the user corrects a memory, update it. Can call multiple times in parallel \
|
||||
if you need to store or update multiple memories.",
|
||||
schema: z.object({
|
||||
content: z.string().describe(
|
||||
"The main content of the memory. For example: \
|
||||
'User expressed interest in learning about French.'",
|
||||
),
|
||||
context: z.string().describe(
|
||||
"Additional context for the memory. For example: \
|
||||
'This was mentioned while discussing career options in Europe.'",
|
||||
),
|
||||
memoryId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"The memory ID to overwrite. Only provide if updating an existing memory.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
return [upsertMemoryTool];
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BaseStore, LangGraphRunnableConfig } from "@langchain/langgraph";
|
||||
/**
|
||||
* Get the store from the configuration or throw an error.
|
||||
*/
|
||||
export function getStoreFromConfigOrThrow(
|
||||
config: LangGraphRunnableConfig,
|
||||
): BaseStore {
|
||||
if (!config.store) {
|
||||
throw new Error("Store not found in configuration");
|
||||
}
|
||||
|
||||
return config.store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the fully specified model name into model and provider.
|
||||
*/
|
||||
export function splitModelAndProvider(fullySpecifiedName: string): {
|
||||
model: string;
|
||||
provider?: string;
|
||||
} {
|
||||
let provider: string | undefined;
|
||||
let model: string;
|
||||
|
||||
if (fullySpecifiedName.includes("/")) {
|
||||
[provider, model] = fullySpecifiedName.split("/", 2);
|
||||
} else {
|
||||
model = fullySpecifiedName;
|
||||
}
|
||||
|
||||
return { model, provider };
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
# LangGraph.js ReAct Agent Template
|
||||
|
||||
This template showcases a [ReAct agent](https://arxiv.org/abs/2210.03629) implemented using [LangGraph.js](https://github.com/langchain-ai/langgraphjs), designed for [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio). ReAct agents are uncomplicated, prototypical agents that can be flexibly extended to many tools.
|
||||
|
||||

|
||||
|
||||
The core logic, defined in `src/react_agent/graph.ts`, demonstrates a flexible ReAct agent that iteratively reasons about user queries and executes actions, showcasing the power of this approach for complex problem-solving tasks.
|
||||
|
||||
## What it does
|
||||
|
||||
The ReAct agent:
|
||||
|
||||
1. Takes a user **query** as input
|
||||
2. Reasons about the query and decides on an action
|
||||
3. Executes the chosen action using available tools
|
||||
4. Observes the result of the action
|
||||
5. Repeats steps 2-4 until it can provide a final answer
|
||||
|
||||
By default, it's set up with a basic set of tools, but can be easily extended with custom tools to suit various use cases.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
|
||||
|
||||
1. Create a `.env` file.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Define required API keys in your `.env` file.
|
||||
|
||||
The primary [search tool](./tools.ts) [^1] used is [Tavily](https://tavily.com/). Create an API key [here](https://app.tavily.com/sign-in).
|
||||
|
||||
<
|
||||
|
||||
<!--
|
||||
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
-->
|
||||
|
||||
### Setup Model
|
||||
|
||||
The defaults values for `model` are shown below:
|
||||
|
||||
```yaml
|
||||
model: anthropic/claude-3-5-sonnet-20240620
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Anthropic
|
||||
|
||||
To use Anthropic's chat models:
|
||||
|
||||
1. Sign up for an [Anthropic API key](https://console.anthropic.com/) if you haven't already.
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's chat models:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
<!--
|
||||
End setup instructions
|
||||
-->
|
||||
|
||||
3. Customize whatever you'd like in the code.
|
||||
4. Open the folder in LangGraph Studio!
|
||||
|
||||
## How to customize
|
||||
|
||||
1. **Add new tools**: Extend the agent's capabilities by adding new tools in [`./tools.ts`](./tools.ts). These can be any TypeScript functions that perform specific tasks.
|
||||
2. **Select a different model**: We default to Anthropic's Claude 3.5 Sonnet. You can select a compatible chat model using `provider/model-name` via configuration, then installing the proper [chat model integration package](https://js.langchain.com/docs/integrations/chat/). Example: `openai/gpt-4-turbo-preview`, then run `npm i @langchain/openai`.
|
||||
3. **Customize the prompt**: We provide a default system prompt in [`./prompts.ts`](./prompts.ts). You can easily update this via configuration in the studio.
|
||||
|
||||
You can also quickly extend this template by:
|
||||
|
||||
- Modifying the agent's reasoning process in [`./graph.ts`](./graph.ts).
|
||||
- Adjusting the ReAct loop or adding additional steps to the agent's decision-making process.
|
||||
|
||||
## Development
|
||||
|
||||
While iterating on your graph, you can edit past state and rerun your app from past states to debug specific nodes. Local changes will be automatically applied via hot reload. Try adding an interrupt before the agent calls tools, updating the default system message in [`./configuration.ts`](./configuration.ts) to take on a persona, or adding additional nodes and edges!
|
||||
|
||||
Follow up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
|
||||
|
||||
You can find the latest (under construction) docs on [LangGraph](https://langchain-ai.github.io/langgraphjs/) here, including examples and other references. Using those guides can help you pick the right patterns to adapt here for your use case.
|
||||
|
||||
LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates.
|
||||
|
||||
[^1]: https://js.langchain.com/docs/concepts#tools
|
||||
|
||||
<!--
|
||||
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
{
|
||||
"config_schemas": {
|
||||
"agent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"description": "The name of the language model to use for the agent's main interactions. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"environment": [
|
||||
"TAVILY_API_KEY"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Define the configurable parameters for the agent.
|
||||
*/
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { SYSTEM_PROMPT_TEMPLATE } from "./prompts.js";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
|
||||
export const ConfigurationSchema = Annotation.Root({
|
||||
/**
|
||||
* The system prompt to be used by the agent.
|
||||
*/
|
||||
systemPromptTemplate: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The name of the language model to be used by the agent.
|
||||
*/
|
||||
model: Annotation<string>,
|
||||
});
|
||||
|
||||
export function ensureConfiguration(
|
||||
config: RunnableConfig,
|
||||
): typeof ConfigurationSchema.State {
|
||||
/**
|
||||
* Ensure the defaults are populated.
|
||||
*/
|
||||
const configurable = config.configurable ?? {};
|
||||
return {
|
||||
systemPromptTemplate:
|
||||
configurable.systemPromptTemplate ?? SYSTEM_PROMPT_TEMPLATE,
|
||||
model: configurable.model ?? "claude-3-5-sonnet-20240620",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { AIMessage } from "@langchain/core/messages";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
|
||||
import { ConfigurationSchema, ensureConfiguration } from "./configuration.js";
|
||||
import { TOOLS } from "./tools.js";
|
||||
import { loadChatModel } from "./utils.js";
|
||||
|
||||
// Define the function that calls the model
|
||||
async function callModel(
|
||||
state: typeof MessagesAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof MessagesAnnotation.Update> {
|
||||
/** Call the LLM powering our agent. **/
|
||||
const configuration = ensureConfiguration(config);
|
||||
|
||||
// Feel free to customize the prompt, model, and other logic!
|
||||
const model = (await loadChatModel(configuration.model)).bindTools(TOOLS);
|
||||
|
||||
const response = await model.invoke([
|
||||
{
|
||||
role: "system",
|
||||
content: configuration.systemPromptTemplate.replace(
|
||||
"{system_time}",
|
||||
new Date().toISOString(),
|
||||
),
|
||||
},
|
||||
...state.messages,
|
||||
]);
|
||||
|
||||
// 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: typeof MessagesAnnotation.State): 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. We use the prebuilt MessagesAnnotation to define state:
|
||||
// https://langchain-ai.github.io/langgraphjs/concepts/low_level/#messagesannotation
|
||||
const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema)
|
||||
// Define the two nodes we will cycle between
|
||||
.addNode("callModel", callModel)
|
||||
.addNode("tools", new ToolNode(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,7 @@
|
||||
/**
|
||||
* Default prompts used by the agent.
|
||||
*/
|
||||
|
||||
export const SYSTEM_PROMPT_TEMPLATE = `You are a helpful AI assistant.
|
||||
|
||||
System time: {system_time}`;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 568 KiB |
@@ -0,0 +1,18 @@
|
||||
import { it } from "@jest/globals";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
|
||||
import { graph } from "../../graph.js";
|
||||
|
||||
it("Simple runthrough", async () => {
|
||||
const res = await graph.invoke({
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "What is the current weather in SF?",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
res.messages.find((message: BaseMessage) => message._getType() === "tool"),
|
||||
).toBeDefined();
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { it } from "@jest/globals";
|
||||
|
||||
it("Test", async () => {});
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* This file defines the tools available to the ReAct agent.
|
||||
* Tools are functions that the agent can use to interact with external systems or perform specific tasks.
|
||||
*/
|
||||
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
|
||||
|
||||
/**
|
||||
* Tavily search tool configuration
|
||||
* This tool allows the agent to perform web searches using the Tavily API.
|
||||
*/
|
||||
const searchTavily = new TavilySearchResults({
|
||||
maxResults: 3,
|
||||
});
|
||||
|
||||
/**
|
||||
* Export an array of all available tools
|
||||
* Add new tools to this array to make them available to the agent
|
||||
*
|
||||
* Note: You can create custom tools by implementing the Tool interface from @langchain/core/tools
|
||||
* and add them to this array.
|
||||
* See https://js.langchain.com/docs/how_to/custom_tools/#tool-function for more information.
|
||||
*/
|
||||
export const TOOLS = [searchTavily];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
/**
|
||||
* Load a chat model from a fully specified name.
|
||||
* @param fullySpecifiedName - String in the format 'provider/model' or 'provider/account/provider/model'.
|
||||
* @returns A Promise that resolves to a BaseChatModel instance.
|
||||
*/
|
||||
export async function loadChatModel(fullySpecifiedName: string) {
|
||||
const index = fullySpecifiedName.indexOf("/");
|
||||
if (index === -1) {
|
||||
// If there's no "/", assume it's just the model
|
||||
return await initChatModel(fullySpecifiedName);
|
||||
} else {
|
||||
const provider = fullySpecifiedName.slice(0, index);
|
||||
const model = fullySpecifiedName.slice(index + 1);
|
||||
return await initChatModel(model, { modelProvider: provider });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
# LangGraph RAG Research Agent Template
|
||||
|
||||
This is a starter project to help you get started with developing a RAG research agent using [LangGraph.js](https://github.com/langchain-ai/langgraphjs) in [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio).
|
||||
|
||||

|
||||
|
||||
## What it does
|
||||
|
||||
This project has three graphs:
|
||||
|
||||
* an "index" graph (`src/index_graph/graph.ts`)
|
||||
* a "retrieval" graph (`src/retrieval_graph/graph.ts`)
|
||||
* a "researcher" subgraph (part of the retrieval graph) (`src/retrieval_graph/researcher_graph/graph.ts`)
|
||||
|
||||
The index graph takes in document objects indexes them.
|
||||
|
||||
```json
|
||||
[{ "page_content": "LangGraph is a library for building stateful, multi-actor applications with LLMs, used to create agent and multi-agent workflows." }]
|
||||
```
|
||||
|
||||
If an empty list is provided (default), a list of sample documents from `src/sample_docs.json` is indexed instead. Those sample documents are based on the conceptual guides for LangChain and LangGraph.
|
||||
|
||||
The retrieval graph manages a chat history and responds based on the fetched documents. Specifically, it:
|
||||
|
||||
1. Takes a user **query** as input
|
||||
2. Analyzes the query and determines how to route it:
|
||||
- if the query is about "LangChain", it creates a research plan based on the user's query and passes the plan to the researcher subgraph
|
||||
- if the query is ambiguous, it asks for more information
|
||||
- if the query is general (unrelated to LangChain), it lets the user know
|
||||
3. If the query is about "LangChain", the researcher subgraph runs for each step in the research plan, until no more steps are left:
|
||||
- it first generates a list of queries based on the step
|
||||
- it then retrieves the relevant documents in parallel for all queries and return the documents to the retrieval graph
|
||||
4. Finally, the retrieval graph generates a response based on the retrieved documents and the conversation context
|
||||
|
||||
## Getting Started
|
||||
|
||||
Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
|
||||
|
||||
1. Create a `.env` file.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Select your retriever & index, and save the access instructions to your `.env` file.
|
||||
|
||||
<!--
|
||||
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
-->
|
||||
|
||||
### Setup Retriever
|
||||
|
||||
The defaults values for `retrieverProvider` are shown below:
|
||||
|
||||
```yaml
|
||||
retrieverProvider: elastic
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Setup Elasticsearch
|
||||
|
||||
**Elastic Cloud**
|
||||
|
||||
1. Signup for a free trial with [Elastic Cloud](https://cloud.elastic.co/registration?onboarding_token=search&cta=cloud-registration&tech=trial&plcmt=article%20content&pg=langchain).
|
||||
2. Get the Elasticsearch URL, found under Applications of your deployment.
|
||||
3. Create an API key. See the [official elastic documentation](https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#creating-an-api-key) for more information.
|
||||
4. Copy the URL and API key to your `.env` file created above:
|
||||
|
||||
```
|
||||
ELASTICSEARCH_URL=<ES_URL>
|
||||
ELASTICSEARCH_API_KEY=<API_KEY>
|
||||
```
|
||||
|
||||
**Local Elasticsearch (Docker)**
|
||||
|
||||
```
|
||||
docker run \
|
||||
-p 127.0.0.1:9200:9200 \
|
||||
-d \
|
||||
--name elasticsearch \
|
||||
-e ELASTIC_PASSWORD=changeme \
|
||||
-e "discovery.type=single-node" \
|
||||
-e "xpack.security.http.ssl.enabled=false" \
|
||||
-e "xpack.license.self_generated.type=trial" \
|
||||
docker.elastic.co/elasticsearch/elasticsearch:8.15.1
|
||||
```
|
||||
|
||||
See the [official Elastic documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/run-elasticsearch-locally.html) for more information on running it locally.
|
||||
|
||||
Then populate the following in your `.env` file:
|
||||
|
||||
```
|
||||
# As both Elasticsearch and LangGraph Studio runs in Docker, we need to use host.docker.internal to access.
|
||||
|
||||
ELASTICSEARCH_URL=http://host.docker.internal:9200
|
||||
ELASTICSEARCH_USER=elastic
|
||||
ELASTICSEARCH_PASSWORD=changeme
|
||||
```
|
||||
|
||||
#### MongoDB Atlas
|
||||
|
||||
MongoDB Atlas is a fully-managed cloud database that includes vector search capabilities for AI-powered applications.
|
||||
|
||||
1. Create a free Atlas cluster:
|
||||
|
||||
- Go to the [MongoDB Atlas website](https://www.mongodb.com/cloud/atlas/register) and sign up for a free account.
|
||||
- After logging in, create a free cluster by following the on-screen instructions.
|
||||
|
||||
2. Create a vector search index
|
||||
|
||||
- Follow the instructions at [the Mongo docs](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/)
|
||||
- By default, we use the collection `langgraph_retrieval_agent.default` - create the index there
|
||||
- Add an indexed filter for path `user_id`
|
||||
- **IMPORTANT**: select Atlas Vector Search NOT Atlas Search when creating the index
|
||||
Your final JSON editor configuration should look something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"numDimensions": 1536,
|
||||
"path": "embedding",
|
||||
"similarity": "cosine",
|
||||
"type": "vector"
|
||||
},
|
||||
{
|
||||
"path": "user_id",
|
||||
"type": "filter"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The exact numDimensions may differ if you select a different embedding model.
|
||||
|
||||
2. Set up your environment:
|
||||
|
||||
- In the Atlas dashboard, click on "Connect" for your cluster.
|
||||
- Choose "Connect your application" and copy the provided connection string.
|
||||
- Create a `.env` file in your project root if you haven't already.
|
||||
- Add your MongoDB Atlas connection string to the `.env` file:
|
||||
|
||||
```
|
||||
MONGODB_URI="mongodb+srv://username:password@your-cluster-url.mongodb.net/?retryWrites=true&w=majority&appName=your-cluster-name"
|
||||
```
|
||||
|
||||
Replace `username`, `password`, `your-cluster-url`, and `your-cluster-name` with your actual credentials and cluster information.
|
||||
|
||||
#### Pinecone Serverless
|
||||
|
||||
Pinecone is a managed, cloud-native vector database that provides long-term memory for high-performance AI applications.
|
||||
|
||||
1. Sign up for a Pinecone account at [https://login.pinecone.io/login](https://login.pinecone.io/login) if you haven't already.
|
||||
|
||||
2. After logging in, generate an API key from the Pinecone console.
|
||||
|
||||
3. Create a serverless index:
|
||||
|
||||
- Choose a name for your index (e.g., "example-index")
|
||||
- Set the dimension based on your embedding model (e.g., 1536 for OpenAI embeddings)
|
||||
- Select "cosine" as the metric
|
||||
- Choose "Serverless" as the index type
|
||||
- Select your preferred cloud provider and region (e.g., AWS us-east-1)
|
||||
|
||||
4. Once you have created your index and obtained your API key, add them to your `.env` file:
|
||||
|
||||
```
|
||||
PINECONE_API_KEY=your-api-key
|
||||
PINECONE_INDEX_NAME=your-index-name
|
||||
```
|
||||
|
||||
### Setup Model
|
||||
|
||||
The defaults values for `responseModel`, `queryModel` are shown below:
|
||||
|
||||
```yaml
|
||||
responseModel: anthropic/claude-3-5-sonnet-20240620
|
||||
queryModel: anthropic/claude-3-haiku-20240307
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Anthropic
|
||||
|
||||
To use Anthropic's chat models:
|
||||
|
||||
1. Sign up for an [Anthropic API key](https://console.anthropic.com/) if you haven't already.
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's chat models:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### Setup Embedding Model
|
||||
|
||||
The defaults values for `embeddingModel` are shown below:
|
||||
|
||||
```yaml
|
||||
embeddingModel: openai/text-embedding-3-small
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's embeddings:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### Cohere
|
||||
|
||||
To use Cohere's embeddings:
|
||||
|
||||
1. Sign up for a [Cohere API key](https://dashboard.cohere.com/welcome/register).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```bash
|
||||
COHERE_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
<!--
|
||||
End setup instructions
|
||||
-->
|
||||
|
||||
## Using
|
||||
|
||||
Once you've set up your retriever and saved your model secrets, it's time to try it out! First, let's add some information to the index. Open studio, select the "indexer" graph from the dropdown in the top-left, and then add some content to chat over. You can just invoke it with an empty list (default) to index sample documents from LangChain and LangGraph documentation.
|
||||
|
||||
You'll know that the indexing is complete when the indexer "delete"'s the content from its graph memory (since it's been persisted in your configured storage provider).
|
||||
|
||||
Next, open the "retrieval_graph" using the dropdown in the top-left. Ask it questions about LangChain to confirm it can fetch the required information!
|
||||
|
||||
## How to customize
|
||||
|
||||
You can customize this retrieval agent template in several ways:
|
||||
|
||||
1. **Change the retriever**: You can switch between different vector stores (Elasticsearch, MongoDB, Pinecone) by modifying the `retriever_provider` in the configuration. Each provider has its own setup instructions in the "Getting Started" section above.
|
||||
|
||||
2. **Modify the embedding model**: You can change the embedding model used for document indexing and query embedding by updating the `embedding_model` in the configuration. Options include various OpenAI and Cohere models.
|
||||
|
||||
3. **Adjust search parameters**: Fine-tune the retrieval process by modifying the `search_kwargs` in the configuration. This allows you to control aspects like the number of documents retrieved or similarity thresholds.
|
||||
|
||||
4. **Customize the response generation**: You can modify the `response_system_prompt` to change how the agent formulates its responses. This allows you to adjust the agent's personality or add specific instructions for answer generation.
|
||||
|
||||
5. **Modify prompts**: Update the prompts used for user query routing, research planning, query generation and more in `src/retrieval_graph/prompts.ts` to better suit your specific use case or to improve the agent's performance. You can also modify these directly in LangGraph Studio. For example, you can:
|
||||
|
||||
* Modify system prompt for creating research plan (`research_plan_system_prompt`)
|
||||
* Modify system prompt for generating search queries based on the research plan (`generate_queries_system_prompt`)
|
||||
|
||||
6. **Change the language model**: Update the `response_model` in the configuration to use different language models for response generation. Options include various Claude models from Anthropic, as well as models from other providers like Fireworks AI.
|
||||
|
||||
7. **Extend the graph**: You can add new nodes or modify existing ones in the `src/retrieval_graph/graph.ts` file to introduce additional processing steps or decision points in the agent's workflow.
|
||||
|
||||
8. **Add tools**: Implement tools to expand the researcher agent's capabilities beyond simple retrieval generation.
|
||||
|
||||
Remember to test your changes thoroughly to ensure they improve the agent's performance for your specific use case.
|
||||
|
||||
## Development
|
||||
|
||||
While iterating on your graph, you can edit past state and rerun your app from past states to debug specific nodes. Local changes will be automatically applied via hot reload. Try adding an interrupt before the agent calls the researcher subgraph, updating the default system message in `src/retrieval_graph/prompts.ts` to take on a persona, or adding additional nodes and edges!
|
||||
|
||||
Follow up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
|
||||
|
||||
You can find the latest (under construction) docs on [LangGraph](https://github.com/langchain-ai/langgraphjs) here, including examples and other references. Using those guides can help you pick the right patterns to adapt here for your use case.
|
||||
|
||||
LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates.
|
||||
|
||||
<!--
|
||||
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
{
|
||||
"config_schemas": {
|
||||
"indexer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"embeddingModel": {
|
||||
"type": "string",
|
||||
"default": "openai/text-embedding-3-small",
|
||||
"description": "Name of the embedding model to use. Must be a valid embedding model name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "cohere/embed-english-light-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-large",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-small",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-ada-002",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retrieverProvider": {
|
||||
"enum": [
|
||||
"elastic-local",
|
||||
"elastic",
|
||||
"mongodb",
|
||||
"pinecone"
|
||||
],
|
||||
"default": "elastic-local",
|
||||
"description": "The vector store provider to use for retrieval. Options are 'elastic', 'pinecone', or 'mongodb'.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "elastic",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_API_KEY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "elastic-local",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_USER",
|
||||
"ELASTICSEARCH_PASSWORD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "mongodb",
|
||||
"variables": [
|
||||
"MONGODB_URI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "pinecone",
|
||||
"variables": [
|
||||
"PINECONE_API_KEY",
|
||||
"PINECONE_INDEX_NAME"
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"retrieval_graph": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"embeddingModel": {
|
||||
"type": "string",
|
||||
"default": "openai/text-embedding-3-small",
|
||||
"description": "Name of the embedding model to use. Must be a valid embedding model name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "cohere/embed-english-light-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-large",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-small",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-ada-002",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retrieverProvider": {
|
||||
"enum": [
|
||||
"elastic-local",
|
||||
"elastic",
|
||||
"mongodb",
|
||||
"pinecone"
|
||||
],
|
||||
"default": "elastic-local",
|
||||
"description": "The vector store provider to use for retrieval. Options are 'elastic', 'pinecone', or 'mongodb'.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "elastic",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_API_KEY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "elastic-local",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_USER",
|
||||
"ELASTICSEARCH_PASSWORD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "mongodb",
|
||||
"variables": [
|
||||
"MONGODB_URI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "pinecone",
|
||||
"variables": [
|
||||
"PINECONE_API_KEY",
|
||||
"PINECONE_INDEX_NAME"
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"responseModel": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"description": "The language model used for generating responses. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"queryModel": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-haiku-20240307",
|
||||
"description": "The language model used for processing and refining queries. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { BaseConfigurationAnnotation } from "../shared/configuration.js";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { ensureBaseConfiguration } from "../shared/configuration.js";
|
||||
|
||||
// This file contains sample documents to index, based on the following LangChain and LangGraph documentation pages:
|
||||
// - https://python.langchain.com/docs/concepts/
|
||||
// - https://langchain-ai.github.io/langgraph/concepts/low_level/
|
||||
const DEFAULT_DOCS_FILE = "src/research-agent/sample_docs.json";
|
||||
|
||||
/**
|
||||
* The configuration for the indexing process.
|
||||
*/
|
||||
export const IndexConfigurationAnnotation = Annotation.Root({
|
||||
...BaseConfigurationAnnotation.spec,
|
||||
|
||||
/**
|
||||
* Path to a JSON file containing default documents to index.
|
||||
*/
|
||||
docsFile: Annotation<string>,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an typeof IndexConfigurationAnnotation.State instance from a RunnableConfig object.
|
||||
*
|
||||
* @param config - The configuration object to use.
|
||||
* @returns An instance of typeof IndexConfigurationAnnotation.State with the specified configuration.
|
||||
*/
|
||||
export function ensureIndexConfiguration(
|
||||
config: RunnableConfig,
|
||||
): typeof IndexConfigurationAnnotation.State {
|
||||
const configurable = (config?.configurable || {}) as Partial<
|
||||
typeof IndexConfigurationAnnotation.State
|
||||
>;
|
||||
const baseConfig = ensureBaseConfiguration(config);
|
||||
return {
|
||||
...baseConfig,
|
||||
docsFile: configurable.docsFile || DEFAULT_DOCS_FILE,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* This "graph" simply exposes an endpoint for a user to upload docs to be indexed.
|
||||
*/
|
||||
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { StateGraph, END, START } from "@langchain/langgraph";
|
||||
import fs from "fs/promises";
|
||||
|
||||
import { IndexStateAnnotation } from "./state.js";
|
||||
import { makeRetriever } from "../shared/retrieval.js";
|
||||
import {
|
||||
ensureIndexConfiguration,
|
||||
IndexConfigurationAnnotation,
|
||||
} from "./configuration.js";
|
||||
import { reduceDocs } from "../shared/state.js";
|
||||
|
||||
async function indexDocs(
|
||||
state: typeof IndexStateAnnotation.State,
|
||||
config?: RunnableConfig,
|
||||
): Promise<typeof IndexStateAnnotation.Update> {
|
||||
if (!config) {
|
||||
throw new Error("Configuration required to run index_docs.");
|
||||
}
|
||||
|
||||
const configuration = ensureIndexConfiguration(config);
|
||||
let docs = state.docs;
|
||||
|
||||
if (!docs.length) {
|
||||
const fileContent = await fs.readFile(configuration.docsFile, "utf-8");
|
||||
const serializedDocs = JSON.parse(fileContent);
|
||||
docs = reduceDocs([], serializedDocs);
|
||||
}
|
||||
|
||||
const retriever = await makeRetriever(config);
|
||||
await retriever.addDocuments(docs);
|
||||
|
||||
return { docs: "delete" };
|
||||
}
|
||||
|
||||
// Define the graph
|
||||
const builder = new StateGraph(
|
||||
IndexStateAnnotation,
|
||||
IndexConfigurationAnnotation,
|
||||
)
|
||||
.addNode("indexDocs", indexDocs)
|
||||
.addEdge(START, "indexDocs")
|
||||
.addEdge("indexDocs", END);
|
||||
|
||||
// Compile into a graph object that you can invoke and deploy.
|
||||
export const graph = builder.compile().withConfig({ runName: "IndexGraph" });
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { reduceDocs } from "../shared/state.js";
|
||||
|
||||
/**
|
||||
* Represents the state for document indexing and retrieval.
|
||||
*
|
||||
* This interface defines the structure of the index state, which includes
|
||||
* the documents to be indexed and the retriever used for searching
|
||||
* these documents.
|
||||
*/
|
||||
export const IndexStateAnnotation = Annotation.Root({
|
||||
/**
|
||||
* A list of documents that the agent can index.
|
||||
*/
|
||||
docs: Annotation<
|
||||
Document[],
|
||||
Document[] | { [key: string]: any }[] | string[] | string | "delete"
|
||||
>({
|
||||
default: () => [],
|
||||
reducer: reduceDocs,
|
||||
}),
|
||||
});
|
||||
|
||||
export type IndexStateType = typeof IndexStateAnnotation.State;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
|
||||
import {
|
||||
ROUTER_SYSTEM_PROMPT,
|
||||
MORE_INFO_SYSTEM_PROMPT,
|
||||
GENERAL_SYSTEM_PROMPT,
|
||||
RESEARCH_PLAN_SYSTEM_PROMPT,
|
||||
GENERATE_QUERIES_SYSTEM_PROMPT,
|
||||
RESPONSE_SYSTEM_PROMPT,
|
||||
} from "./prompts.js";
|
||||
import {
|
||||
BaseConfigurationAnnotation,
|
||||
ensureBaseConfiguration,
|
||||
} from "../shared/configuration.js";
|
||||
|
||||
/**
|
||||
* The configuration for the agent.
|
||||
*/
|
||||
export const AgentConfigurationAnnotation = Annotation.Root({
|
||||
...BaseConfigurationAnnotation.spec,
|
||||
|
||||
// models
|
||||
/**
|
||||
* The language model used for processing and refining queries.
|
||||
* Should be in the form: provider/model-name.
|
||||
*/
|
||||
queryModel: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The language model used for generating responses.
|
||||
* Should be in the form: provider/model-name.
|
||||
*/
|
||||
responseModel: Annotation<string>,
|
||||
|
||||
// prompts
|
||||
/**
|
||||
* The system prompt used for classifying user questions to route them to the correct node.
|
||||
*/
|
||||
routerSystemPrompt: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The system prompt used for asking for more information from the user.
|
||||
*/
|
||||
moreInfoSystemPrompt: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The system prompt used for responding to general questions.
|
||||
*/
|
||||
generalSystemPrompt: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The system prompt used for generating a research plan based on the user's question.
|
||||
*/
|
||||
researchPlanSystemPrompt: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The system prompt used by the researcher to generate queries based on a step in the research plan.
|
||||
*/
|
||||
generateQueriesSystemPrompt: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The system prompt used for generating responses.
|
||||
*/
|
||||
responseSystemPrompt: Annotation<string>,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a typeof ConfigurationAnnotation.State instance from a RunnableConfig object.
|
||||
*
|
||||
* @param config - The configuration object to use.
|
||||
* @returns An instance of typeof ConfigurationAnnotation.State with the specified configuration.
|
||||
*/
|
||||
export function ensureAgentConfiguration(
|
||||
config: RunnableConfig,
|
||||
): typeof AgentConfigurationAnnotation.State {
|
||||
const configurable = (config?.configurable || {}) as Partial<
|
||||
typeof AgentConfigurationAnnotation.State
|
||||
>;
|
||||
const baseConfig = ensureBaseConfiguration(config);
|
||||
return {
|
||||
...baseConfig,
|
||||
queryModel: configurable.queryModel || "anthropic/claude-3-haiku-20240307",
|
||||
responseModel:
|
||||
configurable.responseModel || "anthropic/claude-3-5-sonnet-20240620",
|
||||
routerSystemPrompt: configurable.routerSystemPrompt || ROUTER_SYSTEM_PROMPT,
|
||||
moreInfoSystemPrompt:
|
||||
configurable.moreInfoSystemPrompt || MORE_INFO_SYSTEM_PROMPT,
|
||||
generalSystemPrompt:
|
||||
configurable.generalSystemPrompt || GENERAL_SYSTEM_PROMPT,
|
||||
researchPlanSystemPrompt:
|
||||
configurable.researchPlanSystemPrompt || RESEARCH_PLAN_SYSTEM_PROMPT,
|
||||
generateQueriesSystemPrompt:
|
||||
configurable.generateQueriesSystemPrompt ||
|
||||
GENERATE_QUERIES_SYSTEM_PROMPT,
|
||||
responseSystemPrompt:
|
||||
configurable.responseSystemPrompt || RESPONSE_SYSTEM_PROMPT,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
StateGraph,
|
||||
END,
|
||||
START,
|
||||
LangGraphRunnableConfig,
|
||||
} from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
|
||||
import {
|
||||
AgentConfigurationAnnotation,
|
||||
ensureAgentConfiguration,
|
||||
} from "./configuration.js";
|
||||
import { graph as researcherGraph } from "./researcher_graph/graph.js";
|
||||
import { AgentStateAnnotation, InputStateAnnotation } from "./state.js";
|
||||
import { formatDocs, loadChatModel } from "../shared/utils.js";
|
||||
|
||||
async function analyzeAndRouteQuery(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof AgentStateAnnotation.Update> {
|
||||
const configuration = ensureAgentConfiguration(config);
|
||||
const model = await loadChatModel(configuration.queryModel);
|
||||
const messages = [
|
||||
{ role: "system", content: configuration.routerSystemPrompt },
|
||||
...state.messages,
|
||||
];
|
||||
const Router = z
|
||||
.object({
|
||||
logic: z.string(),
|
||||
type: z.enum(["more-info", "langchain", "general"]),
|
||||
})
|
||||
.describe("Classify user query.");
|
||||
const response = await model.withStructuredOutput(Router).invoke(messages);
|
||||
return { router: response };
|
||||
}
|
||||
|
||||
function routeQuery(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
): "createResearchPlan" | "askForMoreInfo" | "respondToGeneralQuery" {
|
||||
const type = state.router.type;
|
||||
if (type === "langchain") {
|
||||
return "createResearchPlan";
|
||||
} else if (type === "more-info") {
|
||||
return "askForMoreInfo";
|
||||
} else if (type === "general") {
|
||||
return "respondToGeneralQuery";
|
||||
} else {
|
||||
throw new Error(`Unknown router type ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function askForMoreInfo(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof AgentStateAnnotation.Update> {
|
||||
const configuration = ensureAgentConfiguration(config);
|
||||
const model = await loadChatModel(configuration.queryModel);
|
||||
const systemPrompt = configuration.moreInfoSystemPrompt.replace(
|
||||
"{logic}",
|
||||
state.router.logic,
|
||||
);
|
||||
const messages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...state.messages,
|
||||
];
|
||||
const response = await model.invoke(messages);
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
async function respondToGeneralQuery(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof AgentStateAnnotation.Update> {
|
||||
const configuration = ensureAgentConfiguration(config);
|
||||
const model = await loadChatModel(configuration.queryModel);
|
||||
const systemPrompt = configuration.generalSystemPrompt.replace(
|
||||
"{logic}",
|
||||
state.router.logic,
|
||||
);
|
||||
const messages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...state.messages,
|
||||
];
|
||||
const response = await model.invoke(messages);
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
async function createResearchPlan(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof AgentStateAnnotation.Update> {
|
||||
const Plan = z
|
||||
.object({
|
||||
steps: z.array(z.string()),
|
||||
})
|
||||
.describe("Generate research plan.");
|
||||
|
||||
const configuration = ensureAgentConfiguration(config);
|
||||
const model = (
|
||||
await loadChatModel(configuration.queryModel)
|
||||
).withStructuredOutput(Plan);
|
||||
const messages = [
|
||||
{ role: "system", content: configuration.researchPlanSystemPrompt },
|
||||
...state.messages,
|
||||
];
|
||||
const response = await model.invoke(messages);
|
||||
return { steps: response.steps, documents: "delete" };
|
||||
}
|
||||
|
||||
async function conductResearch(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
config: LangGraphRunnableConfig,
|
||||
): Promise<typeof AgentStateAnnotation.Update> {
|
||||
const result = await researcherGraph.invoke(
|
||||
{ question: state.steps[0] },
|
||||
{ ...config },
|
||||
);
|
||||
return { documents: result.documents, steps: state.steps.slice(1) };
|
||||
}
|
||||
|
||||
function checkFinished(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
): "conductResearch" | "respond" {
|
||||
return state.steps && state.steps.length > 0 ? "conductResearch" : "respond";
|
||||
}
|
||||
|
||||
async function respond(
|
||||
state: typeof AgentStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof AgentStateAnnotation.Update> {
|
||||
const configuration = ensureAgentConfiguration(config);
|
||||
const model = await loadChatModel(configuration.responseModel);
|
||||
// @ts-ignore
|
||||
const context = formatDocs(state.documents);
|
||||
const prompt = configuration.responseSystemPrompt.replace(
|
||||
"{context}",
|
||||
context,
|
||||
);
|
||||
const messages = [{ role: "system", content: prompt }, ...state.messages];
|
||||
const response = await model.invoke(messages);
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
// Define the graph
|
||||
const builder = new StateGraph(
|
||||
{
|
||||
stateSchema: AgentStateAnnotation,
|
||||
input: InputStateAnnotation,
|
||||
},
|
||||
AgentConfigurationAnnotation,
|
||||
)
|
||||
.addNode("analyzeAndRouteQuery", analyzeAndRouteQuery)
|
||||
.addNode("askForMoreInfo", askForMoreInfo)
|
||||
.addNode("respondToGeneralQuery", respondToGeneralQuery)
|
||||
.addNode("createResearchPlan", createResearchPlan)
|
||||
.addNode("conductResearch", conductResearch, { subgraphs: [researcherGraph] })
|
||||
.addNode("respond", respond)
|
||||
.addEdge(START, "analyzeAndRouteQuery")
|
||||
.addConditionalEdges("analyzeAndRouteQuery", routeQuery, [
|
||||
"askForMoreInfo",
|
||||
"respondToGeneralQuery",
|
||||
"createResearchPlan",
|
||||
])
|
||||
.addEdge("createResearchPlan", "conductResearch")
|
||||
.addConditionalEdges("conductResearch", checkFinished, [
|
||||
"conductResearch",
|
||||
"respond",
|
||||
])
|
||||
.addEdge("askForMoreInfo", END)
|
||||
.addEdge("respondToGeneralQuery", END)
|
||||
.addEdge("respond", END);
|
||||
|
||||
// Compile into a graph object that you can invoke and deploy.
|
||||
export const graph = builder
|
||||
.compile()
|
||||
.withConfig({ runName: "RetrievalGraph" });
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Default prompts.
|
||||
*/
|
||||
|
||||
// Retrieval graph
|
||||
|
||||
export const ROUTER_SYSTEM_PROMPT = `You are a LangChain Developer advocate. Your job is help people using LangChain answer any issues they are running into.
|
||||
|
||||
A user will come to you with an inquiry. Your first job is to classify what type of inquiry it is. The types of inquiries you should classify it as are:
|
||||
|
||||
## \`more-info\`
|
||||
Classify a user inquiry as this if you need more information before you will be able to help them. Examples include:
|
||||
- The user complains about an error but doesn't provide the error
|
||||
- The user says something isn't working but doesn't explain why/how it's not working
|
||||
|
||||
## \`langchain\`
|
||||
Classify a user inquiry as this if it can be answered by looking up information related to LangChain open source package. The LangChain open source package \
|
||||
is a python library for working with LLMs. It integrates with various LLMs, databases and APIs.
|
||||
|
||||
## \`general\`
|
||||
Classify a user inquiry as this if it is just a general question`;
|
||||
|
||||
export const GENERAL_SYSTEM_PROMPT = `You are a LangChain Developer advocate. Your job is help people using LangChain answer any issues they are running into.
|
||||
|
||||
Your boss has determined that the user is asking a general question, not one related to LangChain. This was their logic:
|
||||
|
||||
<logic>
|
||||
{logic}
|
||||
</logic>
|
||||
|
||||
Respond to the user. Politely decline to answer and tell them you can only answer questions about LangChain-related topics, and that if their question is about LangChain they should clarify how it is.\
|
||||
Be nice to them though - they are still a user!`;
|
||||
|
||||
export const MORE_INFO_SYSTEM_PROMPT = `You are a LangChain Developer advocate. Your job is help people using LangChain answer any issues they are running into.
|
||||
|
||||
Your boss has determined that more information is needed before doing any research on behalf of the user. This was their logic:
|
||||
|
||||
<logic>
|
||||
{logic}
|
||||
</logic>
|
||||
|
||||
Respond to the user and try to get any more relevant information. Do not overwhelm them! Be nice, and only ask them a single follow up question.`;
|
||||
|
||||
export const RESEARCH_PLAN_SYSTEM_PROMPT = `You are a LangChain expert and a world-class researcher, here to assist with any and all questions or issues with LangChain, LangGraph, LangSmith, or any related functionality. Users may come to you with questions or issues.
|
||||
|
||||
Based on the conversation below, generate a plan for how you will research the answer to their question. \
|
||||
The plan should generally not be more than 3 steps long, it can be as short as one. The length of the plan depends on the question.
|
||||
|
||||
You have access to the following documentation sources:
|
||||
- Conceptual docs
|
||||
- Integration docs
|
||||
- How-to guides
|
||||
|
||||
You do not need to specify where you want to research for all steps of the plan, but it's sometimes helpful.`;
|
||||
|
||||
export const RESPONSE_SYSTEM_PROMPT = `\
|
||||
You are an expert programmer and problem-solver, tasked with answering any question \
|
||||
about LangChain.
|
||||
|
||||
Generate a comprehensive and informative answer for the \
|
||||
given question based solely on the provided search results (URL and content). \
|
||||
Do NOT ramble, and adjust your response length based on the question. If they ask \
|
||||
a question that can be answered in one sentence, do that. If 5 paragraphs of detail is needed, \
|
||||
do that. You must \
|
||||
only use information from the provided search results. Use an unbiased and \
|
||||
journalistic tone. Combine search results together into a coherent answer. Do not \
|
||||
repeat text. Cite search results using [{{number}}] notation. Only cite the most \
|
||||
relevant results that answer the question accurately. Place these citations at the end \
|
||||
of the individual sentence or paragraph that reference them. \
|
||||
Do not put them all at the end, but rather sprinkle them throughout. If \
|
||||
different results refer to different entities within the same name, write separate \
|
||||
answers for each entity.
|
||||
|
||||
You should use bullet points in your answer for readability. Put citations where they apply
|
||||
rather than putting them all at the end. DO NOT PUT THEM ALL THAT END, PUT THEM IN THE BULLET POINTS.
|
||||
|
||||
If there is nothing in the context relevant to the question at hand, do NOT make up an answer. \
|
||||
Rather, tell them why you're unsure and ask for any additional information that may help you answer better.
|
||||
|
||||
Sometimes, what a user is asking may NOT be possible. Do NOT tell them that things are possible if you don't \
|
||||
see evidence for it in the context below. If you don't see based in the information below that something is possible, \
|
||||
do NOT say that it is - instead say that you're not sure.
|
||||
|
||||
Anything between the following \`context\` html blocks is retrieved from a knowledge \
|
||||
bank, not part of the conversation with the user.
|
||||
|
||||
<context>
|
||||
{context}
|
||||
<context/>`;
|
||||
|
||||
// Researcher graph
|
||||
|
||||
export const GENERATE_QUERIES_SYSTEM_PROMPT = `\
|
||||
Generate 3 search queries to search for to answer the user's question. \
|
||||
These search queries should be diverse in nature - do not generate \
|
||||
repetitive ones.`;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Researcher graph used in the conversational retrieval system as a subgraph.
|
||||
* This module defines the core structure and functionality of the researcher graph,
|
||||
* which is responsible for generating search queries and retrieving relevant documents.
|
||||
*/
|
||||
|
||||
import { StateGraph, END, START, Send } from "@langchain/langgraph";
|
||||
import { z } from "zod";
|
||||
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { ensureAgentConfiguration } from "../configuration.js";
|
||||
import { QueryStateAnnotation, ResearcherStateAnnotation } from "./state.js";
|
||||
import { makeRetriever } from "../../shared/retrieval.js";
|
||||
import { loadChatModel } from "../../shared/utils.js";
|
||||
|
||||
async function generateQueries(
|
||||
state: typeof ResearcherStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof ResearcherStateAnnotation.Update> {
|
||||
const Response = z.object({
|
||||
queries: z.array(z.string()),
|
||||
});
|
||||
|
||||
const configuration = ensureAgentConfiguration(config);
|
||||
const model = (
|
||||
await loadChatModel(configuration.queryModel)
|
||||
).withStructuredOutput(Response);
|
||||
const messages: { role: string; content: string }[] = [
|
||||
{ role: "system", content: configuration.generateQueriesSystemPrompt },
|
||||
{ role: "human", content: state.question },
|
||||
];
|
||||
const response = await model.invoke(messages);
|
||||
return { queries: response.queries };
|
||||
}
|
||||
|
||||
async function retrieveDocuments(
|
||||
state: typeof QueryStateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof ResearcherStateAnnotation.Update> {
|
||||
const retriever = await makeRetriever(config);
|
||||
const response = await retriever.invoke(state.query, config);
|
||||
return { documents: response };
|
||||
}
|
||||
|
||||
function retrieveInParallel(
|
||||
state: typeof ResearcherStateAnnotation.State,
|
||||
): Send[] {
|
||||
return state.queries.map(
|
||||
(query: string) => new Send("retrieveDocuments", { query }),
|
||||
);
|
||||
}
|
||||
|
||||
// Define the graph
|
||||
const builder = new StateGraph({
|
||||
stateSchema: ResearcherStateAnnotation,
|
||||
})
|
||||
.addNode("generateQueries", generateQueries)
|
||||
.addNode("retrieveDocuments", retrieveDocuments)
|
||||
.addEdge(START, "generateQueries")
|
||||
.addConditionalEdges("generateQueries", retrieveInParallel, [
|
||||
"retrieveDocuments",
|
||||
])
|
||||
.addEdge("retrieveDocuments", END);
|
||||
|
||||
// Compile into a graph object that you can invoke and deploy.
|
||||
export const graph = builder
|
||||
.compile()
|
||||
.withConfig({ runName: "ResearcherGraph" });
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { reduceDocs } from "../../shared/state.js";
|
||||
|
||||
/**
|
||||
* Private state for the retrieve_documents node in the researcher graph.
|
||||
*/
|
||||
export const QueryStateAnnotation = Annotation.Root({
|
||||
query: Annotation<string>(),
|
||||
});
|
||||
|
||||
/**
|
||||
* State of the researcher graph / agent.
|
||||
*/
|
||||
export const ResearcherStateAnnotation = Annotation.Root({
|
||||
/**
|
||||
* A step in the research plan generated by the retriever agent.
|
||||
*/
|
||||
question: Annotation<string>(),
|
||||
|
||||
/**
|
||||
* A list of search queries based on the question that the researcher generates.
|
||||
*/
|
||||
queries: Annotation<string[]>({
|
||||
default: () => [],
|
||||
reducer: (existing: string[], newQueries: string[]) => [
|
||||
...existing,
|
||||
...newQueries,
|
||||
],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Populated by the retriever. This is a list of documents that the agent can reference.
|
||||
*/
|
||||
documents: Annotation<
|
||||
Document[],
|
||||
Document[] | { [key: string]: any }[] | string[] | string | "delete"
|
||||
>({
|
||||
default: () => [],
|
||||
reducer: reduceDocs,
|
||||
}),
|
||||
|
||||
// Feel free to add additional attributes to your state as needed.
|
||||
// Common examples include retrieved documents, extracted entities, API connections, etc.
|
||||
});
|
||||
|
||||
export type ResearcherStateType = typeof ResearcherStateAnnotation.State;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { reduceDocs } from "../shared/state.js";
|
||||
|
||||
/**
|
||||
* Represents the input state for the agent.
|
||||
* This is a restricted version of the State that defines a narrower interface
|
||||
* to the outside world compared to what is maintained internally.
|
||||
*/
|
||||
export const InputStateAnnotation = Annotation.Root({
|
||||
/**
|
||||
* Messages track the primary execution state of the agent.
|
||||
* @type {BaseMessage[]}
|
||||
* @description
|
||||
* Typically accumulates a pattern of Human/AI/Human/AI messages. If combined with a
|
||||
* tool-calling ReAct agent pattern, it may follow this sequence:
|
||||
* 1. HumanMessage - user input
|
||||
* 2. AIMessage with .tool_calls - agent picking tool(s) to use
|
||||
* 3. ToolMessage(s) - responses (or errors) from executed tools
|
||||
* (... repeat steps 2 and 3 as needed ...)
|
||||
* 4. AIMessage without .tool_calls - agent's unstructured response to user
|
||||
* 5. HumanMessage - user's next conversational turn
|
||||
* (... repeat steps 2-5 as needed ...)
|
||||
*/
|
||||
...MessagesAnnotation.spec,
|
||||
});
|
||||
|
||||
/**
|
||||
* Classifies user query.
|
||||
* @typedef {Object} Router
|
||||
* @property {string} logic - The logic behind the classification.
|
||||
* @property {'more-info' | 'langchain' | 'general'} type - The type of the query.
|
||||
*/
|
||||
|
||||
type Router = {
|
||||
logic: string;
|
||||
type: "more-info" | "langchain" | "general";
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the state of the retrieval graph / agent.
|
||||
*/
|
||||
export const AgentStateAnnotation = Annotation.Root({
|
||||
...InputStateAnnotation.spec,
|
||||
|
||||
/**
|
||||
* The router's classification of the user's query.
|
||||
* @type {Router}
|
||||
*/
|
||||
router: Annotation<Router>({
|
||||
default: () => ({ type: "general", logic: "" }),
|
||||
reducer: (existing: Router, newRouter: Router) => ({
|
||||
...existing,
|
||||
...newRouter,
|
||||
}),
|
||||
}),
|
||||
|
||||
/**
|
||||
* A list of steps in the research plan.
|
||||
* @type {string[]}
|
||||
*/
|
||||
steps: Annotation<string[]>,
|
||||
|
||||
/**
|
||||
* Populated by the retriever. This is a list of documents that the agent can reference.
|
||||
* @type {Document[]}
|
||||
*/
|
||||
documents: Annotation<
|
||||
Document[],
|
||||
Document[] | { [key: string]: any }[] | string[] | string | "delete"
|
||||
>({
|
||||
default: () => [],
|
||||
// @ts-ignore
|
||||
reducer: reduceDocs,
|
||||
}),
|
||||
|
||||
// Additional attributes can be added here as needed
|
||||
// Examples might include retrieved documents, extracted entities, API connections, etc.
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { Document } from "langchain/document";
|
||||
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
export function getMessageText(msg: BaseMessage): string {
|
||||
/** Get the text content of a message. */
|
||||
const content = msg.content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const txts = (content as any[]).map((c) =>
|
||||
typeof c === "string" ? c : c.text || "",
|
||||
);
|
||||
return txts.join("").trim();
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDoc(doc: Document): string {
|
||||
const metadata = doc.metadata || {};
|
||||
const meta = Object.entries(metadata)
|
||||
.map(([k, v]) => ` ${k}=${v}`)
|
||||
.join("");
|
||||
const metaStr = meta ? ` ${meta}` : "";
|
||||
|
||||
return `<document${metaStr}>\n${doc.pageContent}\n</document>`;
|
||||
}
|
||||
|
||||
export function formatDocs(docs?: Document[]): string {
|
||||
/**Format a list of documents as XML. */
|
||||
if (!docs || docs.length === 0) {
|
||||
return "<documents></documents>";
|
||||
}
|
||||
const formatted = docs.map(formatDoc).join("\n");
|
||||
return `<documents>\n${formatted}\n</documents>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a chat model from a fully specified name.
|
||||
* @param fullySpecifiedName - String in the format 'provider/model' or 'provider/account/provider/model'.
|
||||
* @returns A Promise that resolves to a BaseChatModel instance.
|
||||
*/
|
||||
export async function loadChatModel(
|
||||
fullySpecifiedName: string,
|
||||
): Promise<BaseChatModel> {
|
||||
const index = fullySpecifiedName.indexOf("/");
|
||||
if (index === -1) {
|
||||
// If there's no "/", assume it's just the model
|
||||
return await initChatModel(fullySpecifiedName);
|
||||
} else {
|
||||
const provider = fullySpecifiedName.slice(0, index);
|
||||
const model = fullySpecifiedName.slice(index + 1);
|
||||
return await initChatModel(model, { modelProvider: provider });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
[
|
||||
{
|
||||
"pageContent": "All runnables expose input and output **schemas** to inspect the inputs and outputs:\n\n- `input_schema`: an input Pydantic model auto-generated from the structure of the Runnable\n\n- `output_schema`: an output Pydantic model auto-generated from the structure of the Runnable\n\n## Components\u200b\n\nLangChain provides standard, extendable interfaces and external integrations for various components useful for building with LLMs.\nSome components LangChain implements, some components we rely on third-party integrations for, and others are a mix.\n\n### Chat models\u200b\n\nLanguage models that use a sequence of messages as inputs and return chat messages as outputs (as opposed to using plain text).\nThese are traditionally newer models (older models are generally `LLMs`, see below).\nChat models support the assignment of distinct roles to conversation messages, helping to distinguish messages from the AI, users, and instructions such as system messages.\n\nAlthough the underlying models are messages in, message out, the LangChain wrappers also allow these models to take a string as input. This means you can easily use chat models in place of LLMs.\n\nWhen a string is passed in as input, it is converted to a `HumanMessage` and then passed to the underlying model.\n\nLangChain does not host any Chat Models, rather we rely on third party integrations.\n\nWe have some standardized parameters when constructing ChatModels:\n\n- `model`: the name of the model\n\n- `temperature`: the sampling temperature\n\n- `timeout`: request timeout\n\n- `max_tokens`: max tokens to generate\n\n- `stop`: default stop sequences\n\n- `max_retries`: max number of times to retry requests\n\n- `api_key`: API key for the model provider\n\n- `base_url`: endpoint to send requests to\n\nSome important things to note:\n\n- standard params only apply to model providers that expose parameters with the intended functionality. For example, some providers do not expose a configuration for maximum output tokens, so max_tokens can't be supported on these.\n\n- standard params are currently only enforced on integrations that have their own integration packages (e.g. `langchain-openai`, `langchain-anthropic`, etc.), they're not enforced on models in `langchain-community`.\n\nChatModels also accept other parameters that are specific to that integration. To find all the parameters supported by a ChatModel head to the API reference for that model.\n\ninfoSome chat models have been fine-tuned for **tool calling** and provide a dedicated API for it.\nGenerally, such models are better at tool calling than non-fine-tuned models, and are recommended for use cases that require tool calling.\nPlease see the [tool calling section](/v0.2/docs/concepts/#functiontool-calling) for more information.\n\nFor specifics on how to use chat models, see the [relevant how-to guides here](/v0.2/docs/how_to/#chat-models).\n\n#### Multimodality\u200b\n\nSome chat models are multimodal, accepting images, audio and even video as inputs. These are still less common, meaning model providers haven't standardized on the \"best\" way to define the API. Multimodal **outputs** are even less common. As such, we've kept our multimodal abstractions fairly light weight and plan to further solidify the multimodal APIs and interaction patterns as the field matures.\n\nIn LangChain, most chat models that support multimodal inputs also accept those values in OpenAI's content blocks format. So far this is restricted to image inputs. For models like Gemini which support video and other bytes input, the APIs also support the native, model-specific representations.\n\nFor specifics on how to use multimodal models, see the [relevant how-to guides here](/v0.2/docs/how_to/#multimodal).\n\nFor a full list of LangChain model providers with multimodal models, [check out this table](/v0.2/docs/integrations/chat/#advanced-features).\n\n### LLMs\u200b",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "### Prompt templates\u200b\n\nPrompt templates help to translate user input and parameters into instructions for a language model.\nThis can be used to guide a model's response, helping it understand the context and generate relevant and coherent language-based output.\n\nPrompt Templates take as input a dictionary, where each key represents a variable in the prompt template to fill in.\n\nPrompt Templates output a PromptValue. This PromptValue can be passed to an LLM or a ChatModel, and can also be cast to a string or a list of messages.\nThe reason this PromptValue exists is to make it easy to switch between strings and messages.\n\nThere are a few different types of prompt templates:\n\n#### String PromptTemplates\u200b\n\nThese prompt templates are used to format a single string, and generally are used for simpler inputs.\nFor example, a common way to construct and use a PromptTemplate is as follows:\n\n```python\nfrom langchain_core.prompts import PromptTemplate\n\nprompt_template = PromptTemplate.from_template(\"Tell me a joke about {topic}\")\n\nprompt_template.invoke({\"topic\": \"cats\"})\n```\n\n**API Reference:**[PromptTemplate](https://python.langchain.com/v0.2/api_reference/core/prompts/langchain_core.prompts.prompt.PromptTemplate.html)#### ChatPromptTemplates\u200b\n\nThese prompt templates are used to format a list of messages. These \"templates\" consist of a list of templates themselves.\nFor example, a common way to construct and use a ChatPromptTemplate is as follows:\n\n```python\nfrom langchain_core.prompts import ChatPromptTemplate\n\nprompt_template = ChatPromptTemplate.from_messages([\n (\"system\", \"You are a helpful assistant\"),\n (\"user\", \"Tell me a joke about {topic}\")\n])\n\nprompt_template.invoke({\"topic\": \"cats\"})\n```\n\n**API Reference:**[ChatPromptTemplate](https://python.langchain.com/v0.2/api_reference/core/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html)In the above example, this ChatPromptTemplate will construct two messages when called.\nThe first is a system message, that has no variables to format.\nThe second is a HumanMessage, and will be formatted by the `topic` variable the user passes in.\n\n#### MessagesPlaceholder\u200b\n\nThis prompt template is responsible for adding a list of messages in a particular place.\nIn the above ChatPromptTemplate, we saw how we could format two messages, each one a string.\nBut what if we wanted the user to pass in a list of messages that we would slot into a particular spot?\nThis is how you use MessagesPlaceholder.\n\n```python\nfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\nfrom langchain_core.messages import HumanMessage\n\nprompt_template = ChatPromptTemplate.from_messages([\n (\"system\", \"You are a helpful assistant\"),\n MessagesPlaceholder(\"msgs\")\n])\n\nprompt_template.invoke({\"msgs\": [HumanMessage(content=\"hi!\")]})\n```\n\n**API Reference:**[ChatPromptTemplate](https://python.langchain.com/v0.2/api_reference/core/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html) | [MessagesPlaceholder](https://python.langchain.com/v0.2/api_reference/core/prompts/langchain_core.prompts.chat.MessagesPlaceholder.html) | [HumanMessage](https://python.langchain.com/v0.2/api_reference/core/messages/langchain_core.messages.human.HumanMessage.html)This will produce a list of two messages, the first one being a system message, and the second one being the HumanMessage we passed in.\nIf we had passed in 5 messages, then it would have produced 6 messages in total (the system message plus the 5 passed in).\nThis is useful for letting a list of messages be slotted into a particular spot.\n\nAn alternative way to accomplish the same thing without using the `MessagesPlaceholder` class explicitly is:\n\n```python\nprompt_template = ChatPromptTemplate.from_messages([\n (\"system\", \"You are a helpful assistant\"),\n (\"placeholder\", \"{msgs}\") # <-- This is the changed part\n])\n```\n\nFor specifics on how to use prompt templates, see the [relevant how-to guides here](/v0.2/docs/how_to/#prompt-templates).",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "model = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n\nfor chunk in model.stream(\"what color is the sky?\"):\n print(chunk.content, end=\"|\", flush=True)\n```\n\n**API Reference:**[ChatAnthropic](https://python.langchain.com/v0.2/api_reference/anthropic/chat_models/langchain_anthropic.chat_models.ChatAnthropic.html)For models (or other components) that don't support streaming natively, this iterator would just yield a single chunk, but\nyou could still use the same general pattern when calling them. Using `.stream()` will also automatically call the model in streaming mode\nwithout the need to provide additional config.\n\nThe type of each outputted chunk depends on the type of component - for example, chat models yield [AIMessageChunks](https://python.langchain.com/v0.2/api_reference/core/messages/langchain_core.messages.ai.AIMessageChunk.html).\nBecause this method is part of [LangChain Expression Language](/v0.2/docs/concepts/#langchain-expression-language-lcel),\nyou can handle formatting differences from different outputs using an [output parser](/v0.2/docs/concepts/#output-parsers) to transform\neach yielded chunk.\n\nYou can check out [this guide](/v0.2/docs/how_to/streaming/#using-stream) for more detail on how to use `.stream()`.\n\n#### .astream_events()\u200b\n\nWhile the `.stream()` method is intuitive, it can only return the final generated value of your chain. This is fine for single LLM calls,\nbut as you build more complex chains of several LLM calls together, you may want to use the intermediate values of\nthe chain alongside the final output - for example, returning sources alongside the final generation when building a chat\nover documents app.\n\nThere are ways to do this [using callbacks](/v0.2/docs/concepts/#callbacks-1), or by constructing your chain in such a way that it passes intermediate\nvalues to the end with something like chained [.assign()](/v0.2/docs/how_to/passthrough/) calls, but LangChain also includes an\n`.astream_events()` method that combines the flexibility of callbacks with the ergonomics of `.stream()`. When called, it returns an iterator\nwhich yields [various types of events](/v0.2/docs/how_to/streaming/#event-reference) that you can filter and process according\nto the needs of your project.\n\nHere's one small example that prints just events containing streamed chat model output:\n\n```python\nfrom langchain_core.output_parsers import StrOutputParser\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n\nprompt = ChatPromptTemplate.from_template(\"tell me a joke about {topic}\")\nparser = StrOutputParser()\nchain = prompt | model | parser\n\nasync for event in chain.astream_events({\"topic\": \"parrot\"}, version=\"v2\"):\n kind = event[\"event\"]\n if kind == \"on_chat_model_stream\":\n print(event, end=\"|\", flush=True)\n```\n\n**API Reference:**[StrOutputParser](https://python.langchain.com/v0.2/api_reference/core/output_parsers/langchain_core.output_parsers.string.StrOutputParser.html) | [ChatPromptTemplate](https://python.langchain.com/v0.2/api_reference/core/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html) | [ChatAnthropic](https://python.langchain.com/v0.2/api_reference/anthropic/chat_models/langchain_anthropic.chat_models.ChatAnthropic.html)You can roughly think of it as an iterator over callback events (though the format differs) - and you can use it on almost all LangChain components!\n\nSee [this guide](/v0.2/docs/how_to/streaming/#using-stream-events) for more detailed information on how to use `.astream_events()`,\nincluding a table listing available events.\n\n#### Callbacks\u200b",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "You MUST compile your graph before you can use it.\nState\u00b6\nThe first thing you do when you define a graph is define the State of the graph. The State consists of the schema of the graph as well as reducer functions which specify how to apply updates to the state. The schema of the State will be the input schema to all Nodes and Edges in the graph, and can be either a TypedDict or a Pydantic model. All Nodes will emit updates to the State which are then applied using the specified reducer function.\nSchema\u00b6\nThe main documented way to specify the schema of a graph is by using TypedDict. However, we also support using a Pydantic BaseModel as your graph state to add default values and additional data validation.\nBy default, the graph will have the same input and output schemas. If you want to change this, you can also specify explicit input and output schemas directly. This is useful when you have a lot of keys, and some are explicitly for input and others for output. See the notebook here for how to use.\nMultiple schemas\u00b6\nTypically, all graph nodes communicate with a single schema. This means that they will read and write to the same state channels. But, there are cases where we want more control over this:\n\nInternal nodes can pass information that is not required in the graph's input / output.\nWe may also want to use different input / output schemas for the graph. The output might, for example, only contain a single relevant output key.\n\nIt is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, PrivateState. See this notebook for more detail. \nIt is also possible to define explicit input and output schemas for a graph. In these cases, we define an \"internal\" schema that contains all keys relevant to graph operations. But, we also define input and output schemas that are sub-sets of the \"internal\" schema to constrain the input and output of the graph. See this notebook for more detail.\nLet's look at an example:\nclass InputState(TypedDict):\n user_input: str\n\nclass OutputState(TypedDict):\n graph_output: str\n\nclass OverallState(TypedDict):\n foo: str\n user_input: str\n graph_output: str\n\nclass PrivateState(TypedDict):\n bar: str\n\ndef node_1(state: InputState) -> OverallState:\n # Write to OverallState\n return {\"foo\": state[\"user_input\"] + \" name\"}\n\ndef node_2(state: OverallState) -> PrivateState:\n # Read from OverallState, write to PrivateState\n return {\"bar\": state[\"foo\"] + \" is\"}\n\ndef node_3(state: PrivateState) -> OutputState:\n # Read from PrivateState, write to OutputState\n return {\"graph_output\": state[\"bar\"] + \" Lance\"}\n\nbuilder = StateGraph(OverallState,input=InputState,output=OutputState)\nbuilder.add_node(\"node_1\", node_1)\nbuilder.add_node(\"node_2\", node_2)\nbuilder.add_node(\"node_3\", node_3)\nbuilder.add_edge(START, \"node_1\")\nbuilder.add_edge(\"node_1\", \"node_2\")\nbuilder.add_edge(\"node_2\", \"node_3\")\nbuilder.add_edge(\"node_3\", END)\n\ngraph = builder.compile()\ngraph.invoke({\"user_input\":\"My\"})\n{'graph_output': 'My name is Lance'}\n\nThere are two subtle and important points to note here:\n\nWe pass state: InputState as the input schema to node_1. But, we write out to foo, a channel in OverallState. How can we write out to a state channel that is not included in the input schema? This is because a node can write to any state channel in the graph state. The graph state is the union of of the state channels defined at initialization, which includes OverallState and the filters InputState and OutputState.",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "| Name | Index Type | Uses an LLM | When to Use | Description |\n| ---- | ---- | ---- | ---- | ---- |\n| Vector store | Vector store | No | If you are just getting started and looking for something quick and easy. | This is the simplest method and the one that is easiest to get started with. It involves creating embeddings for each piece of text. |\n| ParentDocument | Vector store + Document Store | No | If your pages have lots of smaller pieces of distinct information that are best indexed by themselves, but best retrieved all together. | This involves indexing multiple chunks for each document. Then you find the chunks that are most similar in embedding space, but you retrieve the whole parent document and return that (rather than individual chunks). |\n| Multi Vector | Vector store + Document Store | Sometimes during indexing | If you are able to extract information from documents that you think is more relevant to index than the text itself. | This involves creating multiple vectors for each document. Each vector could be created in a myriad of ways - examples include summaries of the text and hypothetical questions. |\n| Time-Weighted Vector store | Vector store | No | If you have timestamps associated with your documents, and you want to retrieve the most recent ones | This fetches documents based on a combination of semantic similarity (as in normal vector retrieval) and recency (looking at timestamps of indexed documents) |\n\ntip- See our RAG from Scratch video on [indexing fundamentals](https://youtu.be/bjb_EMsTDKI?feature=shared)\n\n- See our RAG from Scratch video on [multi vector retriever](https://youtu.be/gTCU9I6QqCE?feature=shared)\n\nFifth, consider ways to improve the quality of your similarity search itself. Embedding models compress text into fixed-length (vector) representations that capture the semantic content of the document. This compression is useful for search / retrieval, but puts a heavy burden on that single vector representation to capture the semantic nuance / detail of the document. In some cases, irrelevant or redundant content can dilute the semantic usefulness of the embedding.\n\n[ColBERT](https://docs.google.com/presentation/d/1IRhAdGjIevrrotdplHNcc4aXgIYyKamUKTWtB3m3aMU/edit?usp=sharing) is an interesting approach to address this with a higher granularity embeddings: (1) produce a contextually influenced embedding for each token in the document and query, (2) score similarity between each query token and all document tokens, (3) take the max, (4) do this for all query tokens, and (5) take the sum of the max scores (in step 3) for all query tokens to get a query-document similarity score; this token-wise scoring can yield strong results. \n\n\n\nThere are some additional tricks to improve the quality of your retrieval. Embeddings excel at capturing semantic information, but may struggle with keyword-based queries. Many [vector stores](/v0.2/docs/integrations/retrievers/pinecone_hybrid_search/) offer built-in [hybrid-search](https://docs.pinecone.io/guides/data/understanding-hybrid-search) to combine keyword and semantic similarity, which marries the benefits of both approaches. Furthermore, many vector stores have [maximal marginal relevance](https://python.langchain.com/v0.1/docs/modules/model_io/prompts/example_selectors/mmr/), which attempts to diversify the results of a search to avoid returning similar and redundant documents.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "graph = StateGraph(State, config_schema=ConfigSchema)\n\nYou can then pass this configuration into the graph using the configurable config field.\nconfig = {\"configurable\": {\"llm\": \"anthropic\"}}\n\ngraph.invoke(inputs, config=config)\n\nYou can then access and use this configuration inside a node:\ndef node_a(state, config):\n llm_type = config.get(\"configurable\", {}).get(\"llm\", \"openai\")\n llm = get_llm(llm_type)\n ...\n\nSee this guide for a full breakdown on configuration.\nRecursion Limit\u00b6\nThe recursion limit sets the maximum number of super-steps the graph can execute during a single execution. Once the limit is reached, LangGraph will raise GraphRecursionError. By default this value is set to 25 steps. The recursion limit can be set on any graph at runtime, and is passed to .invoke/.stream via the config dictionary. Importantly, recursion_limit is a standalone config key and should not be passed inside the configurable key as all other user-defined configuration. See the example below:\ngraph.invoke(inputs, config={\"recursion_limit\": 5, \"configurable\":{\"llm\": \"anthropic\"}})\n\nRead this how-to to learn more about how the recursion limit works.\nBreakpoints\u00b6\nIt can often be useful to set breakpoints before or after certain nodes execute. This can be used to wait for human approval before continuing. These can be set when you \"compile\" a graph. You can set breakpoints either before a node executes (using interrupt_before) or after a node executes (using interrupt_after.)\nYou MUST use a checkpoiner when using breakpoints. This is because your graph needs to be able to resume execution.\nIn order to resume execution, you can just invoke your graph with None as the input.\n# Initial run of graph\ngraph.invoke(inputs, config=config)\n\n# Let's assume it hit a breakpoint somewhere, you can then resume by passing in None\ngraph.invoke(None, config=config)\n\nSee this guide for a full walkthrough of how to add breakpoints.\nDynamic Breakpoints\u00b6\nIt may be helpful to dynamically interrupt the graph from inside a given node based on some condition. In LangGraph you can do so by using NodeInterrupt -- a special exception that can be raised from inside a node.\ndef my_node(state: State) -> State:\n if len(state['input']) > 5:\n raise NodeInterrupt(f\"Received input that is longer than 5 characters: {state['input']}\")\n\n return state\n\nVisualization\u00b6\nIt's often nice to be able to visualize graphs, especially as they get more complex. LangGraph comes with several built-in ways to visualize graphs. See this how-to guide for more info.\nStreaming\u00b6\nLangGraph is built with first class support for streaming, including streaming updates from graph nodes during the execution, streaming tokens from LLM calls and more. See this conceptual guide for more information.\nComments",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "See [this guide](/v0.2/docs/how_to/streaming/#using-stream-events) for more detailed information on how to use `.astream_events()`,\nincluding a table listing available events.\n\n#### Callbacks\u200b\n\nThe lowest level way to stream outputs from LLMs in LangChain is via the [callbacks](/v0.2/docs/concepts/#callbacks) system. You can pass a\ncallback handler that handles the [on_llm_new_token](https://python.langchain.com/v0.2/api_reference/langchain/callbacks/langchain.callbacks.streaming_aiter.AsyncIteratorCallbackHandler.html#langchain.callbacks.streaming_aiter.AsyncIteratorCallbackHandler.on_llm_new_token) event into LangChain components. When that component is invoked, any\n[LLM](/v0.2/docs/concepts/#llms) or [chat model](/v0.2/docs/concepts/#chat-models) contained in the component calls\nthe callback with the generated token. Within the callback, you could pipe the tokens into some other destination, e.g. a HTTP response.\nYou can also handle the [on_llm_end](https://python.langchain.com/v0.2/api_reference/langchain/callbacks/langchain.callbacks.streaming_aiter.AsyncIteratorCallbackHandler.html#langchain.callbacks.streaming_aiter.AsyncIteratorCallbackHandler.on_llm_end) event to perform any necessary cleanup.\n\nYou can see [this how-to section](/v0.2/docs/how_to/#callbacks) for more specifics on using callbacks.\n\nCallbacks were the first technique for streaming introduced in LangChain. While powerful and generalizable,\nthey can be unwieldy for developers. For example:\n\n- You need to explicitly initialize and manage some aggregator or other stream to collect results.\n\n- The execution order isn't explicitly guaranteed, and you could theoretically have a callback run after the `.invoke()` method finishes.\n\n- Providers would often make you pass an additional parameter to stream outputs instead of returning them all at once.\n\n- You would often ignore the result of the actual model call in favor of callback results.\n\n#### Tokens\u200b\n\nThe unit that most model providers use to measure input and output is via a unit called a **token**.\nTokens are the basic units that language models read and generate when processing or producing text.\nThe exact definition of a token can vary depending on the specific way the model was trained -\nfor instance, in English, a token could be a single word like \"apple\", or a part of a word like \"app\".\n\nWhen you send a model a prompt, the words and characters in the prompt are encoded into tokens using a **tokenizer**.\nThe model then streams back generated output tokens, which the tokenizer decodes into human-readable text.\nThe below example shows how OpenAI models tokenize `LangChain is cool!`:\n\n\n\nYou can see that it gets split into 5 different tokens, and that the boundaries between tokens are not exactly the same as word boundaries.\n\nThe reason language models use tokens rather than something more immediately intuitive like \"characters\"\nhas to do with how they process and understand text. At a high-level, language models iteratively predict their next generated output based on\nthe initial input and their previous generations. Training the model using tokens language models to handle linguistic\nunits (like words or subwords) that carry meaning, rather than individual characters, which makes it easier for the model\nto learn and understand the structure of the language, including grammar and context.\nFurthermore, using tokens can also improve efficiency, since the model processes fewer units of text compared to character-level processing.\n\n### Function/tool calling\u200b\n\ninfoWe use the term `tool calling` interchangeably with `function calling`. Although\nfunction calling is sometimes meant to refer to invocations of a single function,\nwe treat all models as though they can return multiple tool or function calls in\neach message.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "tip- See our RAG from Scratch [code](https://github.com/langchain-ai/rag-from-scratch) and [video series](https://youtube.com/playlist?list=PLfaIDFEXuae2LXbO1_PKyVJiQ23ZztA0x&feature=shared).\n\n- For a high-level guide on retrieval, see this [tutorial on RAG](/v0.2/docs/tutorials/rag/).\n\nRAG is only as good as the retrieved documents\u2019 relevance and quality. Fortunately, an emerging set of techniques can be employed to design and improve RAG systems. We've focused on taxonomizing and summarizing many of these techniques (see below figure) and will share some high-level strategic guidance in the following sections.\nYou can and should experiment with using different pieces together. You might also find [this LangSmith guide](https://docs.smith.langchain.com/how_to_guides/evaluation/evaluate_llm_application) useful for showing how to evaluate different iterations of your app.\n\n\n\n#### Query Translation\u200b\n\nFirst, consider the user input(s) to your RAG system. Ideally, a RAG system can handle a wide range of inputs, from poorly worded questions to complex multi-part queries.\n**Using an LLM to review and optionally modify the input is the central idea behind query translation.** This serves as a general buffer, optimizing raw user inputs for your retrieval system.\nFor example, this can be as simple as extracting keywords or as complex as generating multiple sub-questions for a complex query.\n\n| Name | When to use | Description |\n| ---- | ---- | ---- |\n| Multi-query | When you need to cover multiple perspectives of a question. | Rewrite the user question from multiple perspectives, retrieve documents for each rewritten question, return the unique documents for all queries. |\n| Decomposition | When a question can be broken down into smaller subproblems. | Decompose a question into a set of subproblems / questions, which can either be solved sequentially (use the answer from first + retrieval to answer the second) or in parallel (consolidate each answer into final answer). |\n| Step-back | When a higher-level conceptual understanding is required. | First prompt the LLM to ask a generic step-back question about higher-level concepts or principles, and retrieve relevant facts about them. Use this grounding to help answer the user question.Paper. |\n| HyDE | If you have challenges retrieving relevant documents using the raw user inputs. | Use an LLM to convert questions into hypothetical documents that answer the question. Use the embedded hypothetical documents to retrieve real documents with the premise that doc-doc similarity search can produce more relevant matches.Paper. |\n\ntipSee our RAG from Scratch videos for a few different specific approaches:\n\n- [Multi-query](https://youtu.be/JChPi0CRnDY?feature=shared)\n\n- [Decomposition](https://youtu.be/h0OPWlEOank?feature=shared)\n\n- [Step-back](https://youtu.be/xn1jEjRyJ2U?feature=shared)\n\n- [HyDE](https://youtu.be/SaDzIVkYqyY?feature=shared)\n\n#### Routing\u200b\n\nSecond, consider the data sources available to your RAG system. You want to query across more than one database or across structured and unstructured data sources. **Using an LLM to review the input and route it to the appropriate data source is a simple and effective approach for querying across sources.**\n\n| Name | When to use | Description |\n| ---- | ---- | ---- |\n| Logical routing | When you can prompt an LLM with rules to decide where to route the input. | Logical routing can use an LLM to reason about the query and choose which datastore is most appropriate. |\n| Semantic routing | When semantic similarity is an effective way to determine where to route the input. | Semantic routing embeds both query and, typically a set of prompts. It then chooses the appropriate prompt based upon similarity. |\n\ntipSee our RAG from Scratch video on [routing](https://youtu.be/pfpIndq7Fi8?feature=shared). \n\n#### Query Construction\u200b",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "tipSee our RAG from Scratch video on [routing](https://youtu.be/pfpIndq7Fi8?feature=shared). \n\n#### Query Construction\u200b\n\nThird, consider whether any of your data sources require specific query formats. Many structured databases use SQL. Vector stores often have specific syntax for applying keyword filters to document metadata. **Using an LLM to convert a natural language query into a query syntax is a popular and powerful approach.**\nIn particular, [text-to-SQL](/v0.2/docs/tutorials/sql_qa/), [text-to-Cypher](/v0.2/docs/tutorials/graph/), and [query analysis for metadata filters](/v0.2/docs/tutorials/query_analysis/#query-analysis) are useful ways to interact with structured, graph, and vector databases respectively. \n\n| Name | When to Use | Description |\n| ---- | ---- | ---- |\n| Text to SQL | If users are asking questions that require information housed in a relational database, accessible via SQL. | This uses an LLM to transform user input into a SQL query. |\n| Text-to-Cypher | If users are asking questions that require information housed in a graph database, accessible via Cypher. | This uses an LLM to transform user input into a Cypher query. |\n| Self Query | If users are asking questions that are better answered by fetching documents based on metadata rather than similarity with the text. | This uses an LLM to transform user input into two things: (1) a string to look up semantically, (2) a metadata filter to go along with it. This is useful because oftentimes questions are about the METADATA of documents (not the content itself). |\n\ntipSee our [blog post overview](https://blog.langchain.dev/query-construction/) and RAG from Scratch video on [query construction](https://youtu.be/kl6NwWYxvbM?feature=shared), the process of text-to-DSL where DSL is a domain specific language required to interact with a given database. This converts user questions into structured queries. \n\n#### Indexing\u200b\n\nFourth, consider the design of your document index. A simple and powerful idea is to **decouple the documents that you index for retrieval from the documents that you pass to the LLM for generation.** Indexing frequently uses embedding models with vector stores, which [compress the semantic information in documents to fixed-size vectors](/v0.2/docs/concepts/#embedding-models).\n\nMany RAG approaches focus on splitting documents into chunks and retrieving some number based on similarity to an input question for the LLM. But chunk size and chunk number can be difficult to set and affect results if they do not provide full context for the LLM to answer a question. Furthermore, LLMs are increasingly capable of processing millions of tokens. \n\nTwo approaches can address this tension: (1) [Multi Vector](/v0.2/docs/how_to/multi_vector/) retriever using an LLM to translate documents into any form (e.g., often into a summary) that is well-suited for indexing, but returns full documents to the LLM for generation. (2) [ParentDocument](/v0.2/docs/how_to/parent_document_retriever/) retriever embeds document chunks, but also returns full documents. The idea is to get the best of both worlds: use concise representations (summaries or chunks) for retrieval, but use the full documents for answer generation.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "```python\nfrom langchain_community.document_loaders.csv_loader import CSVLoader\n\nloader = CSVLoader(\n ... # <-- Integration specific parameters here\n)\ndata = loader.load()\n```\n\n**API Reference:**[CSVLoader](https://python.langchain.com/v0.2/api_reference/community/document_loaders/langchain_community.document_loaders.csv_loader.CSVLoader.html)For specifics on how to use document loaders, see the [relevant how-to guides here](/v0.2/docs/how_to/#document-loaders).\n\n### Text splitters\u200b\n\nOnce you've loaded documents, you'll often want to transform them to better suit your application. The simplest example is you may want to split a long document into smaller chunks that can fit into your model's context window. LangChain has a number of built-in document transformers that make it easy to split, combine, filter, and otherwise manipulate documents.\n\nWhen you want to deal with long pieces of text, it is necessary to split up that text into chunks. As simple as this sounds, there is a lot of potential complexity here. Ideally, you want to keep the semantically related pieces of text together. What \"semantically related\" means could depend on the type of text. This notebook showcases several ways to do that.\n\nAt a high level, text splitters work as following:\n\n1. Split the text up into small, semantically meaningful chunks (often sentences).\n\n2. Start combining these small chunks into a larger chunk until you reach a certain size (as measured by some function).\n\n3. Once you reach that size, make that chunk its own piece of text and then start creating a new chunk of text with some overlap (to keep context between chunks).\n\nThat means there are two different axes along which you can customize your text splitter:\n\n1. How the text is split\n\n2. How the chunk size is measured\n\nFor specifics on how to use text splitters, see the [relevant how-to guides here](/v0.2/docs/how_to/#text-splitters).\n\n### Embedding models\u200b\n\nEmbedding models create a vector representation of a piece of text. You can think of a vector as an array of numbers that captures the semantic meaning of the text.\nBy representing the text in this way, you can perform mathematical operations that allow you to do things like search for other pieces of text that are most similar in meaning.\nThese natural language search capabilities underpin many types of [context retrieval](/v0.2/docs/concepts/#retrieval),\nwhere we provide an LLM with the relevant data it needs to effectively respond to a query.\n\n\n\nThe `Embeddings` class is a class designed for interfacing with text embedding models. There are many different embedding model providers (OpenAI, Cohere, Hugging Face, etc) and local models, and this class is designed to provide a standard interface for all of them.\n\nThe base Embeddings class in LangChain provides two methods: one for embedding documents and one for embedding a query. The former takes as input multiple texts, while the latter takes a single text. The reason for having these as two separate methods is that some embedding providers have different embedding methods for documents (to be searched over) vs queries (the search query itself).\n\nFor specifics on how to use embedding models, see the [relevant how-to guides here](/v0.2/docs/how_to/#embedding-models).\n\n### Vector stores\u200b\n\nOne of the most common ways to store and search over unstructured data is to embed it and store the resulting embedding vectors,\nand then at query time to embed the unstructured query and retrieve the embedding vectors that are 'most similar' to the embedded query.\nA vector store takes care of storing embedded data and performing vector search for you.\n\nMost vector stores can also store metadata about embedded vectors and support filtering on that metadata before\nsimilarity search, allowing you more control over returned documents.\n\nVector stores can be converted to the retriever interface by doing:",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "LangGraph Glossary\n\nLangGraph Glossary\u00b6\nGraphs\u00b6\nAt its core, LangGraph models agent workflows as graphs. You define the behavior of your agents using three key components:\n\nState: A shared data structure that represents the current snapshot of your application. It can be any Python type, but is typically a TypedDict or Pydantic BaseModel.\n\nNodes: Python functions that encode the logic of your agents. They receive the current State as input, perform some computation or side-effect, and return an updated State.\n\nEdges: Python functions that determine which Node to execute next based on the current State. They can be conditional branches or fixed transitions.\n\nBy composing Nodes and Edges, you can create complex, looping workflows that evolve the State over time. The real power, though, comes from how LangGraph manages that State. To emphasize: Nodes and Edges are nothing more than Python functions - they can contain an LLM or just good ol' Python code.\nIn short: nodes do the work. edges tell what to do next.\nLangGraph's underlying graph algorithm uses message passing to define a general program. When a Node completes its operation, it sends messages along one or more edges to other node(s). These recipient nodes then execute their functions, pass the resulting messages to the next set of nodes, and the process continues. Inspired by Google's Pregel system, the program proceeds in discrete \"super-steps.\"\nA super-step can be considered a single iteration over the graph nodes. Nodes that run in parallel are part of the same super-step, while nodes that run sequentially belong to separate super-steps. At the start of graph execution, all nodes begin in an inactive state. A node becomes active when it receives a new message (state) on any of its incoming edges (or \"channels\"). The active node then runs its function and responds with updates. At the end of each super-step, nodes with no incoming messages vote to halt by marking themselves as inactive. The graph execution terminates when all nodes are inactive and no messages are in transit.\nStateGraph\u00b6\nThe StateGraph class is the main graph class to uses. This is parameterized by a user defined State object.\nMessageGraph\u00b6\nThe MessageGraph class is a special type of graph. The State of a MessageGraph is ONLY a list of messages. This class is rarely used except for chatbots, as most applications require the State to be more complex than a list of messages.\nCompiling your graph\u00b6\nTo build your graph, you first define the state, you then add nodes and edges, and then you compile it. What exactly is compiling your graph and why is it needed?\nCompiling is a pretty simple step. It provides a few basic checks on the structure of your graph (no orphaned nodes, etc). It is also where you can specify runtime args like checkpointers and breakpoints. You compile your graph by just calling the .compile method:\ngraph = graph_builder.compile(...)",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "- Async callback handlers implement the [AsyncCallbackHandler](https://python.langchain.com/v0.2/api_reference/core/callbacks/langchain_core.callbacks.base.AsyncCallbackHandler.html) interface.\n\nDuring run-time LangChain configures an appropriate callback manager (e.g., [CallbackManager](https://python.langchain.com/v0.2/api_reference/core/callbacks/langchain_core.callbacks.manager.CallbackManager.html) or [AsyncCallbackManager](https://python.langchain.com/v0.2/api_reference/core/callbacks/langchain_core.callbacks.manager.AsyncCallbackManager.html) which will be responsible for calling the appropriate method on each \"registered\" callback handler when the event is triggered.\n\n#### Passing callbacks\u200b\n\nThe `callbacks` property is available on most objects throughout the API (Models, Tools, Agents, etc.) in two different places:\n\nThe callbacks are available on most objects throughout the API (Models, Tools, Agents, etc.) in two different places:\n\n- **Request time callbacks**: Passed at the time of the request in addition to the input data.\nAvailable on all standard `Runnable` objects. These callbacks are INHERITED by all children\nof the object they are defined on. For example, `chain.invoke({\"number\": 25}, {\"callbacks\": [handler]})`.\n\n- **Constructor callbacks**: `chain = TheNameOfSomeChain(callbacks=[handler])`. These callbacks\nare passed as arguments to the constructor of the object. The callbacks are scoped\nonly to the object they are defined on, and are **not** inherited by any children of the object.\n\ndangerConstructor callbacks are scoped only to the object they are defined on. They are **not** inherited by children\nof the object.\n\nIf you're creating a custom chain or runnable, you need to remember to propagate request time\ncallbacks to any child objects.\n\nAsync in Python<=3.10Any `RunnableLambda`, a `RunnableGenerator`, or `Tool` that invokes other runnables\nand is running `async` in python<=3.10, will have to propagate callbacks to child\nobjects manually. This is because LangChain cannot automatically propagate\ncallbacks to child objects in this case.\n\nThis is a common reason why you may fail to see events being emitted from custom\nrunnables or tools.\n\nFor specifics on how to use callbacks, see the [relevant how-to guides here](/v0.2/docs/how_to/#callbacks).\n\n## Techniques\u200b\n\n### Streaming\u200b\n\nIndividual LLM calls often run for much longer than traditional resource requests.\nThis compounds when you build more complex chains or agents that require multiple reasoning steps.\n\nFortunately, LLMs generate output iteratively, which means it's possible to show sensible intermediate results\nbefore the final response is ready. Consuming output as soon as it becomes available has therefore become a vital part of the UX\naround building apps with LLMs to help alleviate latency issues, and LangChain aims to have first-class support for streaming.\n\nBelow, we'll discuss some concepts and considerations around streaming in LangChain.\n\n#### .stream() and .astream()\u200b\n\nMost modules in LangChain include the `.stream()` method (and the equivalent `.astream()` method for [async](https://docs.python.org/3/library/asyncio.html) environments) as an ergonomic streaming interface.\n`.stream()` returns an iterator, which you can consume with a simple `for` loop. Here's an example with a chat model:\n\n```python\nfrom langchain_anthropic import ChatAnthropic\n\nmodel = ChatAnthropic(model=\"claude-3-sonnet-20240229\")\n\nfor chunk in model.stream(\"what color is the sky?\"):\n print(chunk.content, end=\"|\", flush=True)\n```",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "If you are still using AgentExecutor, do not fear: we still have a guide on [how to use AgentExecutor](/v0.2/docs/how_to/agent_executor/).\nIt is recommended, however, that you start to transition to LangGraph.\nIn order to assist in this, we have put together a [transition guide on how to do so](/v0.2/docs/how_to/migrate_agent/).\n\n#### ReAct agents\u200b\n\nOne popular architecture for building agents is [ReAct](https://arxiv.org/abs/2210.03629).\nReAct combines reasoning and acting in an iterative process - in fact the name \"ReAct\" stands for \"Reason\" and \"Act\".\n\nThe general flow looks like this:\n\n- The model will \"think\" about what step to take in response to an input and any previous observations.\n\n- The model will then choose an action from available tools (or choose to respond to the user).\n\n- The model will generate arguments to that tool.\n\n- The agent runtime (executor) will parse out the chosen tool and call it with the generated arguments.\n\n- The executor will return the results of the tool call back to the model as an observation.\n\n- This process repeats until the agent chooses to respond.\n\nThere are general prompting based implementations that do not require any model-specific features, but the most\nreliable implementations use features like [tool calling](/v0.2/docs/how_to/tool_calling/) to reliably format outputs\nand reduce variance.\n\nPlease see the [LangGraph documentation](https://langchain-ai.github.io/langgraph/) for more information,\nor [this how-to guide](/v0.2/docs/how_to/migrate_agent/) for specific information on migrating to LangGraph.\n\n### Callbacks\u200b\n\nLangChain provides a callbacks system that allows you to hook into the various stages of your LLM application. This is useful for logging, monitoring, streaming, and other tasks.\n\nYou can subscribe to these events by using the `callbacks` argument available throughout the API. This argument is list of handler objects, which are expected to implement one or more of the methods described below in more detail.\n\n#### Callback Events\u200b\n\n| Event | Event Trigger | Associated Method |\n| ---- | ---- | ---- |\n| Chat model start | When a chat model starts | on_chat_model_start |\n| LLM start | When a llm starts | on_llm_start |\n| LLM new token | When an llm OR chat model emits a new token | on_llm_new_token |\n| LLM ends | When an llm OR chat model ends | on_llm_end |\n| LLM errors | When an llm OR chat model errors | on_llm_error |\n| Chain start | When a chain starts running | on_chain_start |\n| Chain end | When a chain ends | on_chain_end |\n| Chain error | When a chain errors | on_chain_error |\n| Tool start | When a tool starts running | on_tool_start |\n| Tool end | When a tool ends | on_tool_end |\n| Tool error | When a tool errors | on_tool_error |\n| Agent action | When an agent takes an action | on_agent_action |\n| Agent finish | When an agent ends | on_agent_finish |\n| Retriever start | When a retriever starts | on_retriever_start |\n| Retriever end | When a retriever ends | on_retriever_end |\n| Retriever error | When a retriever errors | on_retriever_error |\n| Text | When arbitrary text is run | on_text |\n| Retry | When a retry event is run | on_retry |\n\n#### Callback handlers\u200b\n\nCallback handlers can either be `sync` or `async`:\n\n- Sync callback handlers implement the [BaseCallbackHandler](https://python.langchain.com/v0.2/api_reference/core/callbacks/langchain_core.callbacks.base.BaseCallbackHandler.html) interface.\n\n- Async callback handlers implement the [AsyncCallbackHandler](https://python.langchain.com/v0.2/api_reference/core/callbacks/langchain_core.callbacks.base.AsyncCallbackHandler.html) interface.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "We initialize the graph with StateGraph(OverallState,input=InputState,output=OutputState). So, how can we write to PrivateState in node_2? How does the graph gain access to this schema if it was not passed in the StateGraph initialization? We can do this because nodes can also declare additional state channels as long as the state schema definition exists. In this case, the PrivateState schema is defined, so we can add bar as a new state channel in the graph and write to it.\n\nReducers\u00b6\nReducers are key to understanding how updates from nodes are applied to the State. Each key in the State has its own independent reducer function. If no reducer function is explicitly specified then it is assumed that all updates to that key should override it. There are a few different types of reducers, starting with the default type of reducer:\nDefault Reducer\u00b6\nThese two examples show how to use the default reducer:\nExample A:\nfrom typing_extensions import TypedDict\n\nclass State(TypedDict):\n foo: int\n bar: list[str]\n\nIn this example, no reducer functions are specified for any key. Let's assume the input to the graph is {\"foo\": 1, \"bar\": [\"hi\"]}. Let's then assume the first Node returns {\"foo\": 2}. This is treated as an update to the state. Notice that the Node does not need to return the whole State schema - just an update. After applying this update, the State would then be {\"foo\": 2, \"bar\": [\"hi\"]}. If the second node returns {\"bar\": [\"bye\"]} then the State would then be {\"foo\": 2, \"bar\": [\"bye\"]}\nExample B:\nfrom typing import Annotated\nfrom typing_extensions import TypedDict\nfrom operator import add\n\nclass State(TypedDict):\n foo: int\n bar: Annotated[list[str], add]",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "Tool calling allows a [chat model](/v0.2/docs/concepts/#chat-models) to respond to a given prompt by generating output that\nmatches a user-defined schema.\n\nWhile the name implies that the model is performing\nsome action, this is actually not the case! The model only generates the arguments to a tool, and actually running the tool (or not) is up to the user.\nOne common example where you **wouldn't** want to call a function with the generated arguments\nis if you want to [extract structured output matching some schema](/v0.2/docs/concepts/#structured-output)\nfrom unstructured text. You would give the model an \"extraction\" tool that takes\nparameters matching the desired schema, then treat the generated output as your final\nresult.\n\n\n\nTool calling is not universal, but is supported by many popular LLM providers, including [Anthropic](/v0.2/docs/integrations/chat/anthropic/),\n[Cohere](/v0.2/docs/integrations/chat/cohere/), [Google](/v0.2/docs/integrations/chat/google_vertex_ai_palm/),\n[Mistral](/v0.2/docs/integrations/chat/mistralai/), [OpenAI](/v0.2/docs/integrations/chat/openai/), and even for locally-running models via [Ollama](/v0.2/docs/integrations/chat/ollama/).\n\nLangChain provides a standardized interface for tool calling that is consistent across different models.\n\nThe standard interface consists of:\n\n- `ChatModel.bind_tools()`: a method for specifying which tools are available for a model to call. This method accepts [LangChain tools](/v0.2/docs/concepts/#tools) as well as [Pydantic](https://pydantic.dev/) objects.\n\n- `AIMessage.tool_calls`: an attribute on the `AIMessage` returned from the model for accessing the tool calls requested by the model.\n\n#### Tool usage\u200b\n\nAfter the model calls tools, you can use the tool by invoking it, then passing the arguments back to the model.\nLangChain provides the [Tool](/v0.2/docs/concepts/#tools) abstraction to help you handle this.\n\nThe general flow is this:\n\n1. Generate tool calls with a chat model in response to a query.\n\n2. Invoke the appropriate tools using the generated tool call as arguments.\n\n3. Format the result of the tool invocations as [ToolMessages](/v0.2/docs/concepts/#toolmessage).\n\n4. Pass the entire list of messages back to the model so that it can generate a final answer (or call more tools).\n\n\n\nThis is how tool calling [agents](/v0.2/docs/concepts/#agents) perform tasks and answer queries.\n\nCheck out some more focused guides below:\n\n- [How to use chat models to call tools](/v0.2/docs/how_to/tool_calling/)\n\n- [How to pass tool outputs to chat models](/v0.2/docs/how_to/tool_results_pass_to_model/)\n\n- [Building an agent with LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/)\n\n### Structured output\u200b\n\nLLMs are capable of generating arbitrary text. This enables the model to respond appropriately to a wide\nrange of inputs, but for some use-cases, it can be useful to constrain the LLM's output\nto a specific format or structure. This is referred to as **structured output**.\n\nFor example, if the output is to be stored in a relational database,\nit is much easier if the model generates output that adheres to a defined schema or format.\n[Extracting specific information](/v0.2/docs/tutorials/extraction/) from unstructured text is another\ncase where this is particularly useful. Most commonly, the output format will be JSON,\nthough other formats such as [YAML](/v0.2/docs/how_to/output_parser_yaml/) can be useful too. Below, we'll discuss\na few ways to get structured output from models in LangChain.\n\n#### .with_structured_output()\u200b",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "| Name | When to use | Description |\n| ---- | ---- | ---- |\n| ColBERT | When higher granularity embeddings are needed. | ColBERT uses contextually influenced embeddings for each token in the document and query to get a granular query-document similarity score.Paper. |\n| Hybrid search | When combining keyword-based and semantic similarity. | Hybrid search combines keyword and semantic similarity, marrying the benefits of both approaches.Paper. |\n| Maximal Marginal Relevance (MMR) | When needing to diversify search results. | MMR attempts to diversify the results of a search to avoid returning similar and redundant documents. |\n\ntipSee our RAG from Scratch video on [ColBERT](https://youtu.be/cN6S0Ehm7_8?feature=shared%3E).\n\n#### Post-processing\u200b\n\nSixth, consider ways to filter or rank retrieved documents. This is very useful if you are [combining documents returned from multiple sources](/v0.2/docs/integrations/retrievers/cohere-reranker/#doing-reranking-with-coherererank), since it can can down-rank less relevant documents and / or [compress similar documents](/v0.2/docs/how_to/contextual_compression/#more-built-in-compressors-filters). \n\n| Name | Index Type | Uses an LLM | When to Use | Description |\n| ---- | ---- | ---- | ---- | ---- |\n| Contextual Compression | Any | Sometimes | If you are finding that your retrieved documents contain too much irrelevant information and are distracting the LLM. | This puts a post-processing step on top of another retriever and extracts only the most relevant information from retrieved documents. This can be done with embeddings or an LLM. |\n| Ensemble | Any | No | If you have multiple retrieval methods and want to try combining them. | This fetches documents from multiple retrievers and then combines them. |\n| Re-ranking | Any | Yes | If you want to rank retrieved documents based upon relevance, especially if you want to combine results from multiple retrieval methods . | Given a query and a list of documents, Rerank indexes the documents from most to least semantically relevant to the query. |\n\ntipSee our RAG from Scratch video on [RAG-Fusion](https://youtu.be/77qELPbNgxA?feature=shared) ([paper](https://arxiv.org/abs/2402.03367)), on approach for post-processing across multiple queries: Rewrite the user question from multiple perspectives, retrieve documents for each rewritten question, and combine the ranks of multiple search result lists to produce a single, unified ranking with [Reciprocal Rank Fusion (RRF)](https://towardsdatascience.com/forget-rag-the-future-is-rag-fusion-1147298d8ad1).\n\n#### Generation\u200b\n\n**Finally, consider ways to build self-correction into your RAG system.** RAG systems can suffer from low quality retrieval (e.g., if a user question is out of the domain for the index) and / or hallucinations in generation. A naive retrieve-generate pipeline has no ability to detect or self-correct from these kinds of errors. The concept of [\"flow engineering\"](https://x.com/karpathy/status/1748043513156272416) has been introduced [in the context of code generation](https://arxiv.org/abs/2401.08500): iteratively build an answer to a code question with unit tests to check and self-correct errors. Several works have applied this RAG, such as Self-RAG and Corrective-RAG. In both cases, checks for document relevance, hallucinations, and / or answer quality are performed in the RAG answer generation flow.\n\nWe've found that graphs are a great way to reliably express logical flows and have implemented ideas from several of these papers [using LangGraph](https://github.com/langchain-ai/langgraph/tree/main/examples/rag), as shown in the figure below (red - routing, blue - fallback, green - self-correction):\n\n- **Routing:** Adaptive RAG ([paper](https://arxiv.org/abs/2403.14403)). Route questions to different retrieval approaches, as discussed above \n\n- **Fallback:** Corrective RAG ([paper](https://arxiv.org/pdf/2401.15884.pdf)). Fallback to web search if docs are not relevant to query",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "Vector stores can be converted to the retriever interface by doing:\n\n```python\nvectorstore = MyVectorStore()\nretriever = vectorstore.as_retriever()\n```\n\nFor specifics on how to use vector stores, see the [relevant how-to guides here](/v0.2/docs/how_to/#vector-stores).\n\n### Retrievers\u200b\n\nA retriever is an interface that returns documents given an unstructured query.\nIt is more general than a vector store.\nA retriever does not need to be able to store documents, only to return (or retrieve) them.\nRetrievers can be created from vector stores, but are also broad enough to include [Wikipedia search](/v0.2/docs/integrations/retrievers/wikipedia/) and [Amazon Kendra](/v0.2/docs/integrations/retrievers/amazon_kendra_retriever/).\n\nRetrievers accept a string query as input and return a list of Document's as output.\n\nFor specifics on how to use retrievers, see the [relevant how-to guides here](/v0.2/docs/how_to/#retrievers).\n\n### Key-value stores\u200b\n\nFor some techniques, such as [indexing and retrieval with multiple vectors per document](/v0.2/docs/how_to/multi_vector/) or\n[caching embeddings](/v0.2/docs/how_to/caching_embeddings/), having a form of key-value (KV) storage is helpful.\n\nLangChain includes a [BaseStore](https://python.langchain.com/v0.2/api_reference/core/stores/langchain_core.stores.BaseStore.html) interface,\nwhich allows for storage of arbitrary data. However, LangChain components that require KV-storage accept a\nmore specific `BaseStore[str, bytes]` instance that stores binary data (referred to as a `ByteStore`), and internally take care of\nencoding and decoding data for their specific needs.\n\nThis means that as a user, you only need to think about one type of store rather than different ones for different types of data.\n\n#### Interface\u200b\n\nAll [BaseStores](https://python.langchain.com/v0.2/api_reference/core/stores/langchain_core.stores.BaseStore.html) support the following interface. Note that the interface allows\nfor modifying **multiple** key-value pairs at once:\n\n- `mget(key: Sequence[str]) -> List[Optional[bytes]]`: get the contents of multiple keys, returning `None` if the key does not exist\n\n- `mset(key_value_pairs: Sequence[Tuple[str, bytes]]) -> None`: set the contents of multiple keys\n\n- `mdelete(key: Sequence[str]) -> None`: delete multiple keys\n\n- `yield_keys(prefix: Optional[str] = None) -> Iterator[str]`: yield all keys in the store, optionally filtering by a prefix\n\nFor key-value store implementations, see [this section](/v0.2/docs/integrations/stores/).\n\n### Tools\u200b\n\nTools are utilities designed to be called by a model: their inputs are designed to be generated by models, and their outputs are designed to be passed back to models.\nTools are needed whenever you want a model to control parts of your code or call out to external APIs.\n\nA tool consists of:\n\n1. The `name` of the tool.\n\n2. A `description` of what the tool does.\n\n3. A `JSON schema` defining the inputs to the tool.\n\n4. A `function` (and, optionally, an async variant of the function).\n\nWhen a tool is bound to a model, the name, description and JSON schema are provided as context to the model.\nGiven a list of tools and a set of instructions, a model can request to call one or more tools with specific inputs.\nTypical usage may look like the following:\n\n```python\ntools = [...] # Define a list of tools\nllm_with_tools = llm.bind_tools(tools)\nai_msg = llm_with_tools.invoke(\"do xyz...\")\n# -> AIMessage(tool_calls=[ToolCall(...), ...], ...)\n```\n\nThe `AIMessage` returned from the model MAY have `tool_calls` associated with it.\nRead [this guide](/v0.2/docs/concepts/#aimessage) for more information on what the response type may look like.\n\nOnce the chosen tools are invoked, the results can be passed back to the model so that it can complete whatever task\nit's performing.\nThere are generally two different ways to invoke the tool and pass back the response:\n\n#### Invoke with just the arguments\u200b",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "- **Output Type**: The output type of the object returned by the parser.\n\n- **Description**: Our commentary on this output parser and when to use it.\n\n| Name | Supports Streaming | Has Format Instructions | Calls LLM | Input Type | Output Type | Description |\n| ---- | ---- | ---- | ---- | ---- | ---- | ---- |\n| JSON | \u2705 | \u2705 | | str|Message | JSON object | Returns a JSON object as specified. You can specify a Pydantic model and it will return JSON for that model. Probably the most reliable output parser for getting structured data that does NOT use function calling. |\n| XML | \u2705 | \u2705 | | str|Message | dict | Returns a dictionary of tags. Use when XML output is needed. Use with models that are good at writing XML (like Anthropic's). |\n| CSV | \u2705 | \u2705 | | str|Message | List[str] | Returns a list of comma separated values. |\n| OutputFixing | | | \u2705 | str|Message | | Wraps another output parser. If that output parser errors, then this will pass the error message and the bad output to an LLM and ask it to fix the output. |\n| RetryWithError | | | \u2705 | str|Message | | Wraps another output parser. If that output parser errors, then this will pass the original inputs, the bad output, and the error message to an LLM and ask it to fix it. Compared to OutputFixingParser, this one also sends the original instructions. |\n| Pydantic | | \u2705 | | str|Message | pydantic.BaseModel | Takes a user defined Pydantic model and returns data in that format. |\n| YAML | | \u2705 | | str|Message | pydantic.BaseModel | Takes a user defined Pydantic model and returns data in that format. Uses YAML to encode it. |\n| PandasDataFrame | | \u2705 | | str|Message | dict | Useful for doing operations with pandas DataFrames. |\n| Enum | | \u2705 | | str|Message | Enum | Parses response into one of the provided enum values. |\n| Datetime | | \u2705 | | str|Message | datetime.datetime | Parses response into a datetime string. |\n| Structured | | \u2705 | | str|Message | Dict[str, str] | An output parser that returns structured information. It is less powerful than other output parsers since it only allows for fields to be strings. This can be useful when you are working with smaller LLMs. |\n\nFor specifics on how to use output parsers, see the [relevant how-to guides here](/v0.2/docs/how_to/#output-parsers).\n\n### Chat history\u200b\n\nMost LLM applications have a conversational interface.\nAn essential component of a conversation is being able to refer to information introduced earlier in the conversation.\nAt bare minimum, a conversational system should be able to access some window of past messages directly.\n\nThe concept of `ChatHistory` refers to a class in LangChain which can be used to wrap an arbitrary chain.\nThis `ChatHistory` will keep track of inputs and outputs of the underlying chain, and append them as messages to a message database.\nFuture interactions will then load those messages and pass them into the chain as part of the input.\n\n### Documents\u200b\n\nA Document object in LangChain contains information about some data. It has two attributes:\n\n- `page_content: str`: The content of this document. Currently is only a string.\n\n- `metadata: dict`: Arbitrary metadata associated with this document. Can track the document id, file name, etc.\n\n### Document loaders\u200b\n\nThese classes load Document objects. LangChain has hundreds of integrations with various data sources to load data from: Slack, Notion, Google Drive, etc.\n\nEach DocumentLoader has its own specific parameters, but they can all be invoked in the same way with the `.load` method.\nAn example use case is as follows:\n\n```python\nfrom langchain_community.document_loaders.csv_loader import CSVLoader\n\nloader = CSVLoader(\n ... # <-- Integration specific parameters here\n)\ndata = loader.load()\n```",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "#### Invoke with just the arguments\u200b\n\nWhen you invoke a tool with just the arguments, you will get back the raw tool output (usually a string).\nThis generally looks like:\n\n```python\n# You will want to previously check that the LLM returned tool calls\ntool_call = ai_msg.tool_calls[0]\n# ToolCall(args={...}, id=..., ...)\ntool_output = tool.invoke(tool_call[\"args\"])\ntool_message = ToolMessage(\n content=tool_output,\n tool_call_id=tool_call[\"id\"],\n name=tool_call[\"name\"]\n)\n```\n\nNote that the `content` field will generally be passed back to the model.\nIf you do not want the raw tool response to be passed to the model, but you still want to keep it around,\nyou can transform the tool output but also pass it as an artifact (read more about [ToolMessage.artifact here](/v0.2/docs/concepts/#toolmessage))\n\n```python\n... # Same code as above\nresponse_for_llm = transform(response)\ntool_message = ToolMessage(\n content=response_for_llm,\n tool_call_id=tool_call[\"id\"],\n name=tool_call[\"name\"],\n artifact=tool_output\n)\n```\n\n#### Invoke with ToolCall\u200b\n\nThe other way to invoke a tool is to call it with the full `ToolCall` that was generated by the model.\nWhen you do this, the tool will return a ToolMessage.\nThe benefits of this are that you don't have to write the logic yourself to transform the tool output into a ToolMessage.\nThis generally looks like:\n\n```python\ntool_call = ai_msg.tool_calls[0]\n# -> ToolCall(args={...}, id=..., ...)\ntool_message = tool.invoke(tool_call)\n# -> ToolMessage(\n content=\"tool result foobar...\",\n tool_call_id=...,\n name=\"tool_name\"\n)\n```\n\nIf you are invoking the tool this way and want to include an [artifact](/v0.2/docs/concepts/#toolmessage) for the ToolMessage, you will need to have the tool return two things.\nRead more about [defining tools that return artifacts here](/v0.2/docs/how_to/tool_artifacts/).\n\n#### Best practices\u200b\n\nWhen designing tools to be used by a model, it is important to keep in mind that:\n\n- Chat models that have explicit [tool-calling APIs](/v0.2/docs/concepts/#functiontool-calling) will be better at tool calling than non-fine-tuned models.\n\n- Models will perform better if the tools have well-chosen names, descriptions, and JSON schemas. This another form of prompt engineering.\n\n- Simple, narrowly scoped tools are easier for models to use than complex tools.\n\n#### Related\u200b\n\nFor specifics on how to use tools, see the [tools how-to guides](/v0.2/docs/how_to/#tools).\n\nTo use a pre-built tool, see the [tool integration docs](/v0.2/docs/integrations/tools/).\n\n### Toolkits\u200b\n\nToolkits are collections of tools that are designed to be used together for specific tasks. They have convenient loading methods.\n\nAll Toolkits expose a `get_tools` method which returns a list of tools.\nYou can therefore do:\n\n```python\n# Initialize a toolkit\ntoolkit = ExampleTookit(...)\n\n# Get list of tools\ntools = toolkit.get_tools()\n```\n\n### Agents\u200b\n\nBy themselves, language models can't take actions - they just output text.\nA big use case for LangChain is creating **agents**.\nAgents are systems that use an LLM as a reasoning engine to determine which actions to take and what the inputs to those actions should be.\nThe results of those actions can then be fed back into the agent and it determine whether more actions are needed, or whether it is okay to finish.\n\n[LangGraph](https://github.com/langchain-ai/langgraph) is an extension of LangChain specifically aimed at creating highly controllable and customizable agents.\nPlease check out that documentation for a more in depth overview of agent concepts.\n\nThere is a legacy `agent` concept in LangChain that we are moving towards deprecating: `AgentExecutor`.\nAgentExecutor was essentially a runtime for agents.\nIt was a great place to get started, however, it was not flexible enough as you started to have more customized agents.\nIn order to solve that we built LangGraph to be this flexible, highly-controllable runtime.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "#### .with_structured_output()\u200b\n\nFor convenience, some LangChain chat models support a [.with_structured_output()](/v0.2/docs/how_to/structured_output/#the-with_structured_output-method)\nmethod. This method only requires a schema as input, and returns a dict or Pydantic object.\nGenerally, this method is only present on models that support one of the more advanced methods described below,\nand will use one of them under the hood. It takes care of importing a suitable output parser and\nformatting the schema in the right format for the model.\n\nHere's an example:\n\n```python\nfrom typing import Optional\n\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\nclass Joke(BaseModel):\n \"\"\"Joke to tell user.\"\"\"\n\n setup: str = Field(description=\"The setup of the joke\")\n punchline: str = Field(description=\"The punchline to the joke\")\n rating: Optional[int] = Field(description=\"How funny the joke is, from 1 to 10\")\n\nstructured_llm = llm.with_structured_output(Joke)\n\nstructured_llm.invoke(\"Tell me a joke about cats\")\n```\n\n```text\nJoke(setup='Why was the cat sitting on the computer?', punchline='To keep an eye on the mouse!', rating=None)\n```\n\nWe recommend this method as a starting point when working with structured output:\n\n- It uses other model-specific features under the hood, without the need to import an output parser.\n\n- For the models that use tool calling, no special prompting is needed.\n\n- If multiple underlying techniques are supported, you can supply a `method` parameter to\n[toggle which one is used](/v0.2/docs/how_to/structured_output/#advanced-specifying-the-method-for-structuring-outputs).\n\nYou may want or need to use other techniques if:\n\n- The chat model you are using does not support tool calling.\n\n- You are working with very complex schemas and the model is having trouble generating outputs that conform.\n\nFor more information, check out this [how-to guide](/v0.2/docs/how_to/structured_output/#the-with_structured_output-method).\n\nYou can also check out [this table](/v0.2/docs/integrations/chat/#advanced-features) for a list of models that support\n`with_structured_output()`.\n\n#### Raw prompting\u200b\n\nThe most intuitive way to get a model to structure output is to ask nicely.\nIn addition to your query, you can give instructions describing what kind of output you'd like, then\nparse the output using an [output parser](/v0.2/docs/concepts/#output-parsers) to convert the raw\nmodel message or string output into something more easily manipulated.\n\nThe biggest benefit to raw prompting is its flexibility:\n\n- Raw prompting does not require any special model features, only sufficient reasoning capability to understand\nthe passed schema.\n\n- You can prompt for any format you'd like, not just JSON. This can be useful if the model you\nare using is more heavily trained on a certain type of data, such as XML or YAML.\n\nHowever, there are some drawbacks too:\n\n- LLMs are non-deterministic, and prompting a LLM to consistently output data in the exactly correct format\nfor smooth parsing can be surprisingly difficult and model-specific.\n\n- Individual models have quirks depending on the data they were trained on, and optimizing prompts can be quite difficult.\nSome may be better at interpreting [JSON schema](https://json-schema.org/), others may be best with TypeScript definitions,\nand still others may prefer XML.\n\nWhile features offered by model providers may increase reliability, prompting techniques remain important for tuning your\nresults no matter which method you choose.\n\n#### JSON mode\u200b\n\nSome models, such as [Mistral](/v0.2/docs/integrations/chat/mistralai/), [OpenAI](/v0.2/docs/integrations/chat/openai/),\n[Together AI](/v0.2/docs/integrations/chat/together/) and [Ollama](/v0.2/docs/integrations/chat/ollama/),\nsupport a feature called **JSON mode**, usually enabled via config.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "- **Optimized parallel execution:**\nWhenever your LCEL chains have steps that can be executed in parallel (eg if you fetch documents from multiple retrievers) we automatically do it, both in the sync and the async interfaces, for the smallest possible latency.\n\n- **Retries and fallbacks:**\nConfigure retries and fallbacks for any part of your LCEL chain. This is a great way to make your chains more reliable at scale. We\u2019re currently working on adding streaming support for retries/fallbacks, so you can get the added reliability without any latency cost.\n\n- **Access intermediate results:**\nFor more complex chains it\u2019s often very useful to access the results of intermediate steps even before the final output is produced. This can be used to let end-users know something is happening, or even just to debug your chain. You can stream intermediate results, and it\u2019s available on every [LangServe](/v0.2/docs/langserve/) server.\n\n- **Input and output schemas**\nInput and output schemas give every LCEL chain Pydantic and JSONSchema schemas inferred from the structure of your chain. This can be used for validation of inputs and outputs, and is an integral part of LangServe.\n\n- [Seamless LangSmith tracing](https://docs.smith.langchain.com)\nAs your chains get more and more complex, it becomes increasingly important to understand what exactly is happening at every step.\nWith LCEL, **all** steps are automatically logged to [LangSmith](https://docs.smith.langchain.com/) for maximum observability and debuggability.\n\nLCEL aims to provide consistency around behavior and customization over legacy subclassed chains such as `LLMChain` and\n`ConversationalRetrievalChain`. Many of these legacy chains hide important details like prompts, and as a wider variety\nof viable models emerge, customization has become more and more important.\n\nIf you are currently using one of these legacy chains, please see [this guide for guidance on how to migrate](/v0.2/docs/versions/migrating_chains/).\n\nFor guides on how to do specific tasks with LCEL, check out [the relevant how-to guides](/v0.2/docs/how_to/#langchain-expression-language-lcel).\n\n### Runnable interface\u200b\n\nTo make it as easy as possible to create custom chains, we've implemented a [\"Runnable\"](https://python.langchain.com/v0.2/api_reference/core/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable) protocol. Many LangChain components implement the `Runnable` protocol, including chat models, LLMs, output parsers, retrievers, prompt templates, and more. There are also several useful primitives for working with runnables, which you can read about below.\n\nThis is a standard interface, which makes it easy to define custom chains as well as invoke them in a standard way.\nThe standard interface includes:\n\n- `stream`: stream back chunks of the response\n\n- `invoke`: call the chain on an input\n\n- `batch`: call the chain on a list of inputs\n\nThese also have corresponding async methods that should be used with [asyncio](https://docs.python.org/3/library/asyncio.html) `await` syntax for concurrency:\n\n- `astream`: stream back chunks of the response async\n\n- `ainvoke`: call the chain on an input async\n\n- `abatch`: call the chain on a list of inputs async\n\n- `astream_log`: stream back intermediate steps as they happen, in addition to the final response\n\n- `astream_events`: **beta** stream events as they happen in the chain (introduced in `langchain-core` 0.1.14)\n\nThe **input type** and **output type** varies by component:\n\n| Component | Input Type | Output Type |\n| ---- | ---- | ---- |\n| Prompt | Dictionary | PromptValue |\n| ChatModel | Single string, list of chat messages or a PromptValue | ChatMessage |\n| LLM | Single string, list of chat messages or a PromptValue | String |\n| OutputParser | The output of an LLM or ChatModel | Depends on the parser |\n| Retriever | Single string | List of Documents |\n| Tool | Single string or dictionary, depending on the tool | Depends on the tool |",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "For specifics on how to use prompt templates, see the [relevant how-to guides here](/v0.2/docs/how_to/#prompt-templates).\n\n### Example selectors\u200b\n\nOne common prompting technique for achieving better performance is to include examples as part of the prompt.\nThis is known as [few-shot prompting](/v0.2/docs/concepts/#few-shot-prompting).\nThis gives the language model concrete examples of how it should behave.\nSometimes these examples are hardcoded into the prompt, but for more advanced situations it may be nice to dynamically select them.\nExample Selectors are classes responsible for selecting and then formatting examples into prompts.\n\nFor specifics on how to use example selectors, see the [relevant how-to guides here](/v0.2/docs/how_to/#example-selectors).\n\n### Output parsers\u200b\n\nnoteThe information here refers to parsers that take a text output from a model try to parse it into a more structured representation.\nMore and more models are supporting function (or tool) calling, which handles this automatically.\nIt is recommended to use function/tool calling rather than output parsing.\nSee documentation for that [here](/v0.2/docs/concepts/#function-tool-calling).\n\n`Output parser` is responsible for taking the output of a model and transforming it to a more suitable format for downstream tasks.\nUseful when you are using LLMs to generate structured data, or to normalize output from chat models and LLMs.\n\nLangChain has lots of different types of output parsers. This is a list of output parsers LangChain supports. The table below has various pieces of information:\n\n- **Name**: The name of the output parser\n\n- **Supports Streaming**: Whether the output parser supports streaming.\n\n- **Has Format Instructions**: Whether the output parser has format instructions. This is generally available except when (a) the desired schema is not specified in the prompt but rather in other parameters (like OpenAI function calling), or (b) when the OutputParser wraps another OutputParser.\n\n- **Calls LLM**: Whether this output parser itself calls an LLM. This is usually only done by output parsers that attempt to correct misformatted output.\n\n- **Input Type**: Expected input type. Most output parsers work on both strings and messages, but some (like OpenAI Functions) need a message with specific kwargs.\n\n- **Output Type**: The output type of the object returned by the parser.\n\n- **Description**: Our commentary on this output parser and when to use it.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "\n\n[LangSmith](https://docs.smith.langchain.com/) helps with this process in a few ways:\n\n- It makes it easier to create and curate datasets via its tracing and annotation features\n\n- It provides an evaluation framework that helps you define metrics and run your app against your dataset\n\n- It allows you to track results over time and automatically run your evaluators on a schedule or as part of CI/Code\n\nTo learn more, check out [this LangSmith guide](https://docs.smith.langchain.com/concepts/evaluation).\n\n### Tracing\u200b\n\nA trace is essentially a series of steps that your application takes to go from input to output.\nTraces contain individual steps called `runs`. These can be individual calls from a model, retriever,\ntool, or sub-chains.\nTracing gives you observability inside your chains and agents, and is vital in diagnosing issues.\n\nFor a deeper dive, check out [this LangSmith conceptual guide](https://docs.smith.langchain.com/concepts/tracing).\n\n#### Was this page helpful?\n\n#### You can also leave detailed feedback on GitHub.\n\n- [Architecture](#architecture)- [langchain-core](#langchain-core)\n\n- [langchain](#langchain)\n\n- [langchain-community](#langchain-community)\n\n- [Partner packages](#partner-packages)\n\n- [langgraph](#langgraph)\n\n- [langserve](#langserve)\n\n- [LangSmith](#langsmith)\n\n- [LangChain Expression Language (LCEL)](#langchain-expression-language-lcel)- [Runnable interface](#runnable-interface)\n\n- [Components](#components)- [Chat models](#chat-models)\n\n- [LLMs](#llms)\n\n- [Messages](#messages)\n\n- [Prompt templates](#prompt-templates)\n\n- [Example selectors](#example-selectors)\n\n- [Output parsers](#output-parsers)\n\n- [Chat history](#chat-history)\n\n- [Documents](#documents)\n\n- [Document loaders](#document-loaders)\n\n- [Text splitters](#text-splitters)\n\n- [Embedding models](#embedding-models)\n\n- [Vector stores](#vector-stores)\n\n- [Retrievers](#retrievers)\n\n- [Key-value stores](#key-value-stores)\n\n- [Tools](#tools)\n\n- [Toolkits](#toolkits)\n\n- [Agents](#agents)\n\n- [Callbacks](#callbacks)\n\n- [Techniques](#techniques)- [Streaming](#streaming)\n\n- [Function/tool calling](#functiontool-calling)\n\n- [Structured output](#structured-output)\n\n- [Few-shot prompting](#few-shot-prompting)\n\n- [Retrieval](#retrieval)\n\n- [Text splitting](#text-splitting)\n\n- [Evaluation](#evaluation)\n\n- [Tracing](#tracing)",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "The following how-to guides are good practical resources for using function/tool calling for structured output:\n\n- [How to return structured data from an LLM](/v0.2/docs/how_to/structured_output/)\n\n- [How to use a model to call tools](/v0.2/docs/how_to/tool_calling/)\n\nFor a full list of model providers that support tool calling, [see this table](/v0.2/docs/integrations/chat/#advanced-features).\n\n### Few-shot prompting\u200b\n\nOne of the most effective ways to improve model performance is to give a model examples of\nwhat you want it to do. The technique of adding example inputs and expected outputs\nto a model prompt is known as \"few-shot prompting\". The technique is based on the\n[Language Models are Few-Shot Learners](https://arxiv.org/abs/2005.14165) paper.\nThere are a few things to think about when doing few-shot prompting:\n\n1. How are examples generated?\n\n2. How many examples are in each prompt?\n\n3. How are examples selected at runtime?\n\n4. How are examples formatted in the prompt?\n\nHere are the considerations for each.\n\n#### 1. Generating examples\u200b\n\nThe first and most important step of few-shot prompting is coming up with a good dataset of examples. Good examples should be relevant at runtime, clear, informative, and provide information that was not already known to the model.\n\nAt a high-level, the basic ways to generate examples are:\n\n- Manual: a person/people generates examples they think are useful.\n\n- Better model: a better (presumably more expensive/slower) model's responses are used as examples for a worse (presumably cheaper/faster) model.\n\n- User feedback: users (or labelers) leave feedback on interactions with the application and examples are generated based on that feedback (for example, all interactions with positive feedback could be turned into examples).\n\n- LLM feedback: same as user feedback but the process is automated by having models evaluate themselves.\n\nWhich approach is best depends on your task. For tasks where a small number core principles need to be understood really well, it can be valuable hand-craft a few really good examples.\nFor tasks where the space of correct behaviors is broader and more nuanced, it can be useful to generate many examples in a more automated fashion so that there's a higher likelihood of there being some highly relevant examples for any runtime input.\n\n**Single-turn v.s. multi-turn examples**\n\nAnother dimension to think about when generating examples is what the example is actually showing.\n\nThe simplest types of examples just have a user input and an expected model output. These are single-turn examples.\n\nOne more complex type if example is where the example is an entire conversation, usually in which a model initially responds incorrectly and a user then tells the model how to correct its answer.\nThis is called a multi-turn example. Multi-turn examples can be useful for more nuanced tasks where its useful to show common errors and spell out exactly why they're wrong and what should be done instead.\n\n#### 2. Number of examples\u200b\n\nOnce we have a dataset of examples, we need to think about how many examples should be in each prompt.\nThe key tradeoff is that more examples generally improve performance, but larger prompts increase costs and latency.\nAnd beyond some threshold having too many examples can start to confuse the model.\nFinding the right number of examples is highly dependent on the model, the task, the quality of the examples, and your cost and latency constraints.\nAnecdotally, the better the model is the fewer examples it needs to perform well and the more quickly you hit steeply diminishing returns on adding more examples.\nBut, the best/only way to reliably answer this question is to run some experiments with different numbers of examples.\n\n#### 3. Selecting examples\u200b\n\nAssuming we are not adding our entire example dataset into each prompt, we need to have a way of selecting examples from our dataset based on a given input. We can do this:\n\n- Randomly",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "class State(MessagesState):\n documents: list[str]\n\nNodes\u00b6\nIn LangGraph, nodes are typically python functions (sync or async) where the first positional argument is the state, and (optionally), the second positional argument is a \"config\", containing optional configurable parameters (such as a thread_id).\nSimilar to NetworkX, you add these nodes to a graph using the add_node method:\nfrom langchain_core.runnables import RunnableConfig\nfrom langgraph.graph import StateGraph\n\nbuilder = StateGraph(dict)\n\ndef my_node(state: dict, config: RunnableConfig):\n print(\"In node: \", config[\"configurable\"][\"user_id\"])\n return {\"results\": f\"Hello, {state['input']}!\"}\n\n# The second argument is optional\ndef my_other_node(state: dict):\n return state\n\nbuilder.add_node(\"my_node\", my_node)\nbuilder.add_node(\"other_node\", my_other_node)\n...\n\nBehind the scenes, functions are converted to RunnableLambda's, which add batch and async support to your function, along with native tracing and debugging.\nIf you add a node to graph without specifying a name, it will be given a default name equivalent to the function name.\nbuilder.add_node(my_node)\n# You can then create edges to/from this node by referencing it as `\"my_node\"`\n\nSTART Node\u00b6\nThe START Node is a special node that represents the node sends user input to the graph. The main purpose for referencing this node is to determine which nodes should be called first.\nfrom langgraph.graph import START\n\ngraph.add_edge(START, \"node_a\")\n\nEND Node\u00b6\nThe END Node is a special node that represents a terminal node. This node is referenced when you want to denote which edges have no actions after they are done.\nfrom langgraph.graph import END\n\ngraph.add_edge(\"node_a\", END)\n\nEdges\u00b6\nEdges define how the logic is routed and how the graph decides to stop. This is a big part of how your agents work and how different nodes communicate with each other. There are a few key types of edges:\n\nNormal Edges: Go directly from one node to the next.\nConditional Edges: Call a function to determine which node(s) to go to next.\nEntry Point: Which node to call first when user input arrives.\nConditional Entry Point: Call a function to determine which node(s) to call first when user input arrives.\n\nA node can have MULTIPLE outgoing edges. If a node has multiple out-going edges, all of those destination nodes will be executed in parallel as a part of the next superstep.\nNormal Edges\u00b6\nIf you always want to go from node A to node B, you can use the add_edge method directly.\ngraph.add_edge(\"node_a\", \"node_b\")\n\nConditional Edges\u00b6\nIf you want to optionally route to 1 or more edges (or optionally terminate), you can use the add_conditional_edges method. This method accepts the name of a node and a \"routing function\" to call after that node is executed:\ngraph.add_conditional_edges(\"node_a\", routing_function)\n\nSimilar to nodes, the routing_function accept the current state of the graph and return a value.\nBy default, the return value routing_function is used as the name of the node (or a list of nodes) to send the state to next. All those nodes will be run in parallel as a part of the next superstep.\nYou can optionally provide a dictionary that maps the routing_function's output to the name of the next node.\ngraph.add_conditional_edges(\"node_a\", routing_function, {True: \"node_b\", False: \"node_c\"})\n\nEntry Point\u00b6\nThe entry point is the first node(s) that are run when the graph starts. You can use the add_edge method from the virtual START node to the first node to execute to specify where to enter the graph.\nfrom langgraph.graph import START\n\ngraph.add_edge(START, \"node_a\")\n\nConditional Entry Point\u00b6\nA conditional entry point lets you start at different nodes depending on custom logic. You can use add_conditional_edges from the virtual START node to accomplish this.\nfrom langgraph.graph import START\n\ngraph.add_conditional_edges(START, routing_function)",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "For a full list of LangChain model providers with multimodal models, [check out this table](/v0.2/docs/integrations/chat/#advanced-features).\n\n### LLMs\u200b\n\ncautionPure text-in/text-out LLMs tend to be older or lower-level. Many new popular models are best used as [chat completion models](/v0.2/docs/concepts/#chat-models),\neven for non-chat use cases.\n\nYou are probably looking for [the section above instead](/v0.2/docs/concepts/#chat-models).\n\nLanguage models that takes a string as input and returns a string.\nThese are traditionally older models (newer models generally are [Chat Models](/v0.2/docs/concepts/#chat-models), see above).\n\nAlthough the underlying models are string in, string out, the LangChain wrappers also allow these models to take messages as input.\nThis gives them the same interface as [Chat Models](/v0.2/docs/concepts/#chat-models).\nWhen messages are passed in as input, they will be formatted into a string under the hood before being passed to the underlying model.\n\nLangChain does not host any LLMs, rather we rely on third party integrations.\n\nFor specifics on how to use LLMs, see the [how-to guides](/v0.2/docs/how_to/#llms).\n\n### Messages\u200b\n\nSome language models take a list of messages as input and return a message.\nThere are a few different types of messages.\nAll messages have a `role`, `content`, and `response_metadata` property.\n\nThe `role` describes WHO is saying the message. The standard roles are \"user\", \"assistant\", \"system\", and \"tool\".\nLangChain has different message classes for different roles.\n\nThe `content` property describes the content of the message.\nThis can be a few different things:\n\n- A string (most models deal with this type of content)\n\n- A List of dictionaries (this is used for multimodal input, where the dictionary contains information about that input type and that input location)\n\nOptionally, messages can have a `name` property which allows for differentiating between multiple speakers with the same role.\nFor example, if there are two users in the chat history it can be useful to differentiate between them. Not all models support this.\n\n#### HumanMessage\u200b\n\nThis represents a message with role \"user\".\n\n#### AIMessage\u200b\n\nThis represents a message with role \"assistant\". In addition to the `content` property, these messages also have:\n\n**response_metadata**\n\nThe `response_metadata` property contains additional metadata about the response. The data here is often specific to each model provider.\nThis is where information like log-probs and token usage may be stored.\n\n**tool_calls**\n\nThese represent a decision from an language model to call a tool. They are included as part of an `AIMessage` output.\nThey can be accessed from there with the `.tool_calls` property.\n\nThis property returns a list of `ToolCall`s. A `ToolCall` is a dictionary with the following arguments:\n\n- `name`: The name of the tool that should be called.\n\n- `args`: The arguments to that tool.\n\n- `id`: The id of that tool call.\n\n#### SystemMessage\u200b\n\nThis represents a message with role \"system\", which tells the model how to behave. Not every model provider supports this.\n\n#### ToolMessage\u200b\n\nThis represents a message with role \"tool\", which contains the result of calling a tool. In addition to `role` and `content`, this message has:\n\n- a `tool_call_id` field which conveys the id of the call to the tool that was called to produce this result.\n\n- an `artifact` field which can be used to pass along arbitrary artifacts of the tool execution which are useful to track but which should not be sent to the model.\n\n#### (Legacy) FunctionMessage\u200b\n\nThis is a legacy message type, corresponding to OpenAI's legacy function-calling API. `ToolMessage` should be used instead to correspond to the updated tool-calling API.\n\nThis represents the result of a function call. In addition to `role` and `content`, this message has a `name` parameter which conveys the name of the function that was called to produce this result.\n\n### Prompt templates\u200b",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "class State(TypedDict):\n foo: int\n bar: Annotated[list[str], add]\n\nIn this example, we've used the Annotated type to specify a reducer function (operator.add) for the second key (bar). Note that the first key remains unchanged. Let's assume the input to the graph is {\"foo\": 1, \"bar\": [\"hi\"]}. Let's then assume the first Node returns {\"foo\": 2}. This is treated as an update to the state. Notice that the Node does not need to return the whole State schema - just an update. After applying this update, the State would then be {\"foo\": 2, \"bar\": [\"hi\"]}. If the second node returns {\"bar\": [\"bye\"]} then the State would then be {\"foo\": 2, \"bar\": [\"hi\", \"bye\"]}. Notice here that the bar key is updated by adding the two lists together.\nWorking with Messages in Graph State\u00b6\nWhy use messages?\u00b6\nMost modern LLM providers have a chat model interface that accepts a list of messages as input. LangChain's ChatModel in particular accepts a list of Message objects as inputs. These messages come in a variety of forms such as HumanMessage (user input) or AIMessage (LLM response). To read more about what message objects are, please refer to this conceptual guide.\nUsing Messages in your Graph\u00b6\nIn many cases, it is helpful to store prior conversation history as a list of messages in your graph state. To do so, we can add a key (channel) to the graph state that stores a list of Message objects and annotate it with a reducer function (see messages key in the example below). The reducer function is vital to telling the graph how to update the list of Message objects in the state with each state update (for example, when a node sends an update). If you don't specify a reducer, every state update will overwrite the list of messages with the most recently provided value. If you wanted to simply append messages to the existing list, you could use operator.add as a reducer.\nHowever, you might also want to manually update messages in your graph state (e.g. human-in-the-loop). If you were to use operator.add, the manual state updates you send to the graph would be appended to the existing list of messages, instead of updating existing messages. To avoid that, you need a reducer that can keep track of message IDs and overwrite existing messages, if updated. To achieve this, you can use the prebuilt add_messages function. For brand new messages, it will simply append to existing list, but it will also handle the updates for existing messages correctly.\nSerialization\u00b6\nIn addition to keeping track of message IDs, the add_messages function will also try to deserialize messages into LangChain Message objects whenever a state update is received on the messages channel. See more information on LangChain serialization/deserialization here. This allows sending graph inputs / state updates in the following format:\n# this is supported\n{\"messages\": [HumanMessage(content=\"message\")]}\n\n# and this is also supported\n{\"messages\": [{\"type\": \"human\", \"content\": \"message\"}]}\n\nSince the state updates are always deserialized into LangChain Messages when using add_messages, you should use dot notation to access message attributes, like state[\"messages\"][-1].content. Below is an example of a graph that uses add_messages as it's reducer function.\nfrom langchain_core.messages import AnyMessage\nfrom langgraph.graph.message import add_messages\nfrom typing import Annotated\nfrom typing_extensions import TypedDict\n\nclass GraphState(TypedDict):\n messages: Annotated[list[AnyMessage], add_messages]\n\nMessagesState\u00b6\nSince having a list of messages in your state is so common, there exists a prebuilt state called MessagesState which makes it easy to use messages. MessagesState is defined with a single messages key which is a list of AnyMessage objects and uses the add_messages reducer. Typically, there is more state to track than just messages, so we see people subclass this state and add more fields, like:\nfrom langgraph.graph import MessagesState",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "When enabled, JSON mode will constrain the model's output to always be some sort of valid JSON.\nOften they require some custom prompting, but it's usually much less burdensome than completely raw prompting and\nmore along the lines of, `\"you must always return JSON\"`. The [output also generally easier to parse](/v0.2/docs/how_to/output_parser_json/).\n\nIt's also generally simpler to use directly and more commonly available than tool calling, and can give\nmore flexibility around prompting and shaping results than tool calling.\n\nHere's an example:\n\n```python\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_openai import ChatOpenAI\nfrom langchain.output_parsers.json import SimpleJsonOutputParser\n\nmodel = ChatOpenAI(\n model=\"gpt-4o\",\n model_kwargs={ \"response_format\": { \"type\": \"json_object\" } },\n)\n\nprompt = ChatPromptTemplate.from_template(\n \"Answer the user's question to the best of your ability.\"\n 'You must always output a JSON object with an \"answer\" key and a \"followup_question\" key.'\n \"{question}\"\n)\n\nchain = prompt | model | SimpleJsonOutputParser()\n\nchain.invoke({ \"question\": \"What is the powerhouse of the cell?\" })\n```\n\n**API Reference:**[ChatPromptTemplate](https://python.langchain.com/v0.2/api_reference/core/prompts/langchain_core.prompts.chat.ChatPromptTemplate.html) | [ChatOpenAI](https://python.langchain.com/v0.2/api_reference/openai/chat_models/langchain_openai.chat_models.base.ChatOpenAI.html) | [SimpleJsonOutputParser](https://python.langchain.com/v0.2/api_reference/core/output_parsers/langchain_core.output_parsers.json.SimpleJsonOutputParser.html)```text\n{'answer': 'The powerhouse of the cell is the mitochondrion. It is responsible for producing energy in the form of ATP through cellular respiration.',\n 'followup_question': 'Would you like to know more about how mitochondria produce energy?'}\n```\n\nFor a full list of model providers that support JSON mode, see [this table](/v0.2/docs/integrations/chat/#advanced-features).\n\n#### Tool calling\u200b\n\nFor models that support it, [tool calling](/v0.2/docs/concepts/#functiontool-calling) can be very convenient for structured output. It removes the\nguesswork around how best to prompt schemas in favor of a built-in model feature.\n\nIt works by first binding the desired schema either directly or via a [LangChain tool](/v0.2/docs/concepts/#tools) to a\n[chat model](/v0.2/docs/concepts/#chat-models) using the `.bind_tools()` method. The model will then generate an `AIMessage` containing\na `tool_calls` field containing `args` that match the desired shape.\n\nThere are several acceptable formats you can use to bind tools to a model in LangChain. Here's one example:\n\n```python\nfrom langchain_core.pydantic_v1 import BaseModel, Field\nfrom langchain_openai import ChatOpenAI\n\nclass ResponseFormatter(BaseModel):\n \"\"\"Always use this tool to structure your response to the user.\"\"\"\n\n answer: str = Field(description=\"The answer to the user's question\")\n followup_question: str = Field(description=\"A followup question the user could ask\")\n\nmodel = ChatOpenAI(\n model=\"gpt-4o\",\n temperature=0,\n)\n\nmodel_with_tools = model.bind_tools([ResponseFormatter])\n\nai_msg = model_with_tools.invoke(\"What is the powerhouse of the cell?\")\n\nai_msg.tool_calls[0][\"args\"]\n```\n\n**API Reference:**[ChatOpenAI](https://python.langchain.com/v0.2/api_reference/openai/chat_models/langchain_openai.chat_models.base.ChatOpenAI.html)```text\n{'answer': \"The powerhouse of the cell is the mitochondrion. It generates most of the cell's supply of adenosine triphosphate (ATP), which is used as a source of chemical energy.\",\n 'followup_question': 'How do mitochondria generate ATP?'}\n```\n\nTool calling is a generally consistent way to get a model to generate structured output, and is the default technique\nused for the [.with_structured_output()](/v0.2/docs/concepts/#with_structured_output) method when a model supports it.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "graph.add_conditional_edges(START, routing_function)\n\nYou can optionally provide a dictionary that maps the routing_function's output to the name of the next node.\ngraph.add_conditional_edges(START, routing_function, {True: \"node_b\", False: \"node_c\"})\n\nSend\u00b6\nBy default, Nodes and Edges are defined ahead of time and operate on the same shared state. However, there can be cases where the exact edges are not known ahead of time and/or you may want different versions of State to exist at the same time. A common of example of this is with map-reduce design patterns. In this design pattern, a first node may generate a list of objects, and you may want to apply some other node to all those objects. The number of objects may be unknown ahead of time (meaning the number of edges may not be known) and the input State to the downstream Node should be different (one for each generated object).\nTo support this design pattern, LangGraph supports returning Send objects from conditional edges. Send takes two arguments: first is the name of the node, and second is the state to pass to that node.\ndef continue_to_jokes(state: OverallState):\n return [Send(\"generate_joke\", {\"subject\": s}) for s in state['subjects']]\n\ngraph.add_conditional_edges(\"node_a\", continue_to_jokes)\n\nPersistence\u00b6\nLangGraph provides built-in persistence for your agent's state using checkpointers. Checkpointers save snapshots of the graph state at every superstep, allowing resumption at any time. This enables features like human-in-the-loop interactions, memory management, and fault-tolerance. You can even directly manipulate a graph's state after its execution using the \nappropriate get and update methods. For more details, see the persistence conceptual guide.\nThreads\u00b6\nThreads in LangGraph represent individual sessions or conversations between your graph and a user. When using checkpointing, turns in a single conversation (and even steps within a single graph execution) are organized by a unique thread ID.\nStorage\u00b6\nLangGraph provides built-in document storage through the BaseStore interface. Unlike checkpointers, which save state by thread ID, stores use custom namespaces for organizing data. This enables cross-thread persistence, allowing agents to maintain long-term memories, learn from past interactions, and accumulate knowledge over time. Common use cases include storing user profiles, building knowledge bases, and managing global preferences across all threads.\nGraph Migrations\u00b6\nLangGraph can easily handle migrations of graph definitions (nodes, edges, and state) even when using a checkpointer to track state.\n\nFor threads at the end of the graph (i.e. not interrupted) you can change the entire topology of the graph (i.e. all nodes and edges, remove, add, rename, etc)\nFor threads currently interrupted, we support all topology changes other than renaming / removing nodes (as that thread could now be about to enter a node that no longer exists) -- if this is a blocker please reach out and we can prioritize a solution.\nFor modifying state, we have full backwards and forwards compatibility for adding and removing keys\nState keys that are renamed lose their saved state in existing threads\nState keys whose types change in incompatible ways could currently cause issues in threads with state from before the change -- if this is a blocker please reach out and we can prioritize a solution.\n\nConfiguration\u00b6\nWhen creating a graph, you can also mark that certain parts of the graph are configurable. This is commonly done to enable easily switching between models or system prompts. This allows you to create a single \"cognitive architecture\" (the graph) but have multiple different instance of it.\nYou can optionally specify a config_schema when creating a graph.\nclass ConfigSchema(TypedDict):\n llm: str\n\ngraph = StateGraph(State, config_schema=ConfigSchema)",
|
||||
"metadata": {
|
||||
"source": "https://langchain-ai.github.io/langgraph/concepts/low_level/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pageContent": "Conceptual guide | \ud83e\udd9c\ufe0f\ud83d\udd17 LangChain\n\n[Skip to main content](#__docusaurus_skipToContent_fallback)A newer LangChain version is out! Check out the [latest version](https://python.langchain.com/docs/introduction).This is documentation for LangChain **v0.2**, which is no longer actively maintained.For the current stable version, see **this version** ( Latest ).# Conceptual guide\n\nThis section contains introductions to key parts of LangChain.\n\n## Architecture\u200b\n\nLangChain as a framework consists of a number of packages.\n\n### langchain-core\u200b\n\nThis package contains base abstractions of different components and ways to compose them together.\nThe interfaces for core components like LLMs, vector stores, retrievers and more are defined here.\nNo third party integrations are defined here.\nThe dependencies are kept purposefully very lightweight.\n\n### langchain\u200b\n\nThe main `langchain` package contains chains, agents, and retrieval strategies that make up an application's cognitive architecture.\nThese are NOT third party integrations.\nAll chains, agents, and retrieval strategies here are NOT specific to any one integration, but rather generic across all integrations.\n\n### langchain-community\u200b\n\nThis package contains third party integrations that are maintained by the LangChain community.\nKey partner packages are separated out (see below).\nThis contains all integrations for various components (LLMs, vector stores, retrievers).\nAll dependencies in this package are optional to keep the package as lightweight as possible.\n\n### Partner packages\u200b\n\nWhile the long tail of integrations is in `langchain-community`, we split popular integrations into their own packages (e.g. `langchain-openai`, `langchain-anthropic`, etc).\nThis was done in order to improve support for these important integrations.\n\n### langgraph\u200b\n\n`langgraph` is an extension of `langchain` aimed at\nbuilding robust and stateful multi-actor applications with LLMs by modeling steps as edges and nodes in a graph.\n\nLangGraph exposes high level interfaces for creating common types of agents, as well as a low-level API for composing custom flows.\n\n### langserve\u200b\n\nA package to deploy LangChain chains as REST APIs. Makes it easy to get a production ready API up and running.\n\n### LangSmith\u200b\n\nA developer platform that lets you debug, test, evaluate, and monitor LLM applications.\n\n## LangChain Expression Language (LCEL)\u200b\n\n`LangChain Expression Language`, or `LCEL`, is a declarative way to chain LangChain components.\nLCEL was designed from day 1 to **support putting prototypes in production, with no code changes**, from the simplest \u201cprompt + LLM\u201d chain to the most complex chains (we\u2019ve seen folks successfully run LCEL chains with 100s of steps in production). To highlight a few of the reasons you might want to use LCEL:\n\n- **First-class streaming support:**\nWhen you build your chains with LCEL you get the best possible time-to-first-token (time elapsed until the first chunk of output comes out). For some chains this means eg. we stream tokens straight from an LLM to a streaming output parser, and you get back parsed, incremental chunks of output at the same rate as the LLM provider outputs the raw tokens.\n\n- **Async support:**\nAny chain built with LCEL can be called both with the synchronous API (eg. in your Jupyter notebook while prototyping) as well as with the asynchronous API (eg. in a [LangServe](/v0.2/docs/langserve/) server). This enables using the same code for prototypes and in production, with great performance, and the ability to handle many concurrent requests in the same server.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "- **Fallback:** Corrective RAG ([paper](https://arxiv.org/pdf/2401.15884.pdf)). Fallback to web search if docs are not relevant to query\n\n- **Self-correction:** Self-RAG ([paper](https://arxiv.org/abs/2310.11511)). Fix answers w/ hallucinations or don\u2019t address question\n\n\n\n| Name | When to use | Description |\n| ---- | ---- | ---- |\n| Self-RAG | When needing to fix answers with hallucinations or irrelevant content. | Self-RAG performs checks for document relevance, hallucinations, and answer quality during the RAG answer generation flow, iteratively building an answer and self-correcting errors. |\n| Corrective-RAG | When needing a fallback mechanism for low relevance docs. | Corrective-RAG includes a fallback (e.g., to web search) if the retrieved documents are not relevant to the query, ensuring higher quality and more relevant retrieval. |\n\ntipSee several videos and cookbooks showcasing RAG with LangGraph: \n\n- [LangGraph Corrective RAG](https://www.youtube.com/watch?v=E2shqsYwxck)\n\n- [LangGraph combining Adaptive, Self-RAG, and Corrective RAG](https://www.youtube.com/watch?v=-ROS6gfYIts) \n\n- [Cookbooks for RAG using LangGraph](https://github.com/langchain-ai/langgraph/tree/main/examples/rag)\n\nSee our LangGraph RAG recipes with partners:\n\n- [Meta](https://github.com/meta-llama/llama-recipes/tree/main/recipes/3p_integrations/langchain)\n\n- [Mistral](https://github.com/mistralai/cookbook/tree/main/third_party/langchain)\n\n### Text splitting\u200b\n\nLangChain offers many different types of `text splitters`.\nThese all live in the `langchain-text-splitters` package.\n\nTable columns:\n\n- **Name**: Name of the text splitter\n\n- **Classes**: Classes that implement this text splitter\n\n- **Splits On**: How this text splitter splits text\n\n- **Adds Metadata**: Whether or not this text splitter adds metadata about where each chunk came from.\n\n- **Description**: Description of the splitter, including recommendation on when to use it.\n\n| Name | Classes | Splits On | Adds Metadata | Description |\n| ---- | ---- | ---- | ---- | ---- |\n| Recursive | RecursiveCharacterTextSplitter,RecursiveJsonSplitter | A list of user defined characters | | Recursively splits text. This splitting is trying to keep related pieces of text next to each other. This is therecommended wayto start splitting text. |\n| HTML | HTMLHeaderTextSplitter,HTMLSectionSplitter | HTML specific characters | \u2705 | Splits text based on HTML-specific characters. Notably, this adds in relevant information about where that chunk came from (based on the HTML) |\n| Markdown | MarkdownHeaderTextSplitter, | Markdown specific characters | \u2705 | Splits text based on Markdown-specific characters. Notably, this adds in relevant information about where that chunk came from (based on the Markdown) |\n| Code | many languages | Code (Python, JS) specific characters | | Splits text based on characters specific to coding languages. 15 different languages are available to choose from. |\n| Token | many classes | Tokens | | Splits text on tokens. There exist a few different ways to measure tokens. |\n| Character | CharacterTextSplitter | A user defined character | | Splits text based on a user defined character. One of the simpler methods. |\n| Semantic Chunker (Experimental) | SemanticChunker | Sentences | | First splits on sentences. Then combines ones next to each other if they are semantically similar enough. Taken fromGreg Kamradt |\n| Integration: AI21 Semantic | AI21SemanticTextSplitter | | \u2705 | Identifies distinct topics that form coherent pieces of text and splits along those. |\n\n### Evaluation\u200b\n\nEvaluation is the process of assessing the performance and effectiveness of your LLM-powered applications.\nIt involves testing the model's responses against a set of predefined criteria or benchmarks to ensure it meets the desired quality standards and fulfills the intended purpose.\nThis process is vital for building reliable applications.",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
},
|
||||
{
|
||||
"pageContent": "Assuming we are not adding our entire example dataset into each prompt, we need to have a way of selecting examples from our dataset based on a given input. We can do this:\n\n- Randomly\n\n- By (semantic or keyword-based) similarity of the inputs\n\n- Based on some other constraints, like token size\n\nLangChain has a number of [ExampleSelectors](/v0.2/docs/concepts/#example-selectors) which make it easy to use any of these techniques.\n\nGenerally, selecting by semantic similarity leads to the best model performance. But how important this is is again model and task specific, and is something worth experimenting with.\n\n#### 4. Formatting examples\u200b\n\nMost state-of-the-art models these days are chat models, so we'll focus on formatting examples for those. Our basic options are to insert the examples:\n\n- In the system prompt as a string\n\n- As their own messages\n\nIf we insert our examples into the system prompt as a string, we'll need to make sure it's clear to the model where each example begins and which parts are the input versus output. Different models respond better to different syntaxes, like [ChatML](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/chat-markup-language), XML, TypeScript, etc.\n\nIf we insert our examples as messages, where each example is represented as a sequence of Human, AI messages, we might want to also assign [names](/v0.2/docs/concepts/#messages) to our messages like `\"example_user\"` and `\"example_assistant\"` to make it clear that these messages correspond to different actors than the latest input message.\n\n**Formatting tool call examples**\n\nOne area where formatting examples as messages can be tricky is when our example outputs have tool calls. This is because different models have different constraints on what types of message sequences are allowed when any tool calls are generated.\n\n- Some models require that any AIMessage with tool calls be immediately followed by ToolMessages for every tool call,\n\n- Some models additionally require that any ToolMessages be immediately followed by an AIMessage before the next HumanMessage,\n\n- Some models require that tools are passed in to the model if there are any tool calls / ToolMessages in the chat history.\n\nThese requirements are model-specific and should be checked for the model you are using. If your model requires ToolMessages after tool calls and/or AIMessages after ToolMessages and your examples only include expected tool calls and not the actual tool outputs, you can try adding dummy ToolMessages / AIMessages to the end of each example with generic contents to satisfy the API constraints.\nIn these cases it's especially worth experimenting with inserting your examples as strings versus messages, as having dummy messages can adversely affect certain models.\n\nYou can see a case study of how Anthropic and OpenAI respond to different few-shot prompting techniques on two different tool calling benchmarks [here](https://blog.langchain.dev/few-shot-prompting-to-improve-tool-calling-performance/).\n\n### Retrieval\u200b\n\nLLMs are trained on a large but fixed dataset, limiting their ability to reason over private or recent information.\nFine-tuning an LLM with specific facts is one way to mitigate this, but is often [poorly suited for factual recall](https://www.anyscale.com/blog/fine-tuning-is-for-form-not-facts) and [can be costly](https://www.glean.com/blog/how-to-build-an-ai-assistant-for-the-enterprise).\n`Retrieval` is the process of providing relevant information to an LLM to improve its response for a given input.\n`Retrieval augmented generation` (`RAG`) [paper](https://arxiv.org/abs/2005.11401) is the process of grounding the LLM generation (output) using the retrieved information.\n\ntip- See our RAG from Scratch [code](https://github.com/langchain-ai/rag-from-scratch) and [video series](https://youtube.com/playlist?list=PLfaIDFEXuae2LXbO1_PKyVJiQ23ZztA0x&feature=shared).",
|
||||
"metadata": { "source": "https://python.langchain.com/v0.2/docs/concepts/" }
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Define the configurable parameters for the agent.
|
||||
*/
|
||||
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
|
||||
/**
|
||||
* typeof ConfigurationAnnotation.State class for indexing and retrieval operations.
|
||||
*
|
||||
* This annotation defines the parameters needed for configuring the indexing and
|
||||
* retrieval processes, including user identification, embedding model selection,
|
||||
* retriever provider choice, and search parameters.
|
||||
*/
|
||||
export const BaseConfigurationAnnotation = Annotation.Root({
|
||||
/**
|
||||
* Name of the embedding model to use. Must be a valid embedding model name.
|
||||
*/
|
||||
embeddingModel: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The vector store provider to use for retrieval.
|
||||
* Options are 'elastic', 'elastic-local', 'pinecone', or 'mongodb'.
|
||||
*/
|
||||
retrieverProvider: Annotation<
|
||||
"elastic" | "elastic-local" | "pinecone" | "mongodb"
|
||||
>,
|
||||
|
||||
/**
|
||||
* Additional keyword arguments to pass to the search function of the retriever.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
searchKwargs: Annotation<Record<string, any>>,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an typeof BaseConfigurationAnnotation.State instance from a RunnableConfig object.
|
||||
*
|
||||
* @param config - The configuration object to use.
|
||||
* @returns An instance of typeof BaseConfigurationAnnotation.State with the specified configuration.
|
||||
*/
|
||||
export function ensureBaseConfiguration(
|
||||
config: RunnableConfig,
|
||||
): typeof BaseConfigurationAnnotation.State {
|
||||
const configurable = (config?.configurable || {}) as Partial<
|
||||
typeof BaseConfigurationAnnotation.State
|
||||
>;
|
||||
return {
|
||||
embeddingModel:
|
||||
configurable.embeddingModel || "openai/text-embedding-3-small",
|
||||
retrieverProvider: configurable.retrieverProvider || "elastic-local",
|
||||
searchKwargs: configurable.searchKwargs || {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Client } from "@elastic/elasticsearch";
|
||||
import { ElasticVectorSearch } from "@langchain/community/vectorstores/elasticsearch";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { VectorStoreRetriever } from "@langchain/core/vectorstores";
|
||||
import { MongoDBAtlasVectorSearch } from "@langchain/mongodb";
|
||||
import { PineconeStore } from "@langchain/pinecone";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { ensureBaseConfiguration } from "./configuration.js";
|
||||
import { Pinecone as PineconeClient } from "@pinecone-database/pinecone";
|
||||
import { Embeddings } from "@langchain/core/embeddings";
|
||||
import { CohereEmbeddings } from "@langchain/cohere";
|
||||
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||
|
||||
async function makeElasticRetriever(
|
||||
configuration: ReturnType<typeof ensureBaseConfiguration>,
|
||||
embeddingModel: Embeddings,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
const elasticUrl = process.env.ELASTICSEARCH_URL;
|
||||
if (!elasticUrl) {
|
||||
throw new Error("ELASTICSEARCH_URL environment variable is not defined");
|
||||
}
|
||||
|
||||
let auth: { username: string; password: string } | { apiKey: string };
|
||||
if (configuration.retrieverProvider === "elastic-local") {
|
||||
const username = process.env.ELASTICSEARCH_USER;
|
||||
const password = process.env.ELASTICSEARCH_PASSWORD;
|
||||
if (!username || !password) {
|
||||
throw new Error(
|
||||
"ELASTICSEARCH_USER or ELASTICSEARCH_PASSWORD environment variable is not defined",
|
||||
);
|
||||
}
|
||||
auth = { username, password };
|
||||
} else {
|
||||
const apiKey = process.env.ELASTICSEARCH_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"ELASTICSEARCH_API_KEY environment variable is not defined",
|
||||
);
|
||||
}
|
||||
auth = { apiKey };
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
node: elasticUrl,
|
||||
auth,
|
||||
});
|
||||
|
||||
const vectorStore = new ElasticVectorSearch(embeddingModel, {
|
||||
client,
|
||||
indexName: "langchain_index",
|
||||
});
|
||||
return vectorStore.asRetriever({ filter: configuration.searchKwargs || {} });
|
||||
}
|
||||
|
||||
async function makePineconeRetriever(
|
||||
configuration: ReturnType<typeof ensureBaseConfiguration>,
|
||||
embeddingModel: Embeddings,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
const indexName = process.env.PINECONE_INDEX_NAME;
|
||||
if (!indexName) {
|
||||
throw new Error("PINECONE_INDEX_NAME environment variable is not defined");
|
||||
}
|
||||
const pinecone = new PineconeClient();
|
||||
const pineconeIndex = pinecone.Index(indexName!);
|
||||
const vectorStore = await PineconeStore.fromExistingIndex(embeddingModel, {
|
||||
pineconeIndex,
|
||||
});
|
||||
|
||||
return vectorStore.asRetriever({ filter: configuration.searchKwargs || {} });
|
||||
}
|
||||
|
||||
async function makeMongoDBRetriever(
|
||||
configuration: ReturnType<typeof ensureBaseConfiguration>,
|
||||
embeddingModel: Embeddings,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error("MONGODB_URI environment variable is not defined");
|
||||
}
|
||||
const client = new MongoClient(process.env.MONGODB_URI);
|
||||
const namespace = `langgraph_retrieval_agent.default`;
|
||||
const [dbName, collectionName] = namespace.split(".");
|
||||
const collection = client.db(dbName).collection(collectionName);
|
||||
const vectorStore = new MongoDBAtlasVectorSearch(embeddingModel, {
|
||||
collection: collection,
|
||||
textKey: "text",
|
||||
embeddingKey: "embedding",
|
||||
indexName: "vector_index",
|
||||
});
|
||||
return vectorStore.asRetriever({ filter: configuration.searchKwargs || {} });
|
||||
}
|
||||
|
||||
function makeTextEmbeddings(modelName: string): Embeddings {
|
||||
/**
|
||||
* Connect to the configured text encoder.
|
||||
*/
|
||||
const index = modelName.indexOf("/");
|
||||
let provider, model;
|
||||
if (index === -1) {
|
||||
model = modelName;
|
||||
provider = "openai"; // Assume openai if no provider included
|
||||
} else {
|
||||
provider = modelName.slice(0, index);
|
||||
model = modelName.slice(index + 1);
|
||||
}
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
return new OpenAIEmbeddings({ model });
|
||||
case "cohere":
|
||||
return new CohereEmbeddings({ model });
|
||||
default:
|
||||
throw new Error(`Unsupported embedding provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeRetriever(
|
||||
config: RunnableConfig,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
const configuration = ensureBaseConfiguration(config);
|
||||
const embeddingModel = makeTextEmbeddings(configuration.embeddingModel);
|
||||
|
||||
switch (configuration.retrieverProvider) {
|
||||
case "elastic":
|
||||
case "elastic-local":
|
||||
return makeElasticRetriever(configuration, embeddingModel);
|
||||
case "pinecone":
|
||||
return makePineconeRetriever(configuration, embeddingModel);
|
||||
case "mongodb":
|
||||
return makeMongoDBRetriever(configuration, embeddingModel);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unrecognized retrieverProvider in configuration: ${configuration.retrieverProvider}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Reduces the document array based on the provided new documents or actions.
|
||||
*
|
||||
* @param existing - The existing array of documents.
|
||||
* @param newDocs - The new documents or actions to apply.
|
||||
* @returns The updated array of documents.
|
||||
*/
|
||||
export function reduceDocs(
|
||||
existing?: Document[],
|
||||
newDocs?:
|
||||
| Document[]
|
||||
| { [key: string]: any }[]
|
||||
| string[]
|
||||
| string
|
||||
| "delete",
|
||||
): Document[] {
|
||||
if (newDocs === "delete") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingList = existing || [];
|
||||
const existingIds = new Set(existingList.map((doc) => doc.metadata?.uuid));
|
||||
|
||||
if (typeof newDocs === "string") {
|
||||
const docId = uuidv4();
|
||||
return [
|
||||
...existingList,
|
||||
{ pageContent: newDocs, metadata: { uuid: docId } },
|
||||
];
|
||||
}
|
||||
|
||||
const newList: Document[] = [];
|
||||
if (Array.isArray(newDocs)) {
|
||||
for (const item of newDocs) {
|
||||
if (typeof item === "string") {
|
||||
const itemId = uuidv4();
|
||||
newList.push({ pageContent: item, metadata: { uuid: itemId } });
|
||||
existingIds.add(itemId);
|
||||
} else if (typeof item === "object") {
|
||||
const metadata = (item as Document).metadata ?? {};
|
||||
let itemId = metadata.uuid ?? uuidv4();
|
||||
|
||||
if (!existingIds.has(itemId)) {
|
||||
if ("pageContent" in item) {
|
||||
// It's a Document-like object
|
||||
newList.push({
|
||||
...(item as Document),
|
||||
metadata: { ...metadata, uuid: itemId },
|
||||
});
|
||||
} else {
|
||||
// It's a generic object, treat it as metadata
|
||||
newList.push({
|
||||
pageContent: "",
|
||||
metadata: { ...(item as { [key: string]: any }), uuid: itemId },
|
||||
});
|
||||
}
|
||||
existingIds.add(itemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...existingList, ...newList];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Document } from "langchain/document";
|
||||
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
export function formatDoc(doc: Document): string {
|
||||
const metadata = doc.metadata || {};
|
||||
const meta = Object.entries(metadata)
|
||||
.map(([k, v]) => ` ${k}=${v}`)
|
||||
.join("");
|
||||
const metaStr = meta ? ` ${meta}` : "";
|
||||
|
||||
return `<document${metaStr}>\n${doc.pageContent}\n</document>`;
|
||||
}
|
||||
|
||||
export function formatDocs(docs?: Document[]): string {
|
||||
/**Format a list of documents as XML. */
|
||||
if (!docs || docs.length === 0) {
|
||||
return "<documents></documents>";
|
||||
}
|
||||
const formatted = docs.map(formatDoc).join("\n");
|
||||
return `<documents>\n${formatted}\n</documents>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a chat model from a fully specified name.
|
||||
* @param fullySpecifiedName - String in the format 'provider/model' or 'provider/account/provider/model'.
|
||||
* @returns A Promise that resolves to a BaseChatModel instance.
|
||||
*/
|
||||
export async function loadChatModel(
|
||||
fullySpecifiedName: string,
|
||||
): Promise<BaseChatModel> {
|
||||
const index = fullySpecifiedName.indexOf("/");
|
||||
if (index === -1) {
|
||||
// If there's no "/", assume it's just the model
|
||||
return await initChatModel(fullySpecifiedName);
|
||||
} else {
|
||||
const provider = fullySpecifiedName.slice(0, index);
|
||||
const model = fullySpecifiedName.slice(index + 1);
|
||||
return await initChatModel(model, { modelProvider: provider });
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 654 KiB |
@@ -0,0 +1,700 @@
|
||||
# LangGraph Retrieval Agent Template
|
||||
|
||||
This is a starter project to help you get started with developing a retrieval agent using [LangGraph.js](https://github.com/langchain-ai/langgraphjs) in [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio).
|
||||
|
||||

|
||||
|
||||
It contains example graphs exported from `src/retrieval_agent/graph.ts` that implement a retrieval-based question answering system.
|
||||
|
||||
## What it does
|
||||
|
||||
This project has two graphs: an "index" graph, and a "retrieval" graph.
|
||||
|
||||
The index graph takes in document objects and strings, and it indexes them for the configured `userId`.
|
||||
|
||||
```json
|
||||
[{ "page_content": "I have 1 cat." }]
|
||||
```
|
||||
|
||||
The retrieval chat bot manages a chat history and responds based on fetched context. It:
|
||||
|
||||
1. Takes a user **query** as input
|
||||
2. Searches for documents in filtered by userId based on the conversation history
|
||||
3. Responds using the retrieved information and conversation context
|
||||
|
||||
By default, it's set up to answer questions based on the user's indexed documents, which are filtered by the user's ID for personalized responses.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Assuming you have already [installed LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download), to set up:
|
||||
|
||||
1. Create a `.env` file.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. Select your retriever & index, and save the access instructions to your `.env` file.
|
||||
|
||||
<!--
|
||||
Setup instruction auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
-->
|
||||
|
||||
### Setup Retriever
|
||||
|
||||
The defaults values for `retrieverProvider` are shown below:
|
||||
|
||||
```yaml
|
||||
retrieverProvider: elastic
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Setup Elasticsearch
|
||||
|
||||
**Elastic Cloud**
|
||||
|
||||
1. Signup for a free trial with [Elastic Cloud](https://cloud.elastic.co/registration?onboarding_token=search&cta=cloud-registration&tech=trial&plcmt=article%20content&pg=langchain).
|
||||
2. Get the Elasticsearch URL, found under Applications of your deployment.
|
||||
3. Create an API key. See the [official elastic documentation](https://www.elastic.co/search-labs/tutorials/install-elasticsearch/elastic-cloud#creating-an-api-key) for more information.
|
||||
4. Copy the URL and API key to your `.env` file created above:
|
||||
|
||||
```
|
||||
ELASTICSEARCH_URL=<ES_URL>
|
||||
ELASTICSEARCH_API_KEY=<API_KEY>
|
||||
```
|
||||
|
||||
**Local Elasticsearch (Docker)**
|
||||
|
||||
```
|
||||
docker run -p 127.0.0.1:9200:9200 -d --name elasticsearch --network elastic-net -e ELASTIC_PASSWORD=changeme -e "discovery.type=single-node" -e "xpack.security.http.ssl.enabled=false" -e "xpack.license.self_generated.type=trial" docker.elastic.co/elasticsearch/elasticsearch:8.15.1
|
||||
```
|
||||
|
||||
See the [official Elastic documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/run-elasticsearch-locally.html) for more information on running it locally.
|
||||
|
||||
Then populate the following in your `.env` file:
|
||||
|
||||
```
|
||||
# As both Elasticsearch and LangGraph Studio runs in Docker, we need to use host.docker.internal to access.
|
||||
|
||||
ELASTICSEARCH_URL=http://host.docker.internal:9200
|
||||
ELASTICSEARCH_USER=elastic
|
||||
ELASTICSEARCH_PASSWORD=changeme
|
||||
```
|
||||
|
||||
#### MongoDB Atlas
|
||||
|
||||
MongoDB Atlas is a fully-managed cloud database that includes vector search capabilities for AI-powered applications.
|
||||
|
||||
1. Create a free Atlas cluster:
|
||||
|
||||
- Go to the [MongoDB Atlas website](https://www.mongodb.com/cloud/atlas/register) and sign up for a free account.
|
||||
- After logging in, create a free cluster by following the on-screen instructions.
|
||||
|
||||
2. Create a vector search index
|
||||
|
||||
- Follow the instructions at [the Mongo docs](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/)
|
||||
- By default, we use the collection `langgraph_retrieval_agent.default` - create the index there
|
||||
- Add an indexed filter for path `user_id`
|
||||
- **IMPORTANT**: select Atlas Vector Search NOT Atlas Search when creating the index
|
||||
Your final JSON editor configuration should look something like the following:
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"numDimensions": 1536,
|
||||
"path": "embedding",
|
||||
"similarity": "cosine",
|
||||
"type": "vector"
|
||||
},
|
||||
{
|
||||
"path": "user_id",
|
||||
"type": "filter"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The exact numDimensions may differ if you select a different embedding model.
|
||||
|
||||
2. Set up your environment:
|
||||
|
||||
- In the Atlas dashboard, click on "Connect" for your cluster.
|
||||
- Choose "Connect your application" and copy the provided connection string.
|
||||
- Create a `.env` file in your project root if you haven't already.
|
||||
- Add your MongoDB Atlas connection string to the `.env` file:
|
||||
|
||||
```
|
||||
MONGODB_URI="mongodb+srv://username:password@your-cluster-url.mongodb.net/?retryWrites=true&w=majority&appName=your-cluster-name"
|
||||
```
|
||||
|
||||
Replace `username`, `password`, `your-cluster-url`, and `your-cluster-name` with your actual credentials and cluster information.
|
||||
|
||||
#### Pinecone Serverless
|
||||
|
||||
Pinecone is a managed, cloud-native vector database that provides long-term memory for high-performance AI applications.
|
||||
|
||||
1. Sign up for a Pinecone account at [https://login.pinecone.io/login](https://login.pinecone.io/login) if you haven't already.
|
||||
|
||||
2. After logging in, generate an API key from the Pinecone console.
|
||||
|
||||
3. Create a serverless index:
|
||||
|
||||
- Choose a name for your index (e.g., "example-index")
|
||||
- Set the dimension based on your embedding model (e.g., 1536 for OpenAI embeddings)
|
||||
- Select "cosine" as the metric
|
||||
- Choose "Serverless" as the index type
|
||||
- Select your preferred cloud provider and region (e.g., AWS us-east-1)
|
||||
|
||||
4. Once you have created your index and obtained your API key, add them to your `.env` file:
|
||||
|
||||
```
|
||||
PINECONE_API_KEY=your-api-key
|
||||
PINECONE_INDEX_NAME=your-index-name
|
||||
```
|
||||
|
||||
### Setup Model
|
||||
|
||||
The defaults values for `responseModel`, `queryModel` are shown below:
|
||||
|
||||
```yaml
|
||||
responseModel: anthropic/claude-3-5-sonnet-20240620
|
||||
queryModel: anthropic/claude-3-haiku-20240307
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### Anthropic
|
||||
|
||||
To use Anthropic's chat models:
|
||||
|
||||
1. Sign up for an [Anthropic API key](https://console.anthropic.com/) if you haven't already.
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
ANTHROPIC_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's chat models:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### Setup Embedding Model
|
||||
|
||||
The defaults values for `embeddingModel` are shown below:
|
||||
|
||||
```yaml
|
||||
embeddingModel: openai/text-embedding-3-small
|
||||
```
|
||||
|
||||
Follow the instructions below to get set up, or pick one of the additional options.
|
||||
|
||||
#### OpenAI
|
||||
|
||||
To use OpenAI's embeddings:
|
||||
|
||||
1. Sign up for an [OpenAI API key](https://platform.openai.com/signup).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
#### Cohere
|
||||
|
||||
To use Cohere's embeddings:
|
||||
|
||||
1. Sign up for a [Cohere API key](https://dashboard.cohere.com/welcome/register).
|
||||
2. Once you have your API key, add it to your `.env` file:
|
||||
|
||||
```bash
|
||||
COHERE_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
<!--
|
||||
End setup instructions
|
||||
-->
|
||||
|
||||
## Using
|
||||
|
||||
Once you've set up your retriever saved your model secrets, it's time to try it out! First, let's add some information to the index. Open studio, select the "indexer" graph from the dropdown in the top-left, provide an example user ID in the configuration at the bottom, and then add some content to chat over.
|
||||
|
||||
```json
|
||||
[{ "page_content": "My cat knows python." }]
|
||||
```
|
||||
|
||||
When you upload content, it will be indexed under the configured user ID. You know it's complete when the indexer "delete"'s the content from its graph memory (since it's been persisted in your configured storage provider).
|
||||
|
||||
Next, open the "retrieval_graph" using the dropdown in the top-left. Ask it about your cat to confirm it can fetch the required information! If you change the `userId` at any time, notice how it no longer has access to your information. The graph is doing simple filtering of content so you only access the information under the provided ID.
|
||||
|
||||
## How to customize
|
||||
|
||||
You can customize this retrieval agent template in several ways:
|
||||
|
||||
1. **Change the retriever**: You can switch between different vector stores (Elasticsearch, MongoDB, Pinecone) by modifying the `retrieverProvider` in the configuration. Each provider has its own setup instructions in the "Getting Started" section above.
|
||||
|
||||
2. **Modify the embedding model**: You can change the embedding model used for document indexing and query embedding by updating the `embeddingModel` in the configuration. Options include various OpenAI and Cohere models.
|
||||
|
||||
3. **Adjust search parameters**: Fine-tune the retrieval process by modifying the `searchKwargs` in the configuration. This allows you to control aspects like the number of documents retrieved or similarity thresholds.
|
||||
|
||||
4. **Customize the response generation**: You can modify the `responseSystemPrompt` to change how the agent formulates its responses. This allows you to adjust the agent's personality or add specific instructions for answer generation.
|
||||
|
||||
5. **Change the language model**: Update the `responseModel` in the configuration to use different language models for response generation. Options include various Claude models from Anthropic, as well as models from other providers like Fireworks AI.
|
||||
|
||||
6. **Extend the graph**: You can add new nodes or modify existing ones in the `src/retrieval_agent/graph.ts` file to introduce additional processing steps or decision points in the agent's workflow.
|
||||
|
||||
7. **Add new tools**: Implement new tools or API integrations in `src/retrieval_agent/tools.ts` to expand the agent's capabilities beyond simple retrieval and response generation.
|
||||
|
||||
8. **Modify prompts**: Update the prompts used for query generation and response formulation in `src/retrieval_agent/prompts.ts` to better suit your specific use case or to improve the agent's performance.
|
||||
|
||||
Remember to test your changes thoroughly to ensure they improve the agent's performance for your specific use case.
|
||||
|
||||
## Development
|
||||
|
||||
While iterating on your graph, you can edit past state and rerun your app from past states to debug specific nodes. Local changes will be automatically applied via hot reload. Try adding an interrupt before the agent calls tools, updating the default system message in `src/retrieval_agent/utils.ts` to take on a persona, or adding additional nodes and edges!
|
||||
|
||||
Follow up requests will be appended to the same thread. You can create an entirely new thread, clearing previous history, using the `+` button in the top right.
|
||||
|
||||
You can find the latest (under construction) docs on [LangGraph](https://github.com/langchain-ai/langgraphjs) here, including examples and other references. Using those guides can help you pick the right patterns to adapt here for your use case.
|
||||
|
||||
LangGraph Studio also integrates with [LangSmith](https://smith.langchain.com/) for more in-depth tracing and collaboration with teammates.
|
||||
|
||||
<!--
|
||||
Configuration auto-generated by `langgraph template lock`. DO NOT EDIT MANUALLY.
|
||||
{
|
||||
"config_schemas": {
|
||||
"indexer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"embeddingModel": {
|
||||
"type": "string",
|
||||
"default": "openai/text-embedding-3-small",
|
||||
"description": "Name of the embedding model to use. Must be a valid embedding model name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "cohere/embed-english-light-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-large",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-small",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-ada-002",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retrieverProvider": {
|
||||
"enum": [
|
||||
"elastic",
|
||||
"elastic-local",
|
||||
"mongodb",
|
||||
"pinecone"
|
||||
],
|
||||
"default": "elastic",
|
||||
"description": "The vector store provider to use for retrieval. Options are 'elastic', 'pinecone', or 'mongodb'.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "elastic",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_API_KEY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "elastic-local",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_USER",
|
||||
"ELASTICSEARCH_PASSWORD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "mongodb",
|
||||
"variables": [
|
||||
"MONGODB_URI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "pinecone",
|
||||
"variables": [
|
||||
"PINECONE_API_KEY",
|
||||
"PINECONE_INDEX_NAME"
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"retrieval_graph": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"embeddingModel": {
|
||||
"type": "string",
|
||||
"default": "openai/text-embedding-3-small",
|
||||
"description": "Name of the embedding model to use. Must be a valid embedding model name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "cohere/embed-english-light-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-english-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-light-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v2.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "cohere/embed-multilingual-v3.0",
|
||||
"variables": "COHERE_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-large",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-3-small",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/text-embedding-ada-002",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"retrieverProvider": {
|
||||
"enum": [
|
||||
"elastic",
|
||||
"elastic-local",
|
||||
"mongodb",
|
||||
"pinecone"
|
||||
],
|
||||
"default": "elastic",
|
||||
"description": "The vector store provider to use for retrieval. Options are 'elastic', 'pinecone', or 'mongodb'.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "elastic",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_API_KEY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "elastic-local",
|
||||
"variables": [
|
||||
"ELASTICSEARCH_URL",
|
||||
"ELASTICSEARCH_USER",
|
||||
"ELASTICSEARCH_PASSWORD"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "mongodb",
|
||||
"variables": [
|
||||
"MONGODB_URI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"value": "pinecone",
|
||||
"variables": [
|
||||
"PINECONE_API_KEY",
|
||||
"PINECONE_INDEX_NAME"
|
||||
]
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"responseModel": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"description": "The language model used for generating responses. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"queryModel": {
|
||||
"type": "string",
|
||||
"default": "anthropic/claude-3-haiku-20240307",
|
||||
"description": "The language model used for processing and refining queries. Should be in the form: provider/model-name.",
|
||||
"environment": [
|
||||
{
|
||||
"value": "anthropic/claude-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.0",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-2.1",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-5-sonnet-20240620",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-haiku-20240307",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-opus-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-3-sonnet-20240229",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "anthropic/claude-instant-1.2",
|
||||
"variables": "ANTHROPIC_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0125",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0301",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-1106",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-3.5-turbo-16k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0125-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-1106-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0314",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-32k-0613",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-turbo-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4-vision-preview",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
},
|
||||
{
|
||||
"value": "openai/gpt-4o-mini",
|
||||
"variables": "OPENAI_API_KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-->
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Define the configurable parameters for the agent.
|
||||
*/
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import {
|
||||
RESPONSE_SYSTEM_PROMPT_TEMPLATE,
|
||||
QUERY_SYSTEM_PROMPT_TEMPLATE,
|
||||
} from "./prompts.js";
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
|
||||
/**
|
||||
* typeof ConfigurationAnnotation.State class for indexing and retrieval operations.
|
||||
*
|
||||
* This annotation defines the parameters needed for configuring the indexing and
|
||||
* retrieval processes, including user identification, embedding model selection,
|
||||
* retriever provider choice, and search parameters.
|
||||
*/
|
||||
export const IndexConfigurationAnnotation = Annotation.Root({
|
||||
/**
|
||||
* Unique identifier for the user.
|
||||
*/
|
||||
userId: Annotation<string>,
|
||||
|
||||
/**
|
||||
* Name of the embedding model to use. Must be a valid embedding model name.
|
||||
*/
|
||||
embeddingModel: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The vector store provider to use for retrieval.
|
||||
* Options are 'elastic', 'elastic-local', 'pinecone', or 'mongodb'.
|
||||
*/
|
||||
retrieverProvider: Annotation<
|
||||
"elastic" | "elastic-local" | "pinecone" | "mongodb"
|
||||
>,
|
||||
|
||||
/**
|
||||
* Additional keyword arguments to pass to the search function of the retriever.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
searchKwargs: Annotation<Record<string, any>>,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an typeof IndexConfigurationAnnotation.State instance from a RunnableConfig object.
|
||||
*
|
||||
* @param config - The configuration object to use.
|
||||
* @returns An instance of typeof IndexConfigurationAnnotation.State with the specified configuration.
|
||||
*/
|
||||
export function ensureIndexConfiguration(
|
||||
config: RunnableConfig | undefined = undefined,
|
||||
): typeof IndexConfigurationAnnotation.State {
|
||||
const configurable = (config?.configurable || {}) as Partial<
|
||||
typeof IndexConfigurationAnnotation.State
|
||||
>;
|
||||
return {
|
||||
userId: configurable.userId || "default", // Give a default user for shared docs
|
||||
embeddingModel:
|
||||
configurable.embeddingModel || "openai/text-embedding-3-small",
|
||||
retrieverProvider: configurable.retrieverProvider || "elastic",
|
||||
searchKwargs: configurable.searchKwargs || {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete configuration for the agent.
|
||||
*/
|
||||
export const ConfigurationAnnotation = Annotation.Root({
|
||||
...IndexConfigurationAnnotation.spec,
|
||||
/**
|
||||
* The system prompt used for generating responses.
|
||||
*/
|
||||
responseSystemPromptTemplate: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The language model used for generating responses. Should be in the form: provider/model-name.
|
||||
*/
|
||||
responseModel: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The system prompt used for processing and refining queries.
|
||||
*/
|
||||
querySystemPromptTemplate: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The language model used for processing and refining queries. Should be in the form: provider/model-name.
|
||||
*/
|
||||
queryModel: Annotation<string>,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a typeof ConfigurationAnnotation.State instance from a RunnableConfig object.
|
||||
*
|
||||
* @param config - The configuration object to use.
|
||||
* @returns An instance of typeof ConfigurationAnnotation.State with the specified configuration.
|
||||
*/
|
||||
export function ensureConfiguration(
|
||||
config: RunnableConfig | undefined = undefined,
|
||||
): typeof ConfigurationAnnotation.State {
|
||||
const indexConfig = ensureIndexConfiguration(config);
|
||||
const configurable = (config?.configurable || {}) as Partial<
|
||||
typeof ConfigurationAnnotation.State
|
||||
>;
|
||||
|
||||
return {
|
||||
...indexConfig,
|
||||
responseSystemPromptTemplate:
|
||||
configurable.responseSystemPromptTemplate ||
|
||||
RESPONSE_SYSTEM_PROMPT_TEMPLATE,
|
||||
responseModel:
|
||||
configurable.responseModel || "anthropic/claude-3-5-sonnet-20240620",
|
||||
querySystemPromptTemplate:
|
||||
configurable.querySystemPromptTemplate || QUERY_SYSTEM_PROMPT_TEMPLATE,
|
||||
queryModel: configurable.queryModel || "anthropic/claude-3-haiku-20240307",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { StateGraph } from "@langchain/langgraph";
|
||||
import {
|
||||
ConfigurationAnnotation,
|
||||
ensureConfiguration,
|
||||
} from "./configuration.js";
|
||||
import { StateAnnotation, InputStateAnnotation } from "./state.js";
|
||||
import { formatDocs, getMessageText, loadChatModel } from "./utils.js";
|
||||
import { z } from "zod";
|
||||
import { makeRetriever } from "./retrieval.js";
|
||||
// Define the function that calls the model
|
||||
|
||||
const SearchQuery = z.object({
|
||||
query: z.string().describe("Search the indexed documents for a query."),
|
||||
});
|
||||
|
||||
async function generateQuery(
|
||||
state: typeof StateAnnotation.State,
|
||||
config?: RunnableConfig,
|
||||
): Promise<typeof StateAnnotation.Update> {
|
||||
const messages = state.messages;
|
||||
if (messages.length === 1) {
|
||||
// It's the first user question. We will use the input directly to search.
|
||||
const humanInput = getMessageText(messages[messages.length - 1]);
|
||||
return { queries: [humanInput] };
|
||||
} else {
|
||||
const configuration = ensureConfiguration(config);
|
||||
// Feel free to customize the prompt, model, and other logic!
|
||||
const systemMessage = configuration.querySystemPromptTemplate
|
||||
.replace("{queries}", (state.queries || []).join("\n- "))
|
||||
.replace("{systemTime}", new Date().toISOString());
|
||||
|
||||
const messageValue = [
|
||||
{ role: "system", content: systemMessage },
|
||||
...state.messages,
|
||||
];
|
||||
const model = (
|
||||
await loadChatModel(configuration.responseModel)
|
||||
).withStructuredOutput(SearchQuery);
|
||||
|
||||
const generated = await model.invoke(messageValue);
|
||||
return {
|
||||
queries: [generated.query],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function retrieve(
|
||||
state: typeof StateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof StateAnnotation.Update> {
|
||||
const query = state.queries[state.queries.length - 1];
|
||||
const retriever = await makeRetriever(config);
|
||||
const response = await retriever.invoke(query);
|
||||
return { retrievedDocs: response };
|
||||
}
|
||||
|
||||
async function respond(
|
||||
state: typeof StateAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof StateAnnotation.Update> {
|
||||
/**
|
||||
* Call the LLM powering our "agent".
|
||||
*/
|
||||
const configuration = ensureConfiguration(config);
|
||||
|
||||
const model = await loadChatModel(configuration.responseModel);
|
||||
|
||||
const retrievedDocs = formatDocs(state.retrievedDocs);
|
||||
// Feel free to customize the prompt, model, and other logic!
|
||||
const systemMessage = configuration.responseSystemPromptTemplate
|
||||
.replace("{retrievedDocs}", retrievedDocs)
|
||||
.replace("{systemTime}", new Date().toISOString());
|
||||
const messageValue = [
|
||||
{ role: "system", content: systemMessage },
|
||||
...state.messages,
|
||||
];
|
||||
const response = await model.invoke(messageValue);
|
||||
// We return a list, because this will get added to the existing list
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
// Lay out the nodes and edges to define a graph
|
||||
const builder = new StateGraph(
|
||||
{
|
||||
stateSchema: StateAnnotation,
|
||||
// The only input field is the user
|
||||
input: InputStateAnnotation,
|
||||
},
|
||||
ConfigurationAnnotation,
|
||||
)
|
||||
.addNode("generateQuery", generateQuery)
|
||||
.addNode("retrieve", retrieve)
|
||||
.addNode("respond", respond)
|
||||
.addEdge("__start__", "generateQuery")
|
||||
.addEdge("generateQuery", "retrieve")
|
||||
.addEdge("retrieve", "respond");
|
||||
|
||||
// Finally, we compile it!
|
||||
// This compiles it into a graph you can invoke and deploy.
|
||||
export const graph = builder.compile({
|
||||
interruptBefore: [], // if you want to update the state before calling the tools
|
||||
interruptAfter: [],
|
||||
});
|
||||
|
||||
graph.name = "Retrieval Graph"; // Customizes the name displayed in LangSmith
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* This "graph" simply exposes an endpoint for a user to upload docs to be indexed.
|
||||
*/
|
||||
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { StateGraph } from "@langchain/langgraph";
|
||||
|
||||
import { IndexStateAnnotation } from "./state.js";
|
||||
import { makeRetriever } from "./retrieval.js";
|
||||
import {
|
||||
ensureIndexConfiguration,
|
||||
IndexConfigurationAnnotation,
|
||||
} from "./configuration.js";
|
||||
|
||||
function ensureDocsHaveUserId(
|
||||
docs: Document[],
|
||||
config: RunnableConfig,
|
||||
): Document[] {
|
||||
const configuration = ensureIndexConfiguration(config);
|
||||
const userId = configuration.userId;
|
||||
return docs.map((doc) => {
|
||||
return new Document({
|
||||
pageContent: doc.pageContent,
|
||||
metadata: { ...doc.metadata, user_id: userId },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function indexDocs(
|
||||
state: typeof IndexStateAnnotation.State,
|
||||
config?: RunnableConfig,
|
||||
): Promise<typeof IndexStateAnnotation.Update> {
|
||||
if (!config) {
|
||||
throw new Error("ConfigurationAnnotation required to run index_docs.");
|
||||
}
|
||||
const docs = state.docs;
|
||||
const retriever = await makeRetriever(config);
|
||||
const stampedDocs = ensureDocsHaveUserId(docs, config);
|
||||
|
||||
await retriever.addDocuments(stampedDocs);
|
||||
return { docs: "delete" };
|
||||
}
|
||||
|
||||
// Define a new graph
|
||||
|
||||
const builder = new StateGraph(
|
||||
IndexStateAnnotation,
|
||||
IndexConfigurationAnnotation,
|
||||
)
|
||||
.addNode("indexDocs", indexDocs)
|
||||
.addEdge("__start__", "indexDocs");
|
||||
|
||||
// Finally, we compile it!
|
||||
// This compiles it into a graph you can invoke and deploy.
|
||||
export const graph = builder.compile();
|
||||
|
||||
graph.name = "Index Graph"; // Customizes the name displayed in LangSmith
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Default prompts.
|
||||
*/
|
||||
|
||||
export const RESPONSE_SYSTEM_PROMPT_TEMPLATE = `You are a helpful AI assistant. Answer the user's questions based on the retrieved documents.
|
||||
|
||||
{retrievedDocs}
|
||||
|
||||
System time: {systemTime}`;
|
||||
|
||||
export const QUERY_SYSTEM_PROMPT_TEMPLATE = `Generate search queries to retrieve documents that may help answer the user's question. Previously, you made the following queries:
|
||||
|
||||
<previous_queries/>
|
||||
{queries}
|
||||
</previous_queries>
|
||||
|
||||
System time: {systemTime}`;
|
||||
@@ -0,0 +1,156 @@
|
||||
import { Client } from "@elastic/elasticsearch";
|
||||
import { ElasticVectorSearch } from "@langchain/community/vectorstores/elasticsearch";
|
||||
import { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { VectorStoreRetriever } from "@langchain/core/vectorstores";
|
||||
import { MongoDBAtlasVectorSearch } from "@langchain/mongodb";
|
||||
import { PineconeStore } from "@langchain/pinecone";
|
||||
import { MongoClient } from "mongodb";
|
||||
import { ensureConfiguration } from "./configuration.js";
|
||||
import { Pinecone as PineconeClient } from "@pinecone-database/pinecone";
|
||||
import { Embeddings } from "@langchain/core/embeddings";
|
||||
import { CohereEmbeddings } from "@langchain/cohere";
|
||||
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||
|
||||
async function makeElasticRetriever(
|
||||
configuration: ReturnType<typeof ensureConfiguration>,
|
||||
embeddingModel: Embeddings,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
const elasticUrl = process.env.ELASTICSEARCH_URL;
|
||||
if (!elasticUrl) {
|
||||
throw new Error("ELASTICSEARCH_URL environment variable is not defined");
|
||||
}
|
||||
|
||||
let auth: { username: string; password: string } | { apiKey: string };
|
||||
if (configuration.retrieverProvider === "elastic-local") {
|
||||
const username = process.env.ELASTICSEARCH_USER;
|
||||
const password = process.env.ELASTICSEARCH_PASSWORD;
|
||||
if (!username || !password) {
|
||||
throw new Error(
|
||||
"ELASTICSEARCH_USER or ELASTICSEARCH_PASSWORD environment variable is not defined",
|
||||
);
|
||||
}
|
||||
auth = { username, password };
|
||||
} else {
|
||||
const apiKey = process.env.ELASTICSEARCH_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"ELASTICSEARCH_API_KEY environment variable is not defined",
|
||||
);
|
||||
}
|
||||
auth = { apiKey };
|
||||
}
|
||||
|
||||
const client = new Client({
|
||||
node: elasticUrl,
|
||||
auth,
|
||||
});
|
||||
|
||||
const vectorStore = new ElasticVectorSearch(embeddingModel, {
|
||||
client,
|
||||
indexName: "langchain_index",
|
||||
});
|
||||
const searchKwargs = configuration.searchKwargs || {};
|
||||
const filter = {
|
||||
...searchKwargs,
|
||||
user_id: configuration.userId,
|
||||
};
|
||||
|
||||
return vectorStore.asRetriever({ filter });
|
||||
}
|
||||
|
||||
async function makePineconeRetriever(
|
||||
configuration: ReturnType<typeof ensureConfiguration>,
|
||||
embeddingModel: Embeddings,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
const indexName = process.env.PINECONE_INDEX_NAME;
|
||||
if (!indexName) {
|
||||
throw new Error("PINECONE_INDEX_NAME environment variable is not defined");
|
||||
}
|
||||
const pinecone = new PineconeClient();
|
||||
const pineconeIndex = pinecone.Index(indexName!);
|
||||
const vectorStore = await PineconeStore.fromExistingIndex(embeddingModel, {
|
||||
pineconeIndex,
|
||||
});
|
||||
|
||||
const searchKwargs = configuration.searchKwargs || {};
|
||||
const filter = {
|
||||
...searchKwargs,
|
||||
user_id: configuration.userId,
|
||||
};
|
||||
|
||||
return vectorStore.asRetriever({ filter });
|
||||
}
|
||||
|
||||
async function makeMongoDBRetriever(
|
||||
configuration: ReturnType<typeof ensureConfiguration>,
|
||||
embeddingModel: Embeddings,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error("MONGODB_URI environment variable is not defined");
|
||||
}
|
||||
const client = new MongoClient(process.env.MONGODB_URI);
|
||||
const namespace = `langgraph_retrieval_agent.${configuration.userId}`;
|
||||
const [dbName, collectionName] = namespace.split(".");
|
||||
const collection = client.db(dbName).collection(collectionName);
|
||||
const vectorStore = new MongoDBAtlasVectorSearch(embeddingModel, {
|
||||
collection: collection,
|
||||
textKey: "text",
|
||||
embeddingKey: "embedding",
|
||||
indexName: "vector_index",
|
||||
});
|
||||
const searchKwargs = { ...configuration.searchKwargs };
|
||||
searchKwargs.preFilter = {
|
||||
...searchKwargs.preFilter,
|
||||
user_id: { $eq: configuration.userId },
|
||||
};
|
||||
return vectorStore.asRetriever({ filter: searchKwargs });
|
||||
}
|
||||
|
||||
function makeTextEmbeddings(modelName: string): Embeddings {
|
||||
/**
|
||||
* Connect to the configured text encoder.
|
||||
*/
|
||||
const index = modelName.indexOf("/");
|
||||
let provider, model;
|
||||
if (index === -1) {
|
||||
model = modelName;
|
||||
provider = "openai"; // Assume openai if no provider included
|
||||
} else {
|
||||
provider = modelName.slice(0, index);
|
||||
model = modelName.slice(index + 1);
|
||||
}
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
return new OpenAIEmbeddings({ model });
|
||||
case "cohere":
|
||||
return new CohereEmbeddings({ model });
|
||||
default:
|
||||
throw new Error(`Unsupported embedding provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeRetriever(
|
||||
config: RunnableConfig,
|
||||
): Promise<VectorStoreRetriever> {
|
||||
const configuration = ensureConfiguration(config);
|
||||
const embeddingModel = makeTextEmbeddings(configuration.embeddingModel);
|
||||
|
||||
const userId = configuration.userId;
|
||||
if (!userId) {
|
||||
throw new Error("Please provide a valid user_id in the configuration.");
|
||||
}
|
||||
|
||||
switch (configuration.retrieverProvider) {
|
||||
case "elastic":
|
||||
case "elastic-local":
|
||||
return makeElasticRetriever(configuration, embeddingModel);
|
||||
case "pinecone":
|
||||
return makePineconeRetriever(configuration, embeddingModel);
|
||||
case "mongodb":
|
||||
return makeMongoDBRetriever(configuration, embeddingModel);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unrecognized retrieverProvider in configuration: ${configuration.retrieverProvider}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { Annotation, MessagesAnnotation } from "@langchain/langgraph";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Reduces the document array based on the provided new documents or actions.
|
||||
*
|
||||
* @param existing - The existing array of documents.
|
||||
* @param newDocs - The new documents or actions to apply.
|
||||
* @returns The updated array of documents.
|
||||
*/
|
||||
export function reduceDocs(
|
||||
existing?: Document[],
|
||||
newDocs?:
|
||||
| Document[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| { [key: string]: any }[]
|
||||
| string[]
|
||||
| string
|
||||
| "delete",
|
||||
) {
|
||||
// Supports deletion by returning an empty array when "delete" is specified
|
||||
if (newDocs === "delete") {
|
||||
return [];
|
||||
}
|
||||
// Supports adding a single string document
|
||||
if (typeof newDocs === "string") {
|
||||
const docId = uuidv4();
|
||||
return [{ pageContent: newDocs, metadata: { id: docId }, id: docId }];
|
||||
}
|
||||
// User can provide "docs" content in a few different ways
|
||||
if (Array.isArray(newDocs)) {
|
||||
const coerced: Document[] = [];
|
||||
for (const item of newDocs) {
|
||||
if (typeof item === "string") {
|
||||
coerced.push({ pageContent: item, metadata: { id: uuidv4() } });
|
||||
} else if (typeof item === "object") {
|
||||
const doc = item as Document;
|
||||
const docId = item?.id || uuidv4();
|
||||
item.id = docId;
|
||||
if (!doc.metadata || !doc.metadata.id) {
|
||||
doc.metadata = doc.metadata || {};
|
||||
doc.metadata.id = docId;
|
||||
}
|
||||
coerced.push(doc);
|
||||
}
|
||||
}
|
||||
return coerced;
|
||||
}
|
||||
// Returns existing documents if no valid update is provided
|
||||
return existing || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the structure and behavior of the index state.
|
||||
* This state is used to manage the documents in the index.
|
||||
*/
|
||||
export const IndexStateAnnotation = Annotation.Root({
|
||||
/**
|
||||
* Stores the documents in the index.
|
||||
*
|
||||
* @type {Document[]} - An array of Document objects.
|
||||
* @reducer reduceDocs - A function that handles updates to the documents array.
|
||||
* It can add new documents, replace existing ones, or delete all documents.
|
||||
* @default An empty array ([]).
|
||||
* @see reduceDocs for detailed behavior on how updates are processed.
|
||||
*/
|
||||
docs: Annotation<
|
||||
Document[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Document[] | { [key: string]: any }[] | string[] | string | "delete"
|
||||
>({
|
||||
reducer: reduceDocs,
|
||||
default: () => [],
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* This narrows the interface with the user.
|
||||
*/
|
||||
export const InputStateAnnotation = Annotation.Root({
|
||||
messages: Annotation<BaseMessage[]>,
|
||||
});
|
||||
|
||||
/**
|
||||
* The State defines three things:
|
||||
* 1. The structure of the graph's state (which "channels" are available to read/write)
|
||||
* 2. The default values for the state's channels
|
||||
* 3. The reducers for the state's channels. Reducers are functions that determine how to apply updates to the state.
|
||||
* See [Reducers](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#reducers) for more information.
|
||||
*/
|
||||
export const StateAnnotation = Annotation.Root({
|
||||
/**
|
||||
* Stores the conversation messages.
|
||||
* @type {BaseMessage[]}
|
||||
* @reducer Default reducer that appends new messages to the existing ones.
|
||||
* @default An empty array.
|
||||
*
|
||||
* Nodes can return a list of "MessageLike" objects, which can be LangChain messages
|
||||
* or dictionaries following a common message format.
|
||||
*
|
||||
* To delete messages, use RemoveMessage.
|
||||
* @see https://langchain-ai.github.io/langgraphjs/how-tos/delete-messages/
|
||||
*
|
||||
* For more information, see:
|
||||
* @see https://langchain-ai.github.io/langgraphjs/reference/variables/langgraph.MessagesAnnotation.html
|
||||
*/
|
||||
...MessagesAnnotation.spec,
|
||||
|
||||
/**
|
||||
* Stores the user queries.
|
||||
* @type {string[]}
|
||||
* @reducer A custom reducer function that appends new queries to the existing array.
|
||||
* It handles both single string and string array inputs.
|
||||
* @default An empty array ([]).
|
||||
* @description This annotation manages the list of user queries in the state.
|
||||
* It uses a reducer to add new queries while preserving existing ones.
|
||||
* The reducer supports adding either a single query (string) or multiple queries (string[]).
|
||||
*/
|
||||
queries: Annotation<string[], string | string[]>({
|
||||
reducer: (existing: string[], newQueries: string[] | string) => {
|
||||
/**
|
||||
* This reducer is currently "append only" - it only adds new queries to the existing list.
|
||||
*
|
||||
* To extend this reducer to support more complex operations, you could modify it in ways like this:
|
||||
*
|
||||
* reducer: (existing: string[], action: { type: string; payload: string | string[] }) => {
|
||||
* switch (action.type) {
|
||||
* case 'ADD':
|
||||
* return [...existing, ...(Array.isArray(action.payload) ? action.payload : [action.payload])];
|
||||
* case 'DELETE':
|
||||
* return existing.filter(query => query !== action.payload);
|
||||
* case 'REPLACE':
|
||||
* return Array.isArray(action.payload) ? action.payload : [action.payload];
|
||||
* default:
|
||||
* return existing;
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
return [
|
||||
...existing,
|
||||
...(Array.isArray(newQueries) ? newQueries : [newQueries]),
|
||||
];
|
||||
},
|
||||
default: () => [],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stores the retrieved documents.
|
||||
* @type {Document[]}
|
||||
*/
|
||||
retrievedDocs: Annotation<Document[]>,
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 624 KiB |
@@ -0,0 +1,57 @@
|
||||
import { BaseMessage } from "@langchain/core/messages";
|
||||
import { Document } from "langchain/document";
|
||||
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
import { initChatModel } from "langchain/chat_models/universal";
|
||||
|
||||
export function getMessageText(msg: BaseMessage): string {
|
||||
/** Get the text content of a message. */
|
||||
const content = msg.content;
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const txts = (content as any[]).map((c) =>
|
||||
typeof c === "string" ? c : c.text || "",
|
||||
);
|
||||
return txts.join("").trim();
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDoc(doc: Document): string {
|
||||
const metadata = doc.metadata || {};
|
||||
const meta = Object.entries(metadata)
|
||||
.map(([k, v]) => ` ${k}=${v}`)
|
||||
.join("");
|
||||
const metaStr = meta ? ` ${meta}` : "";
|
||||
|
||||
return `<document${metaStr}>\n${doc.pageContent}\n</document>`;
|
||||
}
|
||||
|
||||
export function formatDocs(docs?: Document[]): string {
|
||||
/**Format a list of documents as XML. */
|
||||
if (!docs || docs.length === 0) {
|
||||
return "<documents></documents>";
|
||||
}
|
||||
const formatted = docs.map(formatDoc).join("\n");
|
||||
return `<documents>\n${formatted}\n</documents>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a chat model from a fully specified name.
|
||||
* @param fullySpecifiedName - String in the format 'provider/model' or 'provider/account/provider/model'.
|
||||
* @returns A Promise that resolves to a BaseChatModel instance.
|
||||
*/
|
||||
export async function loadChatModel(
|
||||
fullySpecifiedName: string,
|
||||
): Promise<BaseChatModel> {
|
||||
const index = fullySpecifiedName.indexOf("/");
|
||||
if (index === -1) {
|
||||
// If there's no "/", assume it's just the model
|
||||
return await initChatModel(fullySpecifiedName);
|
||||
} else {
|
||||
const provider = fullySpecifiedName.slice(0, index);
|
||||
const model = fullySpecifiedName.slice(index + 1);
|
||||
return await initChatModel(model, { modelProvider: provider });
|
||||
}
|
||||
}
|
||||
@@ -788,6 +788,14 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.20.7"
|
||||
|
||||
"@types/fs-extra@^11.0.4":
|
||||
version "11.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45"
|
||||
integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==
|
||||
dependencies:
|
||||
"@types/jsonfile" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/gensync@^1.0.0":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/gensync/-/gensync-1.0.4.tgz#7122d8f0cd3bf437f9725cc95b180197190cf50b"
|
||||
@@ -837,6 +845,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/jsonfile@*":
|
||||
version "6.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702"
|
||||
integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*", "@types/node@^22.10.6":
|
||||
version "22.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
|
||||
@@ -844,6 +859,14 @@
|
||||
dependencies:
|
||||
undici-types "~6.20.0"
|
||||
|
||||
"@types/prompts@^2.4.9":
|
||||
version "2.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.4.9.tgz#8775a31e40ad227af511aa0d7f19a044ccbd371e"
|
||||
integrity sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
kleur "^3.0.3"
|
||||
|
||||
"@types/semver@^7.3.12":
|
||||
version "7.5.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
|
||||
|
||||
Reference in New Issue
Block a user