Compare commits

...

5 Commits

Author SHA1 Message Date
leehuwuj ffe8919cde test 2025-03-04 17:08:02 +07:00
Huu Le 081698d68c chore: simplify imports of agent workflow (#1700) 2025-03-04 17:01:29 +07:00
Huu Le ab5fe5d7a0 chore: remove core import in document (#1699) 2025-03-04 16:14:31 +07:00
Huu Le 56689707d3 feat: Support AgentWorkflow (#1685)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
2025-03-04 16:05:25 +07:00
Brian Lange fd74ba4bf1 fix: Voyage typescript configs + docs (#1696) 2025-03-04 11:00:05 +07:00
22 changed files with 1374 additions and 6 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@llamaindex/core": patch
"@llamaindex/workflow": patch
---
feat: Support AgentWorkflow
+5
View File
@@ -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.
+83
View File
@@ -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);
});
+110
View File
@@ -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);
});
+37
View File
@@ -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);
});
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -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
View File
@@ -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",
+4
View File
@@ -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;
};
/**
+15 -1
View File
@@ -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);
};
}
+1
View File
@@ -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",
+1
View File
@@ -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";
+1 -1
View File
@@ -10,7 +10,7 @@
"include": ["./src"],
"references": [
{
"path": "../openai/tsconfig.json"
"path": "../../core/tsconfig.json"
},
{
"path": "../../env/tsconfig.json"
+32 -1
View File
@@ -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,
},
);
};
+62
View File
@@ -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;
}
+43
View File
@@ -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;
}
}
+19
View File
@@ -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";
+13
View File
@@ -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
+3
View File
@@ -172,6 +172,9 @@
},
{
"path": "./packages/providers/cohere/tsconfig.json"
},
{
"path": "./packages/providers/voyage-ai/tsconfig.json"
}
]
}