mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-10 15:53:42 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffe8919cde | |||
| 081698d68c | |||
| ab5fe5d7a0 | |||
| 56689707d3 | |||
| fd74ba4bf1 |
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@llamaindex/core": patch
|
||||
"@llamaindex/workflow": patch
|
||||
---
|
||||
|
||||
feat: Support AgentWorkflow
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@llamaindex/voyage-ai": patch
|
||||
---
|
||||
|
||||
Fixing tsconfig for voyage-ai provider
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Agent Workflow
|
||||
---
|
||||
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
import CodeSource from "!raw-loader!../../../../../../../examples/agentworkflow/blog_writer.ts";
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
|
||||
`AgentWorkflow` is a powerful system that enables you to create and orchestrate one or multiple agents with tools to perform specific tasks. It's built on top of the base `Workflow` system and provides a streamlined interface for agent interactions.
|
||||
|
||||
## Installation
|
||||
|
||||
You'll need to install the `@llamaindex/workflow` package:
|
||||
|
||||
<Tabs groupId="install" items={["npm", "yarn", "pnpm"]} persist>
|
||||
```shell tab="npm"
|
||||
npm install @llamaindex/workflow
|
||||
```
|
||||
|
||||
```shell tab="yarn"
|
||||
yarn add @llamaindex/workflow
|
||||
```
|
||||
|
||||
```shell tab="pnpm"
|
||||
pnpm add @llamaindex/workflow
|
||||
```
|
||||
</Tabs>
|
||||
|
||||
## Usage
|
||||
|
||||
### Single Agent Workflow
|
||||
|
||||
The simplest use case is creating a single agent with specific tools. Here's an example of creating an assistant that tells jokes:
|
||||
|
||||
```typescript
|
||||
import { AgentWorkflow, FunctionTool } from "llamaindex";
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
|
||||
// Define a joke-telling tool
|
||||
const jokeTool = FunctionTool.from(
|
||||
() => "Baby Llama is called cria",
|
||||
{
|
||||
name: "joke",
|
||||
description: "Use this tool to get a joke",
|
||||
}
|
||||
);
|
||||
|
||||
// Create an agent workflow with the tool
|
||||
const workflow = AgentWorkflow.fromTools({
|
||||
tools: [jokeTool],
|
||||
llm: new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
}),
|
||||
});
|
||||
|
||||
// Run the workflow
|
||||
const result = await workflow.run("Tell me something funny");
|
||||
console.log(result); // Baby Llama is called cria
|
||||
```
|
||||
|
||||
### Event Streaming
|
||||
|
||||
`AgentWorkflow` provides a unified interface for event streaming, making it easy to track and respond to different events during execution:
|
||||
|
||||
```typescript
|
||||
import { AgentToolCall, AgentStream } from "llamaindex";
|
||||
|
||||
// Get the workflow execution context
|
||||
const context = workflow.run("Tell me something funny");
|
||||
|
||||
// Stream and handle events
|
||||
for await (const event of context) {
|
||||
if (event instanceof AgentToolCall) {
|
||||
console.log(`Tool being called: ${event.data.toolName}`);
|
||||
}
|
||||
if (event instanceof AgentStream) {
|
||||
process.stdout.write(event.data.delta);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Agent Workflow
|
||||
|
||||
`AgentWorkflow` can orchestrate multiple agents, enabling complex interactions and task handoffs. Each agent in a multi-agent workflow requires:
|
||||
|
||||
- `name`: Unique identifier for the agent
|
||||
- `description`: Purpose description used for task routing
|
||||
- `tools`: Array of tools the agent can use
|
||||
- `canHandoffTo` (optional): Array of agent names or agent instances that this agent can delegate tasks to
|
||||
|
||||
Here's an example of a multi-agent system that combines joke-telling and weather information:
|
||||
|
||||
```typescript
|
||||
import { AgentWorkflow, FunctionAgent, FunctionTool } from "llamaindex";
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { z } from "zod";
|
||||
|
||||
// Create a weather agent
|
||||
const weatherAgent = new FunctionAgent({
|
||||
name: "WeatherAgent",
|
||||
description: "Provides weather information for any city",
|
||||
tools: [
|
||||
FunctionTool.from(
|
||||
({ city }: { city: string }) => `The weather in ${city} is sunny`,
|
||||
{
|
||||
name: "fetchWeather",
|
||||
description: "Get weather information for a city",
|
||||
parameters: z.object({
|
||||
city: z.string(),
|
||||
}),
|
||||
}
|
||||
),
|
||||
],
|
||||
llm: new OpenAI({ model: "gpt-4o-mini" }),
|
||||
});
|
||||
|
||||
// Create a joke-telling agent
|
||||
const jokeAgent = new FunctionAgent({
|
||||
name: "JokeAgent",
|
||||
description: "Tells jokes and funny stories",
|
||||
tools: [jokeTool], // Using the joke tool defined earlier
|
||||
llm: new OpenAI({ model: "gpt-4o-mini" }),
|
||||
canHandoffTo: [weatherAgent], // Can hand off to the weather agent
|
||||
});
|
||||
|
||||
// Create the multi-agent workflow
|
||||
const workflow = new AgentWorkflow({
|
||||
agents: [jokeAgent, weatherAgent],
|
||||
rootAgent: jokeAgent, // Start with the joke agent
|
||||
});
|
||||
|
||||
// Run the workflow
|
||||
const result = await workflow.run(
|
||||
"Give me a morning greeting with a joke and the weather in San Francisco"
|
||||
);
|
||||
```
|
||||
|
||||
The workflow will coordinate between agents, allowing them to handle different aspects of the request and hand off tasks when appropriate.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import fs from "fs";
|
||||
import {
|
||||
AgentToolCall,
|
||||
AgentToolCallResult,
|
||||
AgentWorkflow,
|
||||
FunctionAgent,
|
||||
FunctionTool,
|
||||
} from "llamaindex";
|
||||
import os from "os";
|
||||
import { z } from "zod";
|
||||
|
||||
import { WikipediaTool } from "../wiki";
|
||||
const llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
const saveFileTool = FunctionTool.from(
|
||||
({ content }: { content: string }) => {
|
||||
const filePath = os.tmpdir() + "/report.md";
|
||||
fs.writeFileSync(filePath, content);
|
||||
return `File saved successfully at ${filePath}`;
|
||||
},
|
||||
{
|
||||
name: "saveFile",
|
||||
description:
|
||||
"Save the written content into a file that can be downloaded by the user",
|
||||
parameters: z.object({
|
||||
content: z.string({
|
||||
description: "The content to save into a file",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
async function main() {
|
||||
const reportAgent = new FunctionAgent({
|
||||
name: "ReportAgent",
|
||||
description:
|
||||
"Responsible for crafting well-written blog posts based on research findings",
|
||||
systemPrompt: `You are a professional writer. Your task is to create an engaging blog post using the research content provided. Once complete, save the post to a file using the saveFile tool.`,
|
||||
tools: [saveFileTool],
|
||||
llm,
|
||||
});
|
||||
|
||||
const researchAgent = new FunctionAgent({
|
||||
name: "ResearchAgent",
|
||||
description:
|
||||
"Responsible for gathering relevant information from the internet",
|
||||
systemPrompt: `You are a research agent. Your role is to gather information from the internet using the provided tools and then transfer this information to the report agent for content creation.`,
|
||||
tools: [new WikipediaTool()],
|
||||
canHandoffTo: [reportAgent],
|
||||
llm,
|
||||
});
|
||||
|
||||
const workflow = new AgentWorkflow({
|
||||
agents: [researchAgent, reportAgent],
|
||||
rootAgent: researchAgent,
|
||||
});
|
||||
|
||||
const context = workflow.run("Write a blog post about history of LLM");
|
||||
|
||||
let finalResult;
|
||||
for await (const event of context) {
|
||||
if (event instanceof AgentToolCall) {
|
||||
console.log(
|
||||
`[Agent ${event.displayName}] executing tool ${event.data.toolName} with parameters ${JSON.stringify(
|
||||
event.data.toolKwargs,
|
||||
)}`,
|
||||
);
|
||||
} else if (event instanceof AgentToolCallResult) {
|
||||
console.log(
|
||||
`[Agent ${event.displayName}] executed tool ${event.data.toolName} with result ${event.data.toolOutput.result}`,
|
||||
);
|
||||
}
|
||||
finalResult = event;
|
||||
}
|
||||
console.log("Final result:", finalResult?.data);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* This example shows how to use AgentWorkflow with multiple agents
|
||||
* 1. FetchWeatherAgent - Fetches the weather in a city
|
||||
* 2. TemperatureConverterAgent - Converts the temperature from Fahrenheit to Celsius
|
||||
*/
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { StopEvent } from "@llamaindex/workflow";
|
||||
import {
|
||||
AgentInput,
|
||||
AgentOutput,
|
||||
AgentStream,
|
||||
AgentToolCall,
|
||||
AgentToolCallResult,
|
||||
AgentWorkflow,
|
||||
FunctionAgent,
|
||||
FunctionTool,
|
||||
} from "llamaindex";
|
||||
import { z } from "zod";
|
||||
|
||||
const llm = new OpenAI({
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
// Define tools for the agents
|
||||
const temperatureConverterTool = FunctionTool.from(
|
||||
({ temperature }: { temperature: number }) => {
|
||||
return ((temperature - 32) * 5) / 9;
|
||||
},
|
||||
{
|
||||
description: "Convert a temperature from Fahrenheit to Celsius",
|
||||
name: "fahrenheitToCelsius",
|
||||
parameters: z.object({
|
||||
temperature: z.number({
|
||||
description: "The temperature in Fahrenheit",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const temperatureFetcherTool = FunctionTool.from(
|
||||
({ city }: { city: string }) => {
|
||||
const temperature = Math.floor(Math.random() * 58) + 32;
|
||||
return `The current temperature in ${city} is ${temperature}°F`;
|
||||
},
|
||||
{
|
||||
description: "Fetch the temperature (in Fahrenheit) for a city",
|
||||
name: "fetchTemperature",
|
||||
parameters: z.object({
|
||||
city: z.string({
|
||||
description: "The city to fetch the temperature for",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
// Create agents
|
||||
async function multiWeatherAgent() {
|
||||
const converterAgent = new FunctionAgent({
|
||||
name: "TemperatureConverterAgent",
|
||||
description:
|
||||
"An agent that can convert temperatures from Fahrenheit to Celsius.",
|
||||
tools: [temperatureConverterTool],
|
||||
llm,
|
||||
});
|
||||
|
||||
const weatherAgent = new FunctionAgent({
|
||||
name: "FetchWeatherAgent",
|
||||
description: "An agent that can get the weather in a city. ",
|
||||
systemPrompt:
|
||||
"If you can't answer the user question, hand off to other agents.",
|
||||
tools: [temperatureFetcherTool],
|
||||
llm,
|
||||
// Define which next agents can be called next if this agent cannot complete the task
|
||||
// Can be passed as agent name, e.g. "TemperatureConverterAgent"
|
||||
canHandoffTo: [converterAgent],
|
||||
});
|
||||
|
||||
// Create agent workflow with the agents
|
||||
const workflow = new AgentWorkflow({
|
||||
agents: [weatherAgent, converterAgent],
|
||||
rootAgent: weatherAgent,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
// Ask the agent to get the weather in a city
|
||||
const context = workflow.run(
|
||||
"What is the weather in San Francisco in Celsius?",
|
||||
);
|
||||
// Stream the events
|
||||
for await (const event of context) {
|
||||
// These events might be useful for UI
|
||||
if (
|
||||
event instanceof AgentToolCall ||
|
||||
event instanceof AgentToolCallResult ||
|
||||
event instanceof AgentOutput ||
|
||||
event instanceof AgentInput ||
|
||||
event instanceof StopEvent
|
||||
) {
|
||||
console.log(event);
|
||||
} else if (event instanceof AgentStream) {
|
||||
for (const chunk of event.data.delta) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multiWeatherAgent().catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* This example shows how to use AgentWorkflow as a single agent with tools
|
||||
*/
|
||||
import { OpenAI } from "@llamaindex/openai";
|
||||
import { AgentWorkflow } from "llamaindex";
|
||||
import { getWeatherTool } from "../agent/utils/tools";
|
||||
|
||||
const llm = new OpenAI({
|
||||
model: "gpt-4o",
|
||||
});
|
||||
|
||||
async function singleWeatherAgent() {
|
||||
const workflow = AgentWorkflow.fromTools({
|
||||
tools: [getWeatherTool],
|
||||
llm,
|
||||
verbose: false,
|
||||
});
|
||||
|
||||
const workflowContext = workflow.run(
|
||||
"What's the weather like in San Francisco?",
|
||||
);
|
||||
const sfResult = await workflowContext;
|
||||
// The weather in San Francisco, CA is currently sunny.
|
||||
console.log(`${JSON.stringify(sfResult, null, 2)}`);
|
||||
|
||||
// Reuse the context from the previous run
|
||||
const workflowContext2 = workflow.run("Compare it with California?", {
|
||||
context: workflowContext.data,
|
||||
});
|
||||
const caResult = await workflowContext2;
|
||||
// Both San Francisco and California are currently experiencing sunny weather.
|
||||
console.log(`${JSON.stringify(caResult, null, 2)}`);
|
||||
}
|
||||
|
||||
singleWeatherAgent().catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
@@ -56,7 +56,8 @@
|
||||
"mongodb": "6.7.0",
|
||||
"pathe": "^1.1.2",
|
||||
"postgres": "^3.4.4",
|
||||
"wikipedia": "^2.1.2"
|
||||
"wikipedia": "^2.1.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.9.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ async function main() {
|
||||
tools: [
|
||||
{
|
||||
metadata: {
|
||||
name: "wikipedia_tool",
|
||||
name: "wikipedia_search",
|
||||
description: "A tool that uses a query engine to search Wikipedia.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type WikipediaToolParams = {
|
||||
};
|
||||
|
||||
const DEFAULT_META_DATA: ToolMetadata<JSONSchemaType<WikipediaParameter>> = {
|
||||
name: "wikipedia_tool",
|
||||
name: "wikipedia_search",
|
||||
description: "A tool that uses a query engine to search Wikipedia.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
|
||||
@@ -215,6 +215,10 @@ export type ToolMetadata<
|
||||
* @link https://json-schema.org/understanding-json-schema
|
||||
*/
|
||||
parameters?: Parameters;
|
||||
/**
|
||||
* Whether the tool requires workflow context to be passed in.
|
||||
*/
|
||||
requireContext?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -59,14 +59,28 @@ export class FunctionTool<T, R extends JSONValue | Promise<JSONValue>>
|
||||
}
|
||||
|
||||
call = (input: T) => {
|
||||
if (this.#metadata.requireContext) {
|
||||
const inputWithContext = input as Record<string, unknown>;
|
||||
if (!inputWithContext.context) {
|
||||
throw new Error(
|
||||
"Tool call requires context, but context parameter is missing",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (this.#zodType) {
|
||||
const result = this.#zodType.safeParse(input);
|
||||
if (result.success) {
|
||||
return this.#fn.call(null, result.data);
|
||||
if (this.#metadata.requireContext) {
|
||||
const { context } = input as Record<string, unknown>;
|
||||
return this.#fn.call(null, { context, ...result.data });
|
||||
} else {
|
||||
return this.#fn.call(null, result.data);
|
||||
}
|
||||
} else {
|
||||
console.warn(result.error.errors);
|
||||
}
|
||||
}
|
||||
|
||||
return this.#fn.call(null, input);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@llamaindex/node-parser": "workspace:*",
|
||||
"@llamaindex/openai": "workspace:*",
|
||||
"@llamaindex/workflow": "workspace:*",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^22.9.0",
|
||||
"ajv": "^8.17.1",
|
||||
|
||||
@@ -65,6 +65,7 @@ export * from "@llamaindex/core/storage/doc-store";
|
||||
export * from "@llamaindex/core/storage/index-store";
|
||||
export * from "@llamaindex/core/storage/kv-store";
|
||||
export * from "@llamaindex/core/utils";
|
||||
export * from "@llamaindex/workflow/agent";
|
||||
export * from "./agent/index.js";
|
||||
export * from "./cloud/index.js";
|
||||
export * from "./embeddings/index.js";
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../openai/tsconfig.json"
|
||||
"path": "../../core/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "../../env/tsconfig.json"
|
||||
|
||||
@@ -37,6 +37,34 @@
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./agent": {
|
||||
"node": {
|
||||
"types": "./dist/agent/index.d.ts",
|
||||
"import": "./dist/agent/index.js",
|
||||
"require": "./dist/agent/index.cjs",
|
||||
"default": "./dist/agent/index.cjs"
|
||||
},
|
||||
"workerd": {
|
||||
"types": "./dist/agent/index.workerd.d.ts",
|
||||
"default": "./dist/agent/index.workerd.js"
|
||||
},
|
||||
"edge-light": {
|
||||
"types": "./dist/agent/index.edge-light.d.ts",
|
||||
"default": "./dist/agent/index.edge-light.js"
|
||||
},
|
||||
"browser": {
|
||||
"types": "./dist/agent/index.browser.d.ts",
|
||||
"default": "./dist/agent/index.browser.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/agent/index.d.ts",
|
||||
"default": "./dist/agent/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/agent/index.d.cts",
|
||||
"default": "./dist/agent/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -55,10 +83,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@types/node": "^22.9.0",
|
||||
"bunchee": "6.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@llamaindex/env": "workspace:*"
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,568 @@
|
||||
import type {
|
||||
BaseToolWithCall,
|
||||
ChatMessage,
|
||||
ToolCallLLM,
|
||||
} from "@llamaindex/core/llms";
|
||||
import { ChatMemoryBuffer } from "@llamaindex/core/memory";
|
||||
import { PromptTemplate } from "@llamaindex/core/prompts";
|
||||
import { FunctionTool } from "@llamaindex/core/tools";
|
||||
import { stringifyJSONToMessageContent } from "@llamaindex/core/utils";
|
||||
import { z } from "zod";
|
||||
import { Workflow } from "../workflow";
|
||||
import type { HandlerContext, WorkflowContext } from "../workflow-context";
|
||||
import { StartEvent, StopEvent, WorkflowEvent } from "../workflow-event";
|
||||
import type { AgentWorkflowContext, BaseWorkflowAgent } from "./base";
|
||||
import {
|
||||
AgentInput,
|
||||
AgentOutput,
|
||||
AgentSetup,
|
||||
AgentToolCall,
|
||||
AgentToolCallResult,
|
||||
} from "./events";
|
||||
import { FunctionAgent } from "./function-agent";
|
||||
|
||||
export const DEFAULT_HANDOFF_PROMPT = new PromptTemplate({
|
||||
template: `Useful for handing off to another agent.
|
||||
If you are currently not equipped to handle the user's request, or another agent is better suited to handle the request, please hand off to the appropriate agent.
|
||||
|
||||
Currently available agents:
|
||||
{agent_info}
|
||||
`,
|
||||
});
|
||||
|
||||
export const DEFAULT_HANDOFF_OUTPUT_PROMPT = new PromptTemplate({
|
||||
template: `Agent {to_agent} is now handling the request due to the following reason: {reason}.\nPlease continue with the current request.`,
|
||||
});
|
||||
|
||||
export type AgentInputData = {
|
||||
userInput?: string | undefined;
|
||||
chatHistory?: ChatMessage[] | undefined;
|
||||
};
|
||||
|
||||
// Wrapper events for multiple tool calls and results
|
||||
export class ToolCallsEvent extends WorkflowEvent<{
|
||||
agentName: string;
|
||||
toolCalls: AgentToolCall[];
|
||||
}> {}
|
||||
|
||||
export class ToolResultsEvent extends WorkflowEvent<{
|
||||
agentName: string;
|
||||
results: AgentToolCallResult[];
|
||||
}> {}
|
||||
|
||||
export class AgentStepEvent extends WorkflowEvent<{
|
||||
agentName: string;
|
||||
response: ChatMessage;
|
||||
toolCalls: AgentToolCall[];
|
||||
}> {}
|
||||
|
||||
export type AgentWorkflowParams = {
|
||||
/**
|
||||
* List of agents to include in the workflow.
|
||||
* Need at least one agent.
|
||||
*/
|
||||
agents: BaseWorkflowAgent[];
|
||||
/**
|
||||
* The agent to start the workflow with.
|
||||
* Must be an agent in the `agents` list.
|
||||
*/
|
||||
rootAgent: BaseWorkflowAgent;
|
||||
verbose?: boolean;
|
||||
/**
|
||||
* Timeout for the workflow in seconds.
|
||||
*/
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* AgentWorkflow - An event-driven workflow for executing agents with tools
|
||||
*
|
||||
* This class provides a simple interface for creating and running agent workflows
|
||||
* based on the LlamaIndexTS workflow system. It supports single agent workflows
|
||||
* with multiple tools.
|
||||
*/
|
||||
export class AgentWorkflow {
|
||||
private workflow: Workflow<AgentWorkflowContext, AgentInputData, string>;
|
||||
private agents: Map<string, BaseWorkflowAgent> = new Map();
|
||||
private verbose: boolean;
|
||||
private rootAgentName: string;
|
||||
|
||||
constructor({ agents, rootAgent, verbose, timeout }: AgentWorkflowParams) {
|
||||
this.workflow = new Workflow({
|
||||
verbose: verbose ?? false,
|
||||
timeout: timeout ?? 60,
|
||||
});
|
||||
this.verbose = verbose ?? false;
|
||||
this.rootAgentName = rootAgent.name;
|
||||
// Validate root agent
|
||||
if (!agents.some((a) => a.name === this.rootAgentName)) {
|
||||
throw new Error(`Root agent ${rootAgent} not found in agents`);
|
||||
}
|
||||
this.addAgents(agents ?? []);
|
||||
}
|
||||
|
||||
private validateAgent(agent: BaseWorkflowAgent) {
|
||||
// Validate that all canHandoffTo agents exist
|
||||
const invalidAgents = agent.canHandoffTo.filter(
|
||||
(name) => !this.agents.has(name),
|
||||
);
|
||||
if (invalidAgents.length > 0) {
|
||||
throw new Error(
|
||||
`Agent "${agent.name}" references non-existent agents in canHandoffTo: ${invalidAgents.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private addHandoffTool(agent: BaseWorkflowAgent) {
|
||||
const handoffTool = createHandoffTool(this.agents);
|
||||
if (
|
||||
agent.canHandoffTo.length > 0 &&
|
||||
!agent.tools.some((t) => t.metadata.name === handoffTool.metadata.name)
|
||||
) {
|
||||
agent.tools.push(handoffTool);
|
||||
}
|
||||
}
|
||||
|
||||
private addAgents(agents: BaseWorkflowAgent[]): void {
|
||||
const agentNames = new Set(agents.map((a) => a.name));
|
||||
if (agentNames.size !== agents.length) {
|
||||
throw new Error("The agent names must be unique!");
|
||||
}
|
||||
|
||||
// First pass: add all agents to the map
|
||||
agents.forEach((agent) => {
|
||||
this.agents.set(agent.name, agent);
|
||||
});
|
||||
|
||||
// Second pass: validate and setup handoff tools
|
||||
agents.forEach((agent) => {
|
||||
this.validateAgent(agent);
|
||||
this.addHandoffTool(agent);
|
||||
});
|
||||
}
|
||||
|
||||
addAgent(agent: BaseWorkflowAgent): this {
|
||||
this.agents.set(agent.name, agent);
|
||||
this.validateAgent(agent);
|
||||
this.addHandoffTool(agent);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a simple workflow with a single agent and specified tools
|
||||
*/
|
||||
static fromTools({
|
||||
tools,
|
||||
llm,
|
||||
systemPrompt,
|
||||
verbose,
|
||||
timeout,
|
||||
}: {
|
||||
tools: BaseToolWithCall[];
|
||||
llm: ToolCallLLM;
|
||||
systemPrompt?: string;
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
}): AgentWorkflow {
|
||||
const agent = new FunctionAgent({
|
||||
name: "Agent",
|
||||
description: "A single agent that uses the provided tools or functions.",
|
||||
tools,
|
||||
llm,
|
||||
systemPrompt,
|
||||
});
|
||||
|
||||
const workflow = new AgentWorkflow({
|
||||
agents: [agent],
|
||||
rootAgent: agent,
|
||||
verbose: verbose ?? false,
|
||||
timeout: timeout ?? 60,
|
||||
});
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private handleInputStep = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
event: StartEvent<AgentInputData>,
|
||||
): Promise<AgentInput> => {
|
||||
const { userInput, chatHistory } = event.data;
|
||||
const memory = ctx.data.memory;
|
||||
if (chatHistory) {
|
||||
chatHistory.forEach((message) => {
|
||||
memory.put(message);
|
||||
});
|
||||
}
|
||||
if (userInput) {
|
||||
const userMessage: ChatMessage = {
|
||||
role: "user",
|
||||
content: userInput,
|
||||
};
|
||||
memory.put(userMessage);
|
||||
} else if (chatHistory) {
|
||||
// If no user message, use the last message from chat history as user_msg_str
|
||||
const lastMessage = chatHistory[chatHistory.length - 1];
|
||||
if (lastMessage?.role !== "user") {
|
||||
throw new Error(
|
||||
"Either provide a user message or a chat history with a user message as the last message",
|
||||
);
|
||||
}
|
||||
ctx.data.userInput = lastMessage.content as string;
|
||||
} else {
|
||||
throw new Error("No user message or chat history provided");
|
||||
}
|
||||
|
||||
return new AgentInput({
|
||||
input: await memory.getMessages(),
|
||||
currentAgentName: this.rootAgentName,
|
||||
});
|
||||
};
|
||||
|
||||
private setupAgent = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
event: AgentInput,
|
||||
): Promise<AgentSetup> => {
|
||||
const currentAgentName = event.data.currentAgentName;
|
||||
const agent = this.agents.get(currentAgentName);
|
||||
if (!agent) {
|
||||
throw new Error(`Agent ${currentAgentName} not found`);
|
||||
}
|
||||
|
||||
const llmInput = event.data.input;
|
||||
if (agent.systemPrompt) {
|
||||
llmInput.unshift({
|
||||
role: "system",
|
||||
content: agent.systemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
return new AgentSetup({
|
||||
input: llmInput,
|
||||
currentAgentName: currentAgentName,
|
||||
});
|
||||
};
|
||||
|
||||
private runAgentStep = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
event: AgentSetup,
|
||||
): Promise<AgentStepEvent> => {
|
||||
const agent = this.agents.get(event.data.currentAgentName);
|
||||
if (!agent) {
|
||||
throw new Error("No valid agent found");
|
||||
}
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(
|
||||
`[Agent ${agent.name}]: Running for input: ${event.data.input[event.data.input.length - 1]?.content}`,
|
||||
);
|
||||
}
|
||||
|
||||
const output = await agent.takeStep(ctx, event.data.input, agent.tools);
|
||||
|
||||
ctx.sendEvent(output);
|
||||
|
||||
return new AgentStepEvent({
|
||||
agentName: agent.name,
|
||||
response: output.data.response,
|
||||
toolCalls: output.data.toolCalls,
|
||||
});
|
||||
};
|
||||
|
||||
private parseAgentOutput = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
event: AgentStepEvent,
|
||||
): Promise<ToolCallsEvent | StopEvent<{ result: string }>> => {
|
||||
const { agentName, response, toolCalls } = event.data;
|
||||
|
||||
// If no tool calls, return final response
|
||||
if (!toolCalls || toolCalls.length === 0) {
|
||||
if (this.verbose) {
|
||||
console.log(
|
||||
`[Agent ${agentName}]: No tool calls to process, returning final response`,
|
||||
);
|
||||
}
|
||||
const agentOutput = new AgentOutput({
|
||||
response,
|
||||
toolCalls: [],
|
||||
raw: response,
|
||||
currentAgentName: agentName,
|
||||
});
|
||||
const content = await this.agents
|
||||
.get(agentName)
|
||||
?.finalize(ctx, agentOutput, ctx.data.memory);
|
||||
|
||||
return new StopEvent({
|
||||
result: content?.data.response.content as string,
|
||||
});
|
||||
}
|
||||
|
||||
return new ToolCallsEvent({
|
||||
agentName,
|
||||
toolCalls,
|
||||
});
|
||||
};
|
||||
|
||||
private executeToolCalls = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
event: ToolCallsEvent,
|
||||
): Promise<ToolResultsEvent | StopEvent<{ result: string }>> => {
|
||||
const { agentName, toolCalls } = event.data;
|
||||
const agent = this.agents.get(agentName);
|
||||
if (!agent) {
|
||||
throw new Error(`Agent ${agentName} not found`);
|
||||
}
|
||||
|
||||
const results: AgentToolCallResult[] = [];
|
||||
|
||||
// Execute each tool call
|
||||
for (const toolCall of toolCalls) {
|
||||
// Send single tool call event, useful for UI
|
||||
ctx.sendEvent(toolCall);
|
||||
const toolResult = new AgentToolCallResult({
|
||||
toolName: toolCall.data.toolName,
|
||||
toolKwargs: toolCall.data.toolKwargs,
|
||||
toolId: toolCall.data.toolId,
|
||||
toolOutput: {
|
||||
id: toolCall.data.toolId,
|
||||
result: "",
|
||||
isError: false,
|
||||
},
|
||||
returnDirect: false,
|
||||
});
|
||||
try {
|
||||
const output = await this.callTool(toolCall, ctx);
|
||||
toolResult.data.toolOutput.result =
|
||||
stringifyJSONToMessageContent(output);
|
||||
toolResult.data.returnDirect = toolCall.data.toolName === "handOff";
|
||||
} catch (error) {
|
||||
toolResult.data.toolOutput.isError = true;
|
||||
toolResult.data.toolOutput.result = `Error: ${error}`;
|
||||
}
|
||||
results.push(toolResult);
|
||||
// Send single tool result event, useful for UI
|
||||
ctx.sendEvent(toolResult);
|
||||
}
|
||||
|
||||
return new ToolResultsEvent({
|
||||
agentName,
|
||||
results,
|
||||
});
|
||||
};
|
||||
|
||||
private processToolResults = async (
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
event: ToolResultsEvent,
|
||||
): Promise<AgentInput | StopEvent<{ result: string }>> => {
|
||||
const { agentName, results } = event.data;
|
||||
|
||||
// Get agent
|
||||
const agent = this.agents.get(agentName);
|
||||
if (!agent) {
|
||||
throw new Error(`Agent ${agentName} not found`);
|
||||
}
|
||||
|
||||
await agent.handleToolCallResults(ctx, results);
|
||||
|
||||
const directResult = results.find((r) => r.data.returnDirect);
|
||||
if (directResult) {
|
||||
const isHandoff = directResult.data.toolName === "handOff";
|
||||
|
||||
const output =
|
||||
typeof directResult.data.toolOutput.result === "string"
|
||||
? directResult.data.toolOutput.result
|
||||
: JSON.stringify(directResult.data.toolOutput.result);
|
||||
|
||||
const agentOutput = new AgentOutput({
|
||||
response: {
|
||||
role: "assistant" as const,
|
||||
content: output,
|
||||
},
|
||||
toolCalls: [],
|
||||
raw: output,
|
||||
currentAgentName: agent.name,
|
||||
});
|
||||
|
||||
await agent.finalize(ctx, agentOutput, ctx.data.memory);
|
||||
|
||||
if (isHandoff) {
|
||||
const nextAgentName = ctx.data.nextAgentName;
|
||||
console.log(
|
||||
`[Agent ${agentName}]: Handoff to ${nextAgentName}: ${directResult.data.toolOutput.result}`,
|
||||
);
|
||||
if (nextAgentName) {
|
||||
ctx.data.currentAgentName = nextAgentName;
|
||||
ctx.data.nextAgentName = null;
|
||||
|
||||
const messages = await ctx.data.memory.getMessages();
|
||||
return new AgentInput({
|
||||
input: messages,
|
||||
currentAgentName: nextAgentName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new StopEvent({
|
||||
result: output,
|
||||
});
|
||||
}
|
||||
|
||||
// Continue with another agent step
|
||||
const messages = await ctx.data.memory.getMessages();
|
||||
return new AgentInput({
|
||||
input: messages,
|
||||
currentAgentName: agent.name,
|
||||
});
|
||||
};
|
||||
|
||||
private setupWorkflowSteps() {
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [StartEvent<AgentInputData>],
|
||||
outputs: [AgentInput],
|
||||
},
|
||||
this.handleInputStep,
|
||||
);
|
||||
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [AgentInput],
|
||||
outputs: [AgentSetup],
|
||||
},
|
||||
this.setupAgent,
|
||||
);
|
||||
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [AgentSetup],
|
||||
outputs: [AgentStepEvent],
|
||||
},
|
||||
this.runAgentStep,
|
||||
);
|
||||
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [AgentStepEvent],
|
||||
outputs: [ToolCallsEvent, StopEvent],
|
||||
},
|
||||
this.parseAgentOutput,
|
||||
);
|
||||
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [ToolCallsEvent],
|
||||
outputs: [ToolResultsEvent, StopEvent],
|
||||
},
|
||||
this.executeToolCalls,
|
||||
);
|
||||
|
||||
this.workflow.addStep(
|
||||
{
|
||||
inputs: [ToolResultsEvent],
|
||||
outputs: [AgentInput, StopEvent],
|
||||
},
|
||||
this.processToolResults,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private callTool(
|
||||
toolCall: AgentToolCall,
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
) {
|
||||
const tool = this.agents
|
||||
.get(toolCall.data.agentName)
|
||||
?.tools.find((t) => t.metadata.name === toolCall.data.toolName);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool ${toolCall.data.toolName} not found`);
|
||||
}
|
||||
if (tool.metadata.requireContext) {
|
||||
const input = { context: ctx.data, ...toolCall.data.toolKwargs };
|
||||
return tool.call(input);
|
||||
} else {
|
||||
return tool.call(toolCall.data.toolKwargs);
|
||||
}
|
||||
}
|
||||
|
||||
run(
|
||||
userInput: string,
|
||||
params?: {
|
||||
chatHistory?: ChatMessage[];
|
||||
context?: AgentWorkflowContext;
|
||||
},
|
||||
): WorkflowContext<AgentInputData, string, AgentWorkflowContext> {
|
||||
if (this.agents.size === 0) {
|
||||
throw new Error("No agents added to workflow");
|
||||
}
|
||||
this.setupWorkflowSteps();
|
||||
const contextData: AgentWorkflowContext = params?.context ?? {
|
||||
userInput: userInput,
|
||||
memory: new ChatMemoryBuffer(),
|
||||
scratchpad: [],
|
||||
currentAgentName: this.rootAgentName,
|
||||
agents: Array.from(this.agents.keys()),
|
||||
nextAgentName: null,
|
||||
};
|
||||
|
||||
const result = this.workflow.run(
|
||||
{
|
||||
userInput: userInput,
|
||||
chatHistory: params?.chatHistory,
|
||||
},
|
||||
contextData,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const createHandoffTool = (agents: Map<string, BaseWorkflowAgent>) => {
|
||||
const agentInfo = Array.from(agents.values()).reduce(
|
||||
(acc, a) => {
|
||||
acc[a.name] = a.description;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
return FunctionTool.from(
|
||||
({
|
||||
context,
|
||||
toAgent,
|
||||
reason,
|
||||
}: {
|
||||
context?: AgentWorkflowContext;
|
||||
toAgent: string;
|
||||
reason: string;
|
||||
}) => {
|
||||
if (!context) {
|
||||
throw new Error("Context is required for handoff");
|
||||
}
|
||||
const agents = context.agents;
|
||||
if (!agents.includes(toAgent)) {
|
||||
return `Agent ${toAgent} not found. Select a valid agent to hand off to. Valid agents: ${agents.join(
|
||||
", ",
|
||||
)}`;
|
||||
}
|
||||
context.nextAgentName = toAgent;
|
||||
return DEFAULT_HANDOFF_OUTPUT_PROMPT.format({
|
||||
to_agent: toAgent,
|
||||
reason: reason,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "handOff",
|
||||
description: DEFAULT_HANDOFF_PROMPT.format({
|
||||
agent_info: JSON.stringify(agentInfo),
|
||||
}),
|
||||
parameters: z.object({
|
||||
toAgent: z.string({
|
||||
description: "The name of the agent to hand off to",
|
||||
}),
|
||||
reason: z.string({
|
||||
description: "The reason for handing off to the agent",
|
||||
}),
|
||||
}),
|
||||
requireContext: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { BaseToolWithCall, ChatMessage, LLM } from "@llamaindex/core/llms";
|
||||
import { BaseMemory } from "@llamaindex/core/memory";
|
||||
import type { HandlerContext } from "../workflow-context";
|
||||
import type { AgentOutput, AgentToolCallResult } from "./events";
|
||||
|
||||
export type AgentWorkflowContext = {
|
||||
userInput: string;
|
||||
memory: BaseMemory;
|
||||
scratchpad: ChatMessage[];
|
||||
agents: string[];
|
||||
currentAgentName: string;
|
||||
nextAgentName?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base interface for workflow agents
|
||||
*/
|
||||
export interface BaseWorkflowAgent {
|
||||
readonly name: string;
|
||||
readonly systemPrompt: string;
|
||||
readonly description: string;
|
||||
readonly tools: BaseToolWithCall[];
|
||||
readonly llm: LLM;
|
||||
readonly canHandoffTo: string[];
|
||||
|
||||
/**
|
||||
* Take a single step with the agent
|
||||
* Using memory directly to get messages instead of requiring them to be passed in
|
||||
*/
|
||||
takeStep(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
llmInput: ChatMessage[],
|
||||
tools: BaseToolWithCall[],
|
||||
): Promise<AgentOutput>;
|
||||
|
||||
/**
|
||||
* Handle results from tool calls
|
||||
*/
|
||||
handleToolCallResults(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
results: AgentToolCallResult[],
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Finalize the agent's output
|
||||
*/
|
||||
finalize(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
output: AgentOutput,
|
||||
memory: BaseMemory,
|
||||
): Promise<AgentOutput>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for creating an AgentWorkflow
|
||||
*/
|
||||
export interface AgentWorkflowParams {
|
||||
// Using strict typing for optional properties
|
||||
verbose?: boolean;
|
||||
timeout?: number;
|
||||
validate?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { JSONValue } from "@llamaindex/core/global";
|
||||
import type { ChatMessage, ToolResult } from "@llamaindex/core/llms";
|
||||
import { WorkflowEvent } from "../workflow-event";
|
||||
|
||||
export class AgentToolCall extends WorkflowEvent<{
|
||||
agentName: string;
|
||||
toolName: string;
|
||||
toolKwargs: Record<string, JSONValue>;
|
||||
toolId: string;
|
||||
}> {}
|
||||
|
||||
// TODO: Check for if we need a raw tool output
|
||||
export class AgentToolCallResult extends WorkflowEvent<{
|
||||
toolName: string;
|
||||
toolKwargs: Record<string, JSONValue>;
|
||||
toolId: string;
|
||||
toolOutput: ToolResult;
|
||||
returnDirect: boolean;
|
||||
}> {}
|
||||
|
||||
export class AgentInput extends WorkflowEvent<{
|
||||
input: ChatMessage[];
|
||||
currentAgentName: string;
|
||||
}> {}
|
||||
|
||||
export class AgentSetup extends WorkflowEvent<{
|
||||
input: ChatMessage[];
|
||||
currentAgentName: string;
|
||||
}> {}
|
||||
|
||||
export class AgentStream extends WorkflowEvent<{
|
||||
delta: string;
|
||||
response: string;
|
||||
currentAgentName: string;
|
||||
raw: unknown;
|
||||
}> {}
|
||||
|
||||
export class AgentOutput extends WorkflowEvent<{
|
||||
response: ChatMessage;
|
||||
toolCalls: AgentToolCall[];
|
||||
raw: unknown;
|
||||
currentAgentName: string;
|
||||
}> {}
|
||||
@@ -0,0 +1,227 @@
|
||||
import type { JSONObject } from "@llamaindex/core/global";
|
||||
import type {
|
||||
BaseToolWithCall,
|
||||
ChatMessage,
|
||||
ChatResponseChunk,
|
||||
ToolCallLLM,
|
||||
} from "@llamaindex/core/llms";
|
||||
import { BaseMemory } from "@llamaindex/core/memory";
|
||||
import type { HandlerContext } from "../workflow-context";
|
||||
import { type AgentWorkflowContext, type BaseWorkflowAgent } from "./base";
|
||||
import {
|
||||
AgentOutput,
|
||||
AgentStream,
|
||||
AgentToolCall,
|
||||
AgentToolCallResult,
|
||||
} from "./events";
|
||||
|
||||
const DEFAULT_SYSTEM_PROMPT =
|
||||
"You are a helpful assistant. Use the provided tools to answer questions.";
|
||||
|
||||
export type FunctionAgentParams = {
|
||||
name: string;
|
||||
/**
|
||||
* LLM to use for the agent, required.
|
||||
*/
|
||||
llm: ToolCallLLM;
|
||||
/**
|
||||
* Description of the agent, useful for task assignment.
|
||||
* Should provide the capabilities or responsibilities of the agent.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* List of tools that the agent can use, requires at least one tool.
|
||||
*/
|
||||
tools: BaseToolWithCall[];
|
||||
/**
|
||||
* List of agents that this agent can delegate tasks to
|
||||
*/
|
||||
canHandoffTo?: string[] | BaseWorkflowAgent[] | undefined;
|
||||
/**
|
||||
* Custom system prompt for the agent
|
||||
*/
|
||||
systemPrompt?: string | undefined;
|
||||
};
|
||||
|
||||
export class FunctionAgent implements BaseWorkflowAgent {
|
||||
readonly name: string;
|
||||
readonly systemPrompt: string;
|
||||
readonly description: string;
|
||||
readonly llm: ToolCallLLM;
|
||||
readonly tools: BaseToolWithCall[];
|
||||
readonly canHandoffTo: string[];
|
||||
|
||||
constructor({
|
||||
name,
|
||||
llm,
|
||||
description,
|
||||
tools,
|
||||
canHandoffTo,
|
||||
systemPrompt,
|
||||
}: FunctionAgentParams) {
|
||||
this.name = name;
|
||||
this.llm = llm;
|
||||
this.description = description;
|
||||
this.tools = tools;
|
||||
if (tools.length === 0) {
|
||||
throw new Error("FunctionAgent must have at least one tool");
|
||||
}
|
||||
this.canHandoffTo =
|
||||
Array.isArray(canHandoffTo) &&
|
||||
canHandoffTo.every((item) => typeof item === "string")
|
||||
? canHandoffTo
|
||||
: (canHandoffTo?.map((agent) =>
|
||||
typeof agent === "string" ? agent : agent.name,
|
||||
) ?? []);
|
||||
const uniqueHandoffAgents = new Set(this.canHandoffTo);
|
||||
if (uniqueHandoffAgents.size !== this.canHandoffTo.length) {
|
||||
throw new Error("Duplicate handoff agents");
|
||||
}
|
||||
this.systemPrompt = systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
async takeStep(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
llmInput: ChatMessage[],
|
||||
tools: BaseToolWithCall[],
|
||||
): Promise<AgentOutput> {
|
||||
// Get scratchpad from context or initialize if not present
|
||||
const scratchpad: ChatMessage[] = ctx.data.scratchpad;
|
||||
const currentLLMInput = [...llmInput, ...scratchpad];
|
||||
|
||||
const responseStream = await this.llm.chat({
|
||||
messages: currentLLMInput,
|
||||
tools,
|
||||
stream: true,
|
||||
});
|
||||
let response = "";
|
||||
let lastChunk: ChatResponseChunk | undefined;
|
||||
for await (const chunk of responseStream) {
|
||||
response += chunk.delta;
|
||||
ctx.sendEvent(
|
||||
new AgentStream({
|
||||
delta: chunk.delta,
|
||||
response: response,
|
||||
currentAgentName: this.name,
|
||||
raw: chunk.raw,
|
||||
}),
|
||||
);
|
||||
lastChunk = chunk;
|
||||
}
|
||||
|
||||
const message: ChatMessage = {
|
||||
role: "assistant" as const,
|
||||
content: response,
|
||||
};
|
||||
|
||||
const toolCalls = lastChunk
|
||||
? this.getToolCallFromResponseChunk(lastChunk)
|
||||
: [];
|
||||
if (toolCalls.length > 0) {
|
||||
message.options = {
|
||||
toolCall: toolCalls.map((toolCall) => ({
|
||||
name: toolCall.data.toolName,
|
||||
input: toolCall.data.toolKwargs,
|
||||
id: toolCall.data.toolId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
scratchpad.push(message);
|
||||
ctx.data.scratchpad = scratchpad;
|
||||
return new AgentOutput({
|
||||
response: message,
|
||||
toolCalls,
|
||||
raw: lastChunk?.raw,
|
||||
currentAgentName: this.name,
|
||||
});
|
||||
}
|
||||
|
||||
async handleToolCallResults(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
results: AgentToolCallResult[],
|
||||
): Promise<void> {
|
||||
const scratchpad: ChatMessage[] = ctx.data.scratchpad;
|
||||
|
||||
for (const result of results) {
|
||||
const content = result.data.toolOutput.result;
|
||||
|
||||
const rawToolMessage = {
|
||||
role: "user" as const,
|
||||
content,
|
||||
options: {
|
||||
toolResult: {
|
||||
id: result.data.toolId,
|
||||
result: content,
|
||||
isError: result.data.toolOutput.isError,
|
||||
},
|
||||
},
|
||||
};
|
||||
ctx.data.scratchpad.push(rawToolMessage);
|
||||
}
|
||||
|
||||
ctx.data.scratchpad = scratchpad;
|
||||
}
|
||||
|
||||
async finalize(
|
||||
ctx: HandlerContext<AgentWorkflowContext>,
|
||||
output: AgentOutput,
|
||||
memory: BaseMemory,
|
||||
): Promise<AgentOutput> {
|
||||
// Get scratchpad messages
|
||||
const scratchpad: ChatMessage[] = ctx.data.scratchpad;
|
||||
|
||||
for (const msg of scratchpad) {
|
||||
memory.put(msg);
|
||||
}
|
||||
|
||||
// Clear scratchpad after finalization
|
||||
ctx.data.scratchpad = [];
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private getToolCallFromResponseChunk(
|
||||
responseChunk: ChatResponseChunk,
|
||||
): AgentToolCall[] {
|
||||
const toolCalls: AgentToolCall[] = [];
|
||||
const options = responseChunk.options ?? {};
|
||||
if (options && "toolCall" in options && Array.isArray(options.toolCall)) {
|
||||
toolCalls.push(
|
||||
...options.toolCall.map((call) => {
|
||||
// Convert input to arguments format
|
||||
let toolKwargs: JSONObject;
|
||||
if (typeof call.input === "string") {
|
||||
try {
|
||||
toolKwargs = JSON.parse(call.input);
|
||||
} catch (e) {
|
||||
toolKwargs = { rawInput: call.input };
|
||||
}
|
||||
} else {
|
||||
toolKwargs = call.input as JSONObject;
|
||||
}
|
||||
|
||||
return new AgentToolCall({
|
||||
agentName: this.name,
|
||||
toolName: call.name,
|
||||
toolKwargs: toolKwargs,
|
||||
toolId: call.id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const invalidToolCalls = toolCalls.filter(
|
||||
(call) =>
|
||||
!this.tools.some((tool) => tool.metadata.name === call.data.toolName),
|
||||
);
|
||||
|
||||
if (invalidToolCalls.length > 0) {
|
||||
const invalidToolNames = invalidToolCalls
|
||||
.map((call) => call.data.toolName)
|
||||
.join(", ");
|
||||
throw new Error(`Tools not found: ${invalidToolNames}`);
|
||||
}
|
||||
|
||||
return toolCalls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export { AgentWorkflow } from "./agent-workflow";
|
||||
export type {
|
||||
AgentInputData,
|
||||
AgentStepEvent,
|
||||
AgentWorkflowParams,
|
||||
ToolCallsEvent,
|
||||
ToolResultsEvent,
|
||||
} from "./agent-workflow";
|
||||
|
||||
export {
|
||||
AgentInput,
|
||||
AgentOutput,
|
||||
AgentSetup,
|
||||
AgentStream,
|
||||
AgentToolCall,
|
||||
AgentToolCallResult,
|
||||
} from "./events";
|
||||
|
||||
export { FunctionAgent, type FunctionAgentParams } from "./function-agent";
|
||||
Generated
+13
@@ -733,6 +733,9 @@ importers:
|
||||
wikipedia:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.24.2
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.9.0
|
||||
@@ -1079,6 +1082,9 @@ importers:
|
||||
'@llamaindex/openai':
|
||||
specifier: workspace:*
|
||||
version: link:../providers/openai
|
||||
'@llamaindex/workflow':
|
||||
specifier: workspace:*
|
||||
version: link:../workflow
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.7
|
||||
version: 4.17.15
|
||||
@@ -1733,7 +1739,14 @@ importers:
|
||||
version: 5.7.2
|
||||
|
||||
packages/workflow:
|
||||
dependencies:
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.24.2
|
||||
devDependencies:
|
||||
'@llamaindex/core':
|
||||
specifier: workspace:*
|
||||
version: link:../core
|
||||
'@llamaindex/env':
|
||||
specifier: workspace:*
|
||||
version: link:../env
|
||||
|
||||
@@ -172,6 +172,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./packages/providers/cohere/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/providers/voyage-ai/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user