-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MultiAgentWorkflow #17237
base: main
Are you sure you want to change the base?
MultiAgentWorkflow #17237
Changes from all commits
ff9afa6
3f2574f
c992dd9
969b64a
ae072b8
be035d0
6974a9d
d91f93e
85b89af
faabe0d
e9fde68
81efcec
2d47771
ce758fb
4718dce
f2f2a1e
9151673
935e4ae
f527a05
d94d22f
347bda1
aa555a2
ee70508
0027c3a
9a89262
c63849f
96e458c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
::: llama_index.core.agent.workflow | ||
options: | ||
members: | ||
- MultiAgentWorkflow | ||
- BaseWorkflowAgent | ||
- FunctionAgent | ||
- ReactAgent | ||
- AgentInput | ||
- AgentStream | ||
- AgentOutput | ||
- ToolCall | ||
- ToolCallResult |
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,257 @@ | ||||||||||||||||
# Multi-Agent Workflows | ||||||||||||||||
|
||||||||||||||||
The MultiAgentWorkflow uses Workflow Agents to allow you to create a system of multiple agents that can collaborate and hand off tasks to each other based on their specialized capabilities. This enables building more complex agent systems where different agents handle different aspects of a task. | ||||||||||||||||
|
||||||||||||||||
## Quick Start | ||||||||||||||||
|
||||||||||||||||
Here's a simple example of setting up a multi-agent workflow with a calculator agent and a retriever agent: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
from llama_index.core.agent.workflow import ( | ||||||||||||||||
MultiAgentWorkflow, | ||||||||||||||||
FunctionAgent, | ||||||||||||||||
ReactAgent, | ||||||||||||||||
) | ||||||||||||||||
from llama_index.core.tools import FunctionTool | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
# Define some tools | ||||||||||||||||
def add(a: int, b: int) -> int: | ||||||||||||||||
"""Add two numbers.""" | ||||||||||||||||
return a + b | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
def subtract(a: int, b: int) -> int: | ||||||||||||||||
"""Subtract two numbers.""" | ||||||||||||||||
return a - b | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
# Create agent configs | ||||||||||||||||
# NOTE: we can use FunctionAgent or ReactAgent here. | ||||||||||||||||
# FunctionAgent works for LLMs with a function calling API. | ||||||||||||||||
# ReactAgent works for any LLM. | ||||||||||||||||
calculator_agent = FunctionAgent( | ||||||||||||||||
name="calculator", | ||||||||||||||||
description="Performs basic arithmetic operations", | ||||||||||||||||
system_prompt="You are a calculator assistant.", | ||||||||||||||||
tools=[ | ||||||||||||||||
FunctionTool.from_defaults(fn=add), | ||||||||||||||||
FunctionTool.from_defaults(fn=subtract), | ||||||||||||||||
], | ||||||||||||||||
Comment on lines
+37
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. possible in Python to wrap FunctionTool automatically, e.g.?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be possible, although now with FunctionTool and FunctionToolWithContext, I'll need to think a little harder about how to detect when each one is needed |
||||||||||||||||
llm=OpenAI(model="gpt-4"), | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
retriever_agent = FunctionAgent( | ||||||||||||||||
name="retriever", | ||||||||||||||||
description="Manages data retrieval", | ||||||||||||||||
system_prompt="You are a retrieval assistant.", | ||||||||||||||||
is_entrypoint_agent=True, | ||||||||||||||||
llm=OpenAI(model="gpt-4"), | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
# Create and run the workflow | ||||||||||||||||
workflow = MultiAgentWorkflow( | ||||||||||||||||
agent_configs=[calculator_agent, retriever_agent] | ||||||||||||||||
Comment on lines
+53
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how about defining the entrypoint here?
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yea, that is actually a better UX (although I think there can only be one entry point 🤔) |
||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
# Run the system | ||||||||||||||||
response = await workflow.run(user_msg="Can you add 5 and 3?") | ||||||||||||||||
|
||||||||||||||||
# Or stream the events | ||||||||||||||||
handler = workflow.run(user_msg="Can you add 5 and 3?") | ||||||||||||||||
async for event in handler.stream_events(): | ||||||||||||||||
if hasattr(event, "delta"): | ||||||||||||||||
print(event.delta, end="", flush=True) | ||||||||||||||||
Comment on lines
+62
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are these agents streaming dedicated events that can be shown in the UI (we're using this in create-llama). There we're having the here is an example using it to send the progress of tool calls: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! That is the main intention for these events, to show progress in some UI. I didn't capture the concept of "in progress" or "completed" with this, its mostly all just events at points in time (here's the agent input, here's the agent stream, here's the agent output, heres a tool im about to call, here's the tool output) -- I could refactor, but not sure if its needed or not
Comment on lines
+62
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could then also add a helper that is printing the events in a nice way for examples:
Suggested change
|
||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
## How It Works | ||||||||||||||||
|
||||||||||||||||
The MultiAgentWorkflow manages a collection of agents, each with their own specialized capabilities. One agent must be designated as the entry point agent (`is_entrypoint_agent=True`). | ||||||||||||||||
|
||||||||||||||||
When a user message comes in, it's first routed to the entry point agent. Each agent can then: | ||||||||||||||||
|
||||||||||||||||
1. Handle the request directly using their tools | ||||||||||||||||
2. Hand off to another agent better suited for the task | ||||||||||||||||
3. Return a response to the user | ||||||||||||||||
|
||||||||||||||||
## Configuration Options | ||||||||||||||||
|
||||||||||||||||
### Agent Config | ||||||||||||||||
|
||||||||||||||||
Each agent holds a certain set of configuration options. Whether you use `FunctionAgent` or `ReactAgent`, the core options are the same. | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
FunctionAgent( | ||||||||||||||||
# Unique name for the agent (str) | ||||||||||||||||
name="name", | ||||||||||||||||
# Description of agent's capabilities (str) | ||||||||||||||||
description="description", | ||||||||||||||||
# System prompt for the agent (str) | ||||||||||||||||
system_prompt="system_prompt", | ||||||||||||||||
# Tools available to this agent (List[BaseTool]) | ||||||||||||||||
tools=[...], | ||||||||||||||||
# LLM to use for this agent. (BaseLLM) | ||||||||||||||||
llm=OpenAI(model="gpt-4"), | ||||||||||||||||
# Whether this is the entry point. (bool) | ||||||||||||||||
is_entrypoint_agent=True, | ||||||||||||||||
# List of agents this one can hand off to. Defaults to all agents. (List[str]) | ||||||||||||||||
can_handoff_to=[...], | ||||||||||||||||
) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
### Workflow Options | ||||||||||||||||
|
||||||||||||||||
The MultiAgentWorkflow constructor accepts: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
MultiAgentWorkflow( | ||||||||||||||||
# List of agent configs. (List[BaseWorkflowAgent]) | ||||||||||||||||
agents=[...], | ||||||||||||||||
# Initial state dict. (Optional[dict]) | ||||||||||||||||
initial_state=None, | ||||||||||||||||
# Custom prompt for handoffs. Should contain the `agent_info` string variable. (Optional[str]) | ||||||||||||||||
handoff_prompt=None, | ||||||||||||||||
# Custom prompt for state. Should contain the `state` and `msg` string variables. (Optional[str]) | ||||||||||||||||
state_prompt=None, | ||||||||||||||||
) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
### State Management | ||||||||||||||||
|
||||||||||||||||
#### Initial Global State | ||||||||||||||||
|
||||||||||||||||
You can provide an initial state dict that will be available to all agents: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
workflow = MultiAgentWorkflow( | ||||||||||||||||
agents=[...], | ||||||||||||||||
initial_state={"counter": 0}, | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ooc how is the state modified? by the user or by the agent? are there constraints in 1) number of keys, and 2) the types of the values? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the state is thrown into the workflow context, so There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh i see |
||||||||||||||||
state_prompt="Current state: {state}. User message: {msg}", | ||||||||||||||||
) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
The state is stored in the `state` key of the workflow context. | ||||||||||||||||
|
||||||||||||||||
#### Persisting State Between Runs | ||||||||||||||||
|
||||||||||||||||
In order to persist state between runs, you can pass in the context from the previous run: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
workflow = MultiAgentWorkflow(...) | ||||||||||||||||
|
||||||||||||||||
# Run the workflow | ||||||||||||||||
handler = workflow.run(user_msg="Can you add 5 and 3?") | ||||||||||||||||
response = await handler | ||||||||||||||||
|
||||||||||||||||
# Pass in the context from the previous run | ||||||||||||||||
handler = workflow.run(ctx=handler.ctx, user_msg="Can you add 5 and 3?") | ||||||||||||||||
response = await handler | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
#### Serializing Context / State | ||||||||||||||||
|
||||||||||||||||
As with normal workflows, the context is serializable: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
from llama_index.core.workflow import ( | ||||||||||||||||
Context, | ||||||||||||||||
JsonSerializer, | ||||||||||||||||
JsonPickleSerializer, | ||||||||||||||||
) | ||||||||||||||||
|
||||||||||||||||
# the default serializer is JsonSerializer for safety | ||||||||||||||||
ctx_dict = handler.ctx.to_dict(serializer=JsonSerializer()) | ||||||||||||||||
|
||||||||||||||||
# then you can rehydrate the context | ||||||||||||||||
ctx = Context.from_dict(ctx_dict, serializer=JsonSerializer()) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
## Streaming Events | ||||||||||||||||
|
||||||||||||||||
The workflow emits various events during execution that you can stream: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
async for event in workflow.run(...).stream_events(): | ||||||||||||||||
if isinstance(event, AgentInput): | ||||||||||||||||
print(event.input) | ||||||||||||||||
print(event.current_agent_name) | ||||||||||||||||
elif isinstance(event, AgentStream): | ||||||||||||||||
# Agent thinking/tool calling response stream | ||||||||||||||||
print(event.delta) | ||||||||||||||||
print(event.current_agent_name) | ||||||||||||||||
elif isinstance(event, AgentOutput): | ||||||||||||||||
print(event.response) | ||||||||||||||||
print(event.tool_calls) | ||||||||||||||||
print(event.raw) | ||||||||||||||||
print(event.current_agent_name) | ||||||||||||||||
elif isinstance(event, ToolCall): | ||||||||||||||||
# Tool being called | ||||||||||||||||
print(event.tool_name) | ||||||||||||||||
print(event.tool_kwargs) | ||||||||||||||||
elif isinstance(event, ToolCallResult): | ||||||||||||||||
# Result of tool call | ||||||||||||||||
print(event.tool_output) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
## Accessing Context in Tools | ||||||||||||||||
|
||||||||||||||||
The `FunctionToolWithContext` allows tools to access the workflow context: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
from llama_index.core.workflow import FunctionToolWithContext | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
async def get_counter(ctx: Context) -> int: | ||||||||||||||||
"""Get the current counter value.""" | ||||||||||||||||
return await ctx.get("counter", default=0) | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
counter_tool = FunctionToolWithContext.from_defaults( | ||||||||||||||||
async_fn=get_counter, description="Get the current counter value" | ||||||||||||||||
) | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
## Human in the Loop | ||||||||||||||||
|
||||||||||||||||
Using the context, you can implement a human in the loop pattern in your tools: | ||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
from llama_index.core.workflow import Event | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
class AskForConfirmationEvent(Event): | ||||||||||||||||
"""Ask for confirmation event.""" | ||||||||||||||||
|
||||||||||||||||
confirmation_id: str | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
class ConfirmationEvent(Event): | ||||||||||||||||
"""Confirmation event.""" | ||||||||||||||||
|
||||||||||||||||
confirmation: bool | ||||||||||||||||
confirmation_id: str | ||||||||||||||||
|
||||||||||||||||
|
||||||||||||||||
async def ask_for_confirmation(ctx: Context) -> bool: | ||||||||||||||||
"""Ask the user for confirmation.""" | ||||||||||||||||
ctx.write_event_to_stream(AskForConfirmationEvent(confirmation_id="1234")) | ||||||||||||||||
|
||||||||||||||||
result = await ctx.wait_for_event( | ||||||||||||||||
ConfirmationEvent, requirements={"confirmation_id": "1234"} | ||||||||||||||||
) | ||||||||||||||||
return result.confirmation | ||||||||||||||||
``` | ||||||||||||||||
|
||||||||||||||||
When this function is called, it will block the workflow execution until the user sends the required confirmation event. | ||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. when is this function called? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By an agent, its meant to be an agent tool. I'll make this clearer. You could also subclass or use this in your own workflows though |
||||||||||||||||
|
||||||||||||||||
```python | ||||||||||||||||
handler = workflow.run(user_msg="Can you add 5 and 3?") | ||||||||||||||||
|
||||||||||||||||
async for event in handler.stream_events(): | ||||||||||||||||
if isinstance(event, AskForConfirmationEvent): | ||||||||||||||||
print(event.confirmation_id) | ||||||||||||||||
handler.ctx.send_event( | ||||||||||||||||
ConfirmationEvent(confirmation=True, confirmation_id="1234") | ||||||||||||||||
) | ||||||||||||||||
... | ||||||||||||||||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
python_sources() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from llama_index.core.agent.workflow.multi_agent_workflow import MultiAgentWorkflow | ||
from llama_index.core.agent.workflow.base_agent import BaseWorkflowAgent | ||
from llama_index.core.agent.workflow.function_agent import FunctionAgent | ||
from llama_index.core.agent.workflow.react_agent import ReactAgent | ||
from llama_index.core.agent.workflow.workflow_events import ( | ||
AgentInput, | ||
AgentSetup, | ||
AgentStream, | ||
AgentOutput, | ||
ToolCall, | ||
ToolCallResult, | ||
) | ||
|
||
|
||
__all__ = [ | ||
"AgentInput", | ||
"AgentSetup", | ||
"AgentStream", | ||
"AgentOutput", | ||
"BaseWorkflowAgent", | ||
"FunctionAgent", | ||
"MultiAgentWorkflow", | ||
"ReactAgent", | ||
"ToolCall", | ||
"ToolCallResult", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
from abc import ABC, abstractmethod | ||
from typing import List, Optional | ||
|
||
from llama_index.core.agent.workflow.workflow_events import ( | ||
AgentOutput, | ||
ToolCallResult, | ||
) | ||
from llama_index.core.bridge.pydantic import BaseModel, Field, ConfigDict | ||
from llama_index.core.llms import ChatMessage, LLM | ||
from llama_index.core.memory import BaseMemory | ||
from llama_index.core.tools import BaseTool, AsyncBaseTool | ||
from llama_index.core.workflow import Context | ||
from llama_index.core.objects import ObjectRetriever | ||
from llama_index.core.settings import Settings | ||
|
||
|
||
def get_default_llm() -> LLM: | ||
return Settings.llm | ||
|
||
|
||
class BaseWorkflowAgent(BaseModel, ABC): | ||
logan-markewich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Base class for all agents, combining config and logic.""" | ||
|
||
model_config = ConfigDict(arbitrary_types_allowed=True) | ||
|
||
name: str = Field(description="The name of the agent") | ||
description: str = Field( | ||
description="The description of what the agent does and is responsible for" | ||
) | ||
system_prompt: Optional[str] = Field( | ||
default=None, description="The system prompt for the agent" | ||
) | ||
tools: Optional[List[BaseTool]] = Field( | ||
default=None, description="The tools that the agent can use" | ||
) | ||
tool_retriever: Optional[ObjectRetriever] = Field( | ||
default=None, | ||
description="The tool retriever for the agent, can be provided instead of tools", | ||
) | ||
can_handoff_to: Optional[List[str]] = Field( | ||
default=None, description="The agent names that this agent can hand off to" | ||
) | ||
llm: LLM = Field( | ||
default_factory=get_default_llm, description="The LLM that the agent uses" | ||
) | ||
is_entrypoint_agent: bool = Field( | ||
default=False, | ||
description="Whether the agent is the entrypoint agent in a multi-agent workflow", | ||
) | ||
|
||
@abstractmethod | ||
async def take_step( | ||
self, | ||
ctx: Context, | ||
llm_input: List[ChatMessage], | ||
tools: List[AsyncBaseTool], | ||
memory: BaseMemory, | ||
) -> AgentOutput: | ||
"""Take a single step with the agent.""" | ||
|
||
@abstractmethod | ||
async def handle_tool_call_results( | ||
self, ctx: Context, results: List[ToolCallResult], memory: BaseMemory | ||
) -> None: | ||
"""Handle tool call results.""" | ||
|
||
@abstractmethod | ||
async def finalize( | ||
self, ctx: Context, output: AgentOutput, memory: BaseMemory | ||
) -> AgentOutput: | ||
"""Finalize the agent's execution.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we mention that this is built upon our core workflows classes?
the user will at least know that the syntax for running workflows and handling the output event stream is the same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I should at least link to it somewhere, good point