mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-16 07:14:29 -04:00
feat: use agent to handle a workflow step (#2014)
Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@llamaindex/doc": patch
|
||||
---
|
||||
|
||||
Add natural language agent page
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@llamaindex/workflow": patch
|
||||
---
|
||||
|
||||
add agentHandler to handle a workflow steps using natural language.
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Agents",
|
||||
"pages": ["tool", "agent_workflow", "workflows"]
|
||||
"pages": ["tool", "agent_workflow", "workflows", "natural_language_workflow"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: Define workflows using natural language
|
||||
---
|
||||
|
||||
When working with Workflows, you have to write code to handle an event in the workflow.
|
||||
Often, the logic of the handler is not too complex so that it can be expressed using natural language and executed by an LLM.
|
||||
Besides the instructions, we just need the expected result event of the step, possible tool calls and optionally other events that can be emitted.
|
||||
|
||||
## Usage
|
||||
|
||||
Let's take an example of a workflow that generates a joke, gets a critique for it, and then improves it.
|
||||
|
||||
### Define the events
|
||||
|
||||
First, we define the events for our workflow. We need one for writing the joke, one for critiquing it, and one for the final result:
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { zodEvent } from "@llamaindex/workflow";
|
||||
|
||||
const writeJokeSchema = z.object({
|
||||
description: z
|
||||
.string()
|
||||
.describe("The topic to write a joke or describe the joke to improve."),
|
||||
writtenJoke: z.optional(z.string()).describe("The written joke."),
|
||||
retriedTimes: z
|
||||
.number()
|
||||
.default(0)
|
||||
.describe(
|
||||
"The retried times for writing the joke. Always increase this from the input retriedTimes.",
|
||||
),
|
||||
});
|
||||
|
||||
const critiqueSchema = z.object({
|
||||
joke: z.string().describe("The joke to critique"),
|
||||
retriedTimes: z.number().describe("The retried times for writing the joke."),
|
||||
});
|
||||
|
||||
const finalResultSchema = z.object({
|
||||
joke: z.string().describe("The joke to critique"),
|
||||
critique: z.string().describe("The critique of the joke"),
|
||||
});
|
||||
|
||||
const writeJokeEvent = zodEvent(writeJokeSchema, {
|
||||
debugLabel: "writeJokeEvent",
|
||||
});
|
||||
const critiqueEvent = zodEvent(critiqueSchema, {
|
||||
debugLabel: "critiqueEvent",
|
||||
});
|
||||
const finalResultEvent = zodEvent(finalResultSchema, {
|
||||
debugLabel: "finalResultEvent",
|
||||
});
|
||||
```
|
||||
|
||||
Note that your natural language workflows the events need to be created by the `zodEvent` function passing the zod schema as an argument. The agent needs the schema of the event data to correctly generate events.
|
||||
Also, we need a `debugLabel` so the LLM can identify the event to emit in the workflow.
|
||||
|
||||
### Define the workflow
|
||||
|
||||
As usual you first create the workflow:
|
||||
|
||||
```typescript
|
||||
import { agentHandler, createWorkflow } from "@llamaindex/workflow";
|
||||
|
||||
const jokeFlow = createWorkflow();
|
||||
```
|
||||
|
||||
Then you need to handle the events. For the handlers, instead of code, you're now going to use natural language by calling the `agentHandler` function.
|
||||
|
||||
It only requires two parameters:
|
||||
- `instructions`: A prompt to guide the agent how to handle the steps.
|
||||
- `results`: The output events that the agent should return after handling the step.
|
||||
|
||||
Then you will have a simple code to handle the step:
|
||||
|
||||
```typescript
|
||||
jokeFlow.handle(
|
||||
[writeJokeEvent],
|
||||
agentHandler({
|
||||
instructions: `You are a joke writer. You are given a topic and you need to write a joke about it.`,
|
||||
results: [critiqueEvent],
|
||||
}),
|
||||
);
|
||||
|
||||
jokeFlow.handle(
|
||||
[critiqueEvent],
|
||||
agentHandler({
|
||||
instructions: `
|
||||
You are given a joke and you need to critique it. Follow the following guidelines:
|
||||
1. You have maximum 3 times to improve the joke.
|
||||
2. If the joke is not good, increase the retriedTimes, describe how to improve the joke and send a writeJokeEvent.
|
||||
3. If the joke is good, trigger the finalResultEvent event.
|
||||
`,
|
||||
results: [writeJokeEvent, finalResultEvent],
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
For advanced usage, you can add more functionality to `agentHandler` by using these parameters:
|
||||
- `events`: A list of additional events that the agent can emit to the workflow. E.g., your agent can emit a `uiEvent` to update the UI during the execution.
|
||||
- `tools`: A list of tools that the agent can use to handle the step. E.g., your agent can use a `search` tool to search the web.
|
||||
|
||||
You can find more code examples in the [examples](https://github.com/run-llama/LlamaIndexTS/tree/main/examples/agents/natural) folder.
|
||||
@@ -0,0 +1,130 @@
|
||||
import { ToolCallLLM } from "llamaindex";
|
||||
|
||||
import {
|
||||
agentHandler,
|
||||
createWorkflow,
|
||||
workflowEvent,
|
||||
zodEvent,
|
||||
} from "@llamaindex/workflow";
|
||||
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { z } from "zod";
|
||||
|
||||
// ===== 1. Define events =====
|
||||
// An event to trigger the workflow
|
||||
const planEvent = workflowEvent<{ topic: string }>();
|
||||
|
||||
// Generate artifact event
|
||||
const ArtifactRequirementSchema = z.object({
|
||||
type: z.literal("markdown"),
|
||||
title: z.string().describe("The title of the artifact."),
|
||||
requirement: z
|
||||
.string()
|
||||
.describe("The requirement for the artifact generation."),
|
||||
});
|
||||
|
||||
const generateArtifactEvent = zodEvent(ArtifactRequirementSchema, {
|
||||
debugLabel: "generateArtifactEvent",
|
||||
});
|
||||
|
||||
// Artifact output event
|
||||
const ArtifactSchema = z.object({
|
||||
type: z.literal("artifact"),
|
||||
data: z.object({
|
||||
type: z.literal("document"),
|
||||
data: z.object({
|
||||
title: z.string().describe("The title of the data."),
|
||||
content: z.string().describe("The content of the data."),
|
||||
type: z.enum(["markdown", "html"]).describe("The type of the data."),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const outputArtifactEvent = zodEvent(ArtifactSchema, {
|
||||
debugLabel: "outputArtifactEvent",
|
||||
});
|
||||
|
||||
// Events for updating UI
|
||||
// assume that we have a UI that can render different states of the workflow
|
||||
// and update the UI based on the state and the requirement
|
||||
export const UIEventSchema = z.object({
|
||||
type: z.literal("ui_event"),
|
||||
data: z.object({
|
||||
state: z
|
||||
.enum(["plan", "generate", "completed"])
|
||||
.describe("The current state of the workflow."),
|
||||
requirement: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"An optional requirement creating or updating a document, if applicable.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
const uiEvent = zodEvent(UIEventSchema, { debugLabel: "uiEvent" });
|
||||
|
||||
// ===== 2. Define workflow with agents using natural language =====
|
||||
// We have a document artifact workflow that made up of 2 steps:
|
||||
// 1. Generate requirement for the document
|
||||
// 2. Generate document content based on the requirement
|
||||
export function createDocumentArtifactWorkflow(llm: ToolCallLLM) {
|
||||
const workflow = createWorkflow();
|
||||
|
||||
// Generate requirement for the document
|
||||
workflow.handle(
|
||||
[planEvent],
|
||||
agentHandler({
|
||||
instructions: `
|
||||
Your task is to analyze the request and provide requirements for document generation or update.
|
||||
1. Send an uiEvent with the \`plan\` to show UI what you are going to do.
|
||||
2. Analyze the conversation history and the user's request carefully to determine the completed tasks and the next steps.
|
||||
3. Return the generateArtifactEvent with the requirement for the next step of the document generation or update.
|
||||
`,
|
||||
results: [generateArtifactEvent],
|
||||
events: [uiEvent],
|
||||
llm,
|
||||
}),
|
||||
);
|
||||
|
||||
// Generate document content based on the requirement
|
||||
workflow.handle(
|
||||
[generateArtifactEvent],
|
||||
agentHandler({
|
||||
instructions: `
|
||||
You are a skilled technical writer who can assist users with documentation.
|
||||
Your task is to generate document content based on the requirement and update the UI state.
|
||||
|
||||
Here are the steps to handle this task:
|
||||
1. First, send an uiEvent with the \`generate\` state and the requirement you received from the input.
|
||||
2. Next, start generating the content based on the requirement then send an uiEvent with the \`completed\` state to update the state.
|
||||
3. Finally, return the outputArtifactEvent with the document values.
|
||||
`,
|
||||
results: [outputArtifactEvent],
|
||||
events: [uiEvent],
|
||||
llm,
|
||||
}),
|
||||
);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const llm = openai({ model: "gpt-4.1-mini" });
|
||||
const workflow = createDocumentArtifactWorkflow(llm);
|
||||
const { stream, sendEvent } = workflow.createContext();
|
||||
|
||||
// Ask the workflow to generate a document about `llama`
|
||||
sendEvent(planEvent.with({ topic: "llama" }));
|
||||
|
||||
await stream.until(outputArtifactEvent).forEach((event) => {
|
||||
if (planEvent.include(event)) {
|
||||
console.log("Starting workflow: ", event.data);
|
||||
}
|
||||
if (uiEvent.include(event)) {
|
||||
console.log("UI event: ", event.data);
|
||||
} else if (outputArtifactEvent.include(event)) {
|
||||
console.log("Output artifact event: ", event.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Settings } from "@llamaindex/core/global";
|
||||
import { openai } from "@llamaindex/openai";
|
||||
import { agentHandler, createWorkflow, zodEvent } from "@llamaindex/workflow";
|
||||
import { z } from "zod";
|
||||
|
||||
// Create LLM instance
|
||||
const llm = openai({ model: "gpt-4.1-mini" });
|
||||
Settings.llm = llm;
|
||||
|
||||
// Define our workflow events
|
||||
const writeJokeSchema = z.object({
|
||||
description: z
|
||||
.string()
|
||||
.describe("The topic to write a joke or describe the joke to improve."),
|
||||
writtenJoke: z.optional(z.string()).describe("The written joke."),
|
||||
retriedTimes: z
|
||||
.number()
|
||||
.default(0)
|
||||
.describe(
|
||||
"The retried times for writing the joke. Always increase this from the input retriedTimes.",
|
||||
),
|
||||
});
|
||||
|
||||
const critiqueSchema = z.object({
|
||||
joke: z.string().describe("The joke to critique"),
|
||||
retriedTimes: z.number().describe("The retried times for writing the joke."),
|
||||
});
|
||||
|
||||
const finalResultSchema = z.object({
|
||||
joke: z.string().describe("The joke to critique"),
|
||||
critique: z.string().describe("The critique of the joke"),
|
||||
});
|
||||
|
||||
const writeJokeEvent = zodEvent(writeJokeSchema, {
|
||||
debugLabel: "writeJokeEvent",
|
||||
}); // Input topic for writing a joke
|
||||
const critiqueEvent = zodEvent(critiqueSchema, {
|
||||
debugLabel: "critiqueEvent",
|
||||
}); // Ask for critique of the joke
|
||||
const finalResultEvent = zodEvent(finalResultSchema, {
|
||||
debugLabel: "finalResultEvent",
|
||||
}); // Final result
|
||||
|
||||
// Create our workflow
|
||||
const jokeFlow = createWorkflow();
|
||||
|
||||
// Define handlers for each step
|
||||
// This step always write a joke based on the description
|
||||
jokeFlow.handle(
|
||||
[writeJokeEvent],
|
||||
agentHandler({
|
||||
instructions: `You are a joke writer. You are given a topic and you need to write a joke about it.`,
|
||||
results: [critiqueEvent],
|
||||
}),
|
||||
);
|
||||
|
||||
// This step critiques the joke and asks the writer to improve the joke or send a final result event for stopping.
|
||||
jokeFlow.handle(
|
||||
[critiqueEvent],
|
||||
agentHandler({
|
||||
instructions: `
|
||||
You are given a joke and you need to critique it. Follow the following guidelines:
|
||||
1. You have maximum 3 times to improve the joke.
|
||||
2. If the joke is not good, increase the retriedTimes, describe how to improve the joke and send a writeJokeEvent.
|
||||
3. If the joke is good, trigger the finalResultEvent event.
|
||||
`,
|
||||
results: [writeJokeEvent, finalResultEvent],
|
||||
}),
|
||||
);
|
||||
|
||||
// Usage
|
||||
async function main() {
|
||||
const { stream, sendEvent } = jokeFlow.createContext();
|
||||
sendEvent(writeJokeEvent.with({ description: "write a joke about llama" }));
|
||||
|
||||
await stream.until(finalResultEvent).forEach((event) => {
|
||||
if (writeJokeEvent.include(event)) {
|
||||
console.log(
|
||||
"Triggering write joke: ",
|
||||
JSON.stringify(event.data, null, 2),
|
||||
);
|
||||
} else if (critiqueEvent.include(event)) {
|
||||
console.log("Written joke: ", JSON.stringify(event.data, null, 2));
|
||||
} else if (finalResultEvent.include(event)) {
|
||||
console.log("Output: ", JSON.stringify(event.data, null, 2));
|
||||
} else {
|
||||
console.log("Unknown event: ", JSON.stringify(event.data, null, 2));
|
||||
}
|
||||
});
|
||||
console.log("Done");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -45,9 +45,10 @@
|
||||
"peerDependencies": {
|
||||
"@llamaindex/core": "workspace:*",
|
||||
"@llamaindex/env": "workspace:*",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@llama-flow/core": "^0.4.3"
|
||||
"@llama-flow/core": "^0.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { getContext, type WorkflowEventData } from "@llama-flow/core";
|
||||
import {
|
||||
AgentWorkflow,
|
||||
startAgentEvent,
|
||||
stopAgentEvent,
|
||||
} from "./agent-workflow";
|
||||
import { agentToolCallEvent, type AgentToolCall } from "./events";
|
||||
import {
|
||||
FunctionAgent,
|
||||
type StepHandlerParams,
|
||||
type ZodEvent,
|
||||
} from "./function-agent";
|
||||
|
||||
async function handleWorkflowStep(
|
||||
workflow: AgentWorkflow,
|
||||
event: WorkflowEventData<unknown>,
|
||||
results: ZodEvent[],
|
||||
) {
|
||||
const agent = workflow.getAgents()[0];
|
||||
if (!agent) {
|
||||
throw new Error("No valid agent found");
|
||||
}
|
||||
|
||||
const { sendEvent, stream } = workflow.createContext();
|
||||
sendEvent(
|
||||
startAgentEvent.with({
|
||||
userInput: "Handle with this input data: " + JSON.stringify(event.data),
|
||||
}),
|
||||
);
|
||||
|
||||
const agentEvents = await stream.until(stopAgentEvent).toArray();
|
||||
checkAgentSentResultEvents(agentEvents, results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single agent to handle a workflow step
|
||||
* @param params - Parameters for the step handler
|
||||
* @returns A new AgentWorkflow instance
|
||||
*/
|
||||
function createWorkflowForStepHandler(
|
||||
params: StepHandlerParams,
|
||||
): AgentWorkflow {
|
||||
if (!params.workflowContext) {
|
||||
throw new Error("workflowContext must be provided");
|
||||
}
|
||||
if (!params.results) {
|
||||
throw new Error("results must have at least one event");
|
||||
}
|
||||
if (!params.instructions) {
|
||||
throw new Error("instructions must be provided");
|
||||
}
|
||||
const agent = FunctionAgent.fromWorkflowStep({
|
||||
workflowContext: params.workflowContext,
|
||||
results: params.results,
|
||||
events: params.events ?? [],
|
||||
instructions: params.instructions,
|
||||
tools: params.tools,
|
||||
llm: params.llm,
|
||||
});
|
||||
return new AgentWorkflow({
|
||||
agents: [agent],
|
||||
rootAgent: agent,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an agent handler to the workflow
|
||||
* @param params - Parameters for the agent handler
|
||||
* @returns A function that handles a workflow step
|
||||
*/
|
||||
export const agentHandler = (
|
||||
params: Omit<StepHandlerParams, "workflowContext">,
|
||||
) => {
|
||||
return async (event: WorkflowEventData<unknown>) => {
|
||||
const context = getContext();
|
||||
|
||||
const workflow = createWorkflowForStepHandler({
|
||||
...params,
|
||||
workflowContext: context,
|
||||
});
|
||||
await handleWorkflowStep(workflow, event, params.results);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the agent already sent at least one result event
|
||||
* @param agentEvents - Agent workflow events
|
||||
* @param results - The result events that the agent should send
|
||||
* @returns True if the agent already sent at least one result event or throw an error if the agent finished without sending a result event
|
||||
*/
|
||||
const checkAgentSentResultEvents = (
|
||||
agentEvents: WorkflowEventData<unknown>[],
|
||||
results: ZodEvent[],
|
||||
) => {
|
||||
// We cannot check the result event directly because it's not sent to the agent workflow
|
||||
// instead, we check for the tool call event to see if there is a tool call event that match with result events
|
||||
const toolCallEvents = agentEvents.filter((event) =>
|
||||
agentToolCallEvent.include(event),
|
||||
);
|
||||
const resultToolNames = new Set(results.map((r) => `send_${r.debugLabel}`));
|
||||
for (const toolCallEvent of toolCallEvents) {
|
||||
const toolCall = toolCallEvent.data as AgentToolCall;
|
||||
if (resultToolNames.has(toolCall.toolName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
"The agent finished without emitting a required result event.",
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkflowContext } from "@llama-flow/core";
|
||||
import { type WorkflowContext, type WorkflowEvent } from "@llama-flow/core";
|
||||
import type { JSONObject } from "@llamaindex/core/global";
|
||||
import { Settings } from "@llamaindex/core/global";
|
||||
import {
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
type ChatMessage,
|
||||
type ChatResponseChunk,
|
||||
} from "@llamaindex/core/llms";
|
||||
import { tool } from "@llamaindex/core/tools";
|
||||
import { z } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { AgentWorkflow } from "./agent-workflow";
|
||||
import { type AgentWorkflowState, type BaseWorkflowAgent } from "./base";
|
||||
import {
|
||||
@@ -19,6 +22,51 @@ import {
|
||||
const DEFAULT_SYSTEM_PROMPT =
|
||||
"You are a helpful assistant. Use the provided tools to answer questions.";
|
||||
|
||||
const STEP_HANDLER_SYSTEM_PROMPT_TPL = `
|
||||
You are a part of a workflow program that is composed of multiple steps.
|
||||
Your task is to handle the step using the provided tools and finally send an output event back to the workflow and summarize the result.
|
||||
|
||||
## Instructions
|
||||
### Follow these default instructions:
|
||||
1. Provide a plan to handle the actions based on context and the user request.
|
||||
2. Use the provided tools to proceed with your actions. You can call multiple tools to handle the step and send events.
|
||||
3. Always return at least one result event at the end to the workflow by using the provided result tools. Identify the event to send based on the user's instructions.
|
||||
|
||||
### Here is the user's instructions:
|
||||
{instructions}
|
||||
`;
|
||||
|
||||
export type ZodEvent = WorkflowEvent<unknown> & {
|
||||
schema: z.ZodType<unknown>;
|
||||
};
|
||||
|
||||
export type StepHandlerParams = {
|
||||
/**
|
||||
* Workflow context
|
||||
*/
|
||||
workflowContext: WorkflowContext;
|
||||
/**
|
||||
* User instructions to guide the agent to handle the step.
|
||||
*/
|
||||
instructions: string;
|
||||
/**
|
||||
* Event that this agent will return
|
||||
*/
|
||||
results: ZodEvent[];
|
||||
/**
|
||||
* List of additional events that the agent can emit
|
||||
*/
|
||||
events?: ZodEvent[];
|
||||
/**
|
||||
* LLM to use for the agent, required.
|
||||
*/
|
||||
llm?: ToolCallLLM | undefined;
|
||||
/**
|
||||
* List of tools that the agent can use
|
||||
*/
|
||||
tools?: BaseToolWithCall[] | undefined;
|
||||
};
|
||||
|
||||
export type FunctionAgentParams = {
|
||||
/**
|
||||
* Agent name
|
||||
@@ -36,7 +84,7 @@ export type FunctionAgentParams = {
|
||||
/**
|
||||
* List of tools that the agent can use, requires at least one tool.
|
||||
*/
|
||||
tools: BaseToolWithCall[];
|
||||
tools?: BaseToolWithCall[] | undefined;
|
||||
/**
|
||||
* List of agents that this agent can delegate tasks to
|
||||
* Can be a list of agent names as strings, BaseWorkflowAgent instances, or AgentWorkflow instances
|
||||
@@ -48,6 +96,11 @@ export type FunctionAgentParams = {
|
||||
systemPrompt?: string | undefined;
|
||||
};
|
||||
|
||||
export type EmitEvent = {
|
||||
event: WorkflowEvent<unknown> & { schema: z.ZodType<unknown> };
|
||||
name: string;
|
||||
};
|
||||
|
||||
export class FunctionAgent implements BaseWorkflowAgent {
|
||||
readonly name: string;
|
||||
readonly systemPrompt: string;
|
||||
@@ -72,40 +125,43 @@ export class FunctionAgent implements BaseWorkflowAgent {
|
||||
this.description =
|
||||
description ??
|
||||
"A single agent that uses the provided tools or functions.";
|
||||
this.tools = tools;
|
||||
if (tools.length === 0) {
|
||||
throw new Error("FunctionAgent must have at least one tool");
|
||||
}
|
||||
// Process canHandoffTo to extract agent names
|
||||
this.canHandoffTo = [];
|
||||
if (canHandoffTo) {
|
||||
if (Array.isArray(canHandoffTo)) {
|
||||
if (canHandoffTo.length > 0) {
|
||||
if (typeof canHandoffTo[0] === "string") {
|
||||
this.tools = tools ?? [];
|
||||
this.systemPrompt = systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
this.canHandoffTo = this.initHandOffNames(canHandoffTo ?? []);
|
||||
}
|
||||
|
||||
private initHandOffNames(
|
||||
handoffTo: string[] | BaseWorkflowAgent[] | AgentWorkflow[],
|
||||
): string[] {
|
||||
const handoffToNames: string[] = [];
|
||||
if (handoffTo) {
|
||||
if (Array.isArray(handoffTo)) {
|
||||
if (handoffTo.length > 0) {
|
||||
if (typeof handoffTo[0] === "string") {
|
||||
// string[] case
|
||||
this.canHandoffTo = canHandoffTo as string[];
|
||||
} else if (canHandoffTo[0] instanceof AgentWorkflow) {
|
||||
handoffToNames.push(...(handoffTo as string[]));
|
||||
} else if (handoffTo[0] instanceof AgentWorkflow) {
|
||||
// AgentWorkflow[] case
|
||||
const workflows = canHandoffTo as AgentWorkflow[];
|
||||
const workflows = handoffTo as AgentWorkflow[];
|
||||
workflows.forEach((workflow) => {
|
||||
const agentNames = workflow
|
||||
.getAgents()
|
||||
.map((agent) => agent.name);
|
||||
this.canHandoffTo.push(...agentNames);
|
||||
handoffToNames.push(...agentNames);
|
||||
});
|
||||
} else {
|
||||
// BaseWorkflowAgent[] case
|
||||
const agents = canHandoffTo as BaseWorkflowAgent[];
|
||||
this.canHandoffTo = agents.map((agent) => agent.name);
|
||||
const agents = handoffTo as BaseWorkflowAgent[];
|
||||
handoffToNames.push(...agents.map((agent) => agent.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const uniqueHandoffAgents = new Set(this.canHandoffTo);
|
||||
if (uniqueHandoffAgents.size !== this.canHandoffTo.length) {
|
||||
const uniqueHandoffAgents = new Set(handoffToNames);
|
||||
if (uniqueHandoffAgents.size !== handoffToNames.length) {
|
||||
throw new Error("Duplicate handoff agents");
|
||||
}
|
||||
this.systemPrompt = systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
||||
return handoffToNames;
|
||||
}
|
||||
|
||||
async takeStep(
|
||||
@@ -256,4 +312,125 @@ export class FunctionAgent implements BaseWorkflowAgent {
|
||||
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FunctionAgent to handle a step of the workflow.
|
||||
* @param params.workflowContext - The workflow context.
|
||||
* @param params.result - The event to send when the agent is done.
|
||||
* @param params.events - Additional events that the agent can emit.
|
||||
* @param params.instructions - The user instructions to guide the agent to handle the step.
|
||||
* @param params.tools - The tools to use for the agent.
|
||||
* @returns A new FunctionAgent instance
|
||||
*/
|
||||
static fromWorkflowStep({
|
||||
workflowContext,
|
||||
results,
|
||||
events,
|
||||
instructions,
|
||||
tools,
|
||||
llm,
|
||||
}: StepHandlerParams): FunctionAgent {
|
||||
if (!workflowContext) {
|
||||
throw new Error("workflowContext must be provided");
|
||||
}
|
||||
if (results.length === 0) {
|
||||
throw new Error("results must have at least one event");
|
||||
}
|
||||
if (!instructions) {
|
||||
throw new Error("instructions must be provided");
|
||||
}
|
||||
|
||||
// Provided tools
|
||||
const allTools = [...(tools ?? [])];
|
||||
// Add tools for result events
|
||||
results.forEach((result) => {
|
||||
if (!result.debugLabel) {
|
||||
throw new Error("Result event must have a debug label");
|
||||
}
|
||||
const description = `Use this tool to send the ${result.debugLabel} event as the final result of your task.`;
|
||||
allTools.push(
|
||||
createEventEmitterTool(
|
||||
`send_${result.debugLabel}`,
|
||||
result,
|
||||
workflowContext,
|
||||
description,
|
||||
),
|
||||
);
|
||||
});
|
||||
// Add tools for additional events
|
||||
events?.forEach((event) => {
|
||||
if (!event.debugLabel) {
|
||||
throw new Error("Event must have a debug label");
|
||||
}
|
||||
allTools.push(
|
||||
createEventEmitterTool(
|
||||
`send_${event.debugLabel}`,
|
||||
event,
|
||||
workflowContext,
|
||||
`Use this tool to send the ${event.debugLabel} event to the workflow program.`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Construct the system prompt
|
||||
const newSystemPrompt = STEP_HANDLER_SYSTEM_PROMPT_TPL.replace(
|
||||
"{instructions}",
|
||||
instructions,
|
||||
);
|
||||
|
||||
// Check if llm is provided or default LLM is a tool call LLM
|
||||
const llmToUse = llm ?? (Settings.llm as ToolCallLLM);
|
||||
if (!llmToUse.supportToolCall) {
|
||||
throw new Error("LLM must support tool calls");
|
||||
}
|
||||
// Create the function agent
|
||||
return new FunctionAgent({
|
||||
llm: llmToUse,
|
||||
systemPrompt: newSystemPrompt,
|
||||
tools: allTools,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tool that sends an event to the workflow.
|
||||
* @param name - The name of the tool.
|
||||
* @param event - The event to send.
|
||||
* @param workflowContext - The workflow context.
|
||||
* @param description - The description of the tool.
|
||||
*/
|
||||
const createEventEmitterTool = (
|
||||
name: string,
|
||||
event: WorkflowEvent<unknown> & { schema: z.ZodType<unknown> },
|
||||
workflowContext: WorkflowContext,
|
||||
description?: string,
|
||||
) => {
|
||||
// To ensure the model correctly interprets the event data, including the schema in the tool description is crucial.
|
||||
// This is particularly important for special types like literals and enums, which the model might struggle with otherwise.
|
||||
// By incorporating the schema into the tool description, we can facilitate the model's understanding of the event data.
|
||||
const toolDescriptionWithSchema =
|
||||
(description ??
|
||||
event.schema.description ??
|
||||
"Use this tool to send the event to the workflow.") +
|
||||
`\n\nPlease provide the event data in the following JSON schema: ${JSON.stringify(
|
||||
zodToJsonSchema(z.object({ eventData: event.schema })),
|
||||
)}`;
|
||||
return tool({
|
||||
name: name,
|
||||
description: toolDescriptionWithSchema,
|
||||
parameters: z.object({
|
||||
eventData: event.schema,
|
||||
}),
|
||||
execute: (
|
||||
{ eventData }: { eventData?: z.infer<typeof event.schema> },
|
||||
getContext?: () => WorkflowContext,
|
||||
) => {
|
||||
if (!getContext) {
|
||||
throw new Error("Workflow context is not provided.");
|
||||
}
|
||||
const context = getContext();
|
||||
context.sendEvent(event.with(eventData ?? {}));
|
||||
return `Successfully sent a ${name} event!`;
|
||||
},
|
||||
}).bind(() => workflowContext);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./agent-handler";
|
||||
export * from "./agent-workflow";
|
||||
export * from "./base";
|
||||
export * from "./events";
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from "@llama-flow/core";
|
||||
export * from "@llama-flow/core/middleware/snapshot";
|
||||
export * from "@llama-flow/core/middleware/state";
|
||||
export * from "@llama-flow/core/stream/run";
|
||||
export { zodEvent } from "@llama-flow/core/util/zod";
|
||||
export * from "./agent/index.js";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { type WorkflowContext } from "@llama-flow/core";
|
||||
import { zodEvent } from "@llama-flow/core/util/zod";
|
||||
import { ChatMessage } from "@llamaindex/core/llms";
|
||||
import { tool } from "@llamaindex/core/tools";
|
||||
import { MockLLM } from "@llamaindex/core/utils";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { AgentToolCallResult, FunctionAgent } from "../src/agent";
|
||||
|
||||
@@ -59,4 +61,64 @@ describe("FunctionAgent", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("should be initialized with correct tools", () => {
|
||||
// Mock WorkflowContext
|
||||
const mockWorkflowContext = {
|
||||
sendEvent: vi.fn(),
|
||||
} as unknown as WorkflowContext;
|
||||
|
||||
// Create a regular tool
|
||||
const addTool = tool({
|
||||
name: "add",
|
||||
description: "Adds two numbers",
|
||||
parameters: z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
}),
|
||||
execute: (params: { x: number; y: number }) => params.x + params.y,
|
||||
});
|
||||
|
||||
// Create a result event
|
||||
const resultEvent = zodEvent(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
}),
|
||||
{
|
||||
debugLabel: "my_result_event",
|
||||
},
|
||||
);
|
||||
|
||||
// Create an additional event
|
||||
const additionalEvent = zodEvent(
|
||||
z.object({
|
||||
value: z.number(),
|
||||
}),
|
||||
{
|
||||
debugLabel: "additional_event",
|
||||
},
|
||||
);
|
||||
|
||||
// Create the FunctionAgent using fromWorkflowStep
|
||||
const agent = FunctionAgent.fromWorkflowStep({
|
||||
workflowContext: mockWorkflowContext,
|
||||
results: [resultEvent],
|
||||
events: [additionalEvent],
|
||||
instructions: "Test instructions",
|
||||
tools: [addTool],
|
||||
llm: mockLLM,
|
||||
});
|
||||
|
||||
// Check if the agent is created
|
||||
expect(agent).toBeInstanceOf(FunctionAgent);
|
||||
|
||||
// Check the number of tools
|
||||
expect(agent.tools.length).toBe(3);
|
||||
|
||||
// Check the names of the tools
|
||||
const toolNames = agent.tools.map((t) => t.metadata.name);
|
||||
expect(toolNames).toContain("add");
|
||||
expect(toolNames).toContain("send_my_result_event");
|
||||
expect(toolNames).toContain("send_additional_event");
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+8
-5
@@ -1808,11 +1808,14 @@ importers:
|
||||
packages/workflow:
|
||||
dependencies:
|
||||
'@llama-flow/core':
|
||||
specifier: ^0.4.3
|
||||
version: 0.4.3(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.24.2
|
||||
zod-to-json-schema:
|
||||
specifier: ^3.23.3
|
||||
version: 3.24.5(zod@3.24.2)
|
||||
devDependencies:
|
||||
'@llamaindex/core':
|
||||
specifier: workspace:*
|
||||
@@ -3947,8 +3950,8 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@llama-flow/core@0.4.3':
|
||||
resolution: {integrity: sha512-fxvuCO0Jpa/WZ/NFk2HCpKwjE4bwpHAIFyW/MC0L+gKGKj8DMYjJla6WVhADU35EUnxdpf9ZYJoN32yPrJUMMQ==}
|
||||
'@llama-flow/core@0.4.4':
|
||||
resolution: {integrity: sha512-hwK1EQ+atUG/E7XcDV3KsTaA8op29pb8gbpVurpsqbLnGFkdTT4F/6V7Hy1cC2o/yOY+DKc/rxoIsH1uJS0cZg==}
|
||||
peerDependencies:
|
||||
'@modelcontextprotocol/sdk': ^1.7.0
|
||||
hono: ^4.7.4
|
||||
@@ -16607,7 +16610,7 @@ snapshots:
|
||||
p-retry: 6.2.1
|
||||
zod: 3.25.7
|
||||
|
||||
'@llama-flow/core@0.4.3(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)':
|
||||
'@llama-flow/core@0.4.4(@modelcontextprotocol/sdk@1.9.0)(hono@4.7.7)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(p-retry@6.2.1)(zod@3.24.2)':
|
||||
optionalDependencies:
|
||||
'@modelcontextprotocol/sdk': 1.9.0
|
||||
hono: 4.7.7
|
||||
|
||||
Reference in New Issue
Block a user