flock-core 0.3.6__py3-none-any.whl → 0.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/cli/assets/release_notes.md +37 -9
- flock/core/flock_agent.py +5 -23
- flock/core/flock_factory.py +8 -5
- flock/core/flock_module.py +3 -1
- flock/core/flock_router.py +70 -0
- flock/evaluators/zep/zep_evaluator.py +2 -0
- flock/routers/__init__.py +1 -0
- flock/routers/agent/__init__.py +1 -0
- flock/routers/agent/agent_router.py +234 -0
- flock/routers/agent/handoff_agent.py +58 -0
- flock/routers/default/__init__.py +1 -0
- flock/routers/default/default_router.py +76 -0
- flock/routers/llm/__init__.py +1 -0
- flock/routers/llm/llm_router.py +363 -0
- flock/workflow/activities.py +74 -33
- {flock_core-0.3.6.dist-info → flock_core-0.3.8.dist-info}/METADATA +1 -1
- {flock_core-0.3.6.dist-info → flock_core-0.3.8.dist-info}/RECORD +20 -11
- {flock_core-0.3.6.dist-info → flock_core-0.3.8.dist-info}/WHEEL +0 -0
- {flock_core-0.3.6.dist-info → flock_core-0.3.8.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.6.dist-info → flock_core-0.3.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# Flock v0.3 - Hummingbird
|
|
3
2
|
|
|
4
3
|
We're excited to announce Flock v0.3, codenamed **"Hummingbird"**! This release brings a fundamental redesign of Flock's core architecture, introducing **unprecedented modularity and flexibility** to AI agent development.
|
|
@@ -13,17 +12,17 @@ Like a hummingbird, modules are small and nimble code packages. Put enough of th
|
|
|
13
12
|
|
|
14
13
|
### Other notable additions:
|
|
15
14
|
- **CLI Interface** – Flock now has a command-line interface
|
|
15
|
+
- **REST API Server** – Expose your agents via HTTP endpoints
|
|
16
16
|
- **Color-coded logging** – Better debugging experience
|
|
17
17
|
- **New examples**
|
|
18
18
|
- ...and much more!
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
## Core Changes
|
|
22
|
+
## Core Changes
|
|
23
23
|
|
|
24
|
-
### New Module System
|
|
25
|
-
- **
|
|
26
|
-
- Simple yet powerful lifecycle hooks: `initialize`, `pre_evaluate`, `post_evaluate`, `terminate`
|
|
24
|
+
### New Module System
|
|
25
|
+
- **Pluggable modules system á la FastAPI**
|
|
27
26
|
- **Easy-to-implement** module interface
|
|
28
27
|
- **Configuration system** for clean parameter management
|
|
29
28
|
|
|
@@ -32,15 +31,44 @@ Like a hummingbird, modules are small and nimble code packages. Put enough of th
|
|
|
32
31
|
- Built-in support for multiple evaluation strategies:
|
|
33
32
|
- **Declarative Evaluator** – The default way Flock is designed
|
|
34
33
|
- **Natural Language Evaluator** – Use "classic" prompting
|
|
34
|
+
- **Zep Evaluator** – Add or query data
|
|
35
35
|
- **Easily extendable** with custom evaluation approaches
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### New Router System
|
|
38
|
+
- **Pluggable router system** for dynamic agent chaining
|
|
39
|
+
- Built-in support for multiple routing strategies:
|
|
40
|
+
- **Default Router** – Uses the agent's hand_off property
|
|
41
|
+
- **LLM Router** – Uses an LLM to determine the next agent
|
|
42
|
+
- **Agent Router** – Uses a dedicated agent to make routing decisions
|
|
43
|
+
- **Easily extendable** with custom routing approaches
|
|
44
|
+
|
|
45
|
+
### REST API Server
|
|
46
|
+
- **FastAPI-based** HTTP server for exposing agents
|
|
47
|
+
- **Synchronous and asynchronous** execution modes
|
|
48
|
+
- **Run status tracking** with unique run IDs
|
|
49
|
+
- **Agent discovery** endpoint to list available agents
|
|
50
|
+
- **Simple integration** with existing Flock instances
|
|
51
|
+
|
|
52
|
+
### Auto-Handoff Feature
|
|
53
|
+
- **Dynamic agent chaining** without explicit handoff definitions
|
|
54
|
+
- **LLM-powered routing** to determine the best next agent
|
|
55
|
+
- **Emergent behavior** in multi-agent systems
|
|
56
|
+
- **Simple to use** with the "auto_handoff" string value
|
|
57
|
+
|
|
58
|
+
### New high end examples like the Repository Analyzer
|
|
59
|
+
- **Automatic documentation generation** for any codebase
|
|
60
|
+
- **Rule-based version** using custom evaluators
|
|
61
|
+
- **LLM-based version** for more flexible and powerful analysis
|
|
62
|
+
- **Comprehensive documentation** including overview, architecture, components, and more
|
|
63
|
+
|
|
64
|
+
### FlockFactory
|
|
38
65
|
- Provides **pre-configured agents**, so you don't have to manage modules and evaluators manually!
|
|
39
66
|
|
|
40
67
|
### Built-in Modules
|
|
41
68
|
- **Memory Module** – Persistent agent memory
|
|
42
69
|
- **Output Module** – Advanced output formatting and storage
|
|
43
70
|
- **Metrics Module** – Detailed performance tracking
|
|
71
|
+
- **Zep Module** – Uses Zep for Knowledge Graphs
|
|
44
72
|
|
|
45
73
|
---
|
|
46
74
|
|
|
@@ -99,7 +127,7 @@ See? **Basically nothing changed!** Just more modular and flexible.
|
|
|
99
127
|
|
|
100
128
|
---
|
|
101
129
|
|
|
102
|
-
##
|
|
130
|
+
## Installation
|
|
103
131
|
|
|
104
132
|
```bash
|
|
105
133
|
pip install flock-core>=0.3.0
|
|
@@ -107,5 +135,5 @@ pip install flock-core>=0.3.0
|
|
|
107
135
|
|
|
108
136
|
---
|
|
109
137
|
|
|
110
|
-
**Full documentation**: [
|
|
111
|
-
**GitHub**: [github.com/flock
|
|
138
|
+
**Full documentation**: [https://whiteducksoftware.github.io/flock](https://whiteducksoftware.github.io/flock)
|
|
139
|
+
**GitHub**: [https://github.com/whiteducksoftware/flock](https://github.com/whiteducksoftware/flock)
|
flock/core/flock_agent.py
CHANGED
|
@@ -5,15 +5,15 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from abc import ABC
|
|
7
7
|
from collections.abc import Callable
|
|
8
|
-
from typing import Any, TypeVar
|
|
8
|
+
from typing import Any, TypeVar
|
|
9
9
|
|
|
10
10
|
import cloudpickle
|
|
11
11
|
from opentelemetry import trace
|
|
12
12
|
from pydantic import BaseModel, Field
|
|
13
13
|
|
|
14
|
-
from flock.core.context.context import FlockContext
|
|
15
14
|
from flock.core.flock_evaluator import FlockEvaluator
|
|
16
15
|
from flock.core.flock_module import FlockModule
|
|
16
|
+
from flock.core.flock_router import FlockRouter
|
|
17
17
|
from flock.core.logging.logging import get_logger
|
|
18
18
|
|
|
19
19
|
logger = get_logger("agent")
|
|
@@ -23,21 +23,6 @@ tracer = trace.get_tracer(__name__)
|
|
|
23
23
|
T = TypeVar("T", bound="FlockAgent")
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
class HandOff(BaseModel):
|
|
27
|
-
"""Base class for handoff returns."""
|
|
28
|
-
|
|
29
|
-
next_agent: Union[str, "FlockAgent"] = Field(
|
|
30
|
-
default="", description="Next agent to invoke"
|
|
31
|
-
)
|
|
32
|
-
input: dict[str, Any] = Field(
|
|
33
|
-
default_factory=dict,
|
|
34
|
-
description="Input data for the next agent",
|
|
35
|
-
)
|
|
36
|
-
context: FlockContext = Field(
|
|
37
|
-
default=None, descrio="Override context parameters"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
26
|
class FlockAgent(BaseModel, ABC):
|
|
42
27
|
name: str = Field(..., description="Unique identifier for the agent.")
|
|
43
28
|
model: str | None = Field(
|
|
@@ -72,12 +57,9 @@ class FlockAgent(BaseModel, ABC):
|
|
|
72
57
|
description="Set to True to enable caching of the agent's results.",
|
|
73
58
|
)
|
|
74
59
|
|
|
75
|
-
|
|
76
|
-
None,
|
|
77
|
-
description=
|
|
78
|
-
"Specifies the next agent in the workflow or a callable that determines the handoff. "
|
|
79
|
-
"This allows chaining of agents."
|
|
80
|
-
),
|
|
60
|
+
handoff_router: FlockRouter | None = Field(
|
|
61
|
+
default=None,
|
|
62
|
+
description="Router to use for determining the next agent in the workflow.",
|
|
81
63
|
)
|
|
82
64
|
|
|
83
65
|
evaluator: FlockEvaluator = Field(
|
flock/core/flock_factory.py
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from flock.core.flock_agent import FlockAgent
|
|
6
|
+
from flock.core.flock_agent import FlockAgent
|
|
7
7
|
from flock.core.logging.formatters.themes import OutputTheme
|
|
8
8
|
from flock.evaluators.declarative.declarative_evaluator import (
|
|
9
9
|
DeclarativeEvaluator,
|
|
10
10
|
DeclarativeEvaluatorConfig,
|
|
11
11
|
)
|
|
12
|
-
from flock.modules.performance.metrics_module import MetricsModule, MetricsModuleConfig
|
|
13
12
|
from flock.modules.output.output_module import OutputModule, OutputModuleConfig
|
|
13
|
+
from flock.modules.performance.metrics_module import (
|
|
14
|
+
MetricsModule,
|
|
15
|
+
MetricsModuleConfig,
|
|
16
|
+
)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class FlockFactory:
|
|
@@ -24,7 +27,6 @@ class FlockFactory:
|
|
|
24
27
|
input: str | Callable[..., str] | None = None,
|
|
25
28
|
output: str | Callable[..., str] | None = None,
|
|
26
29
|
tools: list[Callable[..., Any] | Any] | None = None,
|
|
27
|
-
hand_off: str | HandOff | Callable[..., HandOff] | None = None,
|
|
28
30
|
use_cache: bool = True,
|
|
29
31
|
enable_rich_tables: bool = False,
|
|
30
32
|
output_theme: OutputTheme = OutputTheme.abernathy,
|
|
@@ -53,7 +55,6 @@ class FlockFactory:
|
|
|
53
55
|
input=input,
|
|
54
56
|
output=output,
|
|
55
57
|
tools=tools,
|
|
56
|
-
hand_off=hand_off,
|
|
57
58
|
model=model,
|
|
58
59
|
description=description,
|
|
59
60
|
evaluator=evaluator,
|
|
@@ -65,7 +66,9 @@ class FlockFactory:
|
|
|
65
66
|
)
|
|
66
67
|
output_module = OutputModule("output", config=output_config)
|
|
67
68
|
|
|
68
|
-
metrics_config = MetricsModuleConfig(
|
|
69
|
+
metrics_config = MetricsModuleConfig(
|
|
70
|
+
latency_threshold_ms=alert_latency_threshold_ms
|
|
71
|
+
)
|
|
69
72
|
metrics_module = MetricsModule("metrics", config=metrics_config)
|
|
70
73
|
|
|
71
74
|
agent.add_module(output_module)
|
flock/core/flock_module.py
CHANGED
|
@@ -43,7 +43,9 @@ class FlockModule(BaseModel, ABC):
|
|
|
43
43
|
2. Using FlockModuleConfig.with_fields() to create a config class
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
|
-
name: str = Field(
|
|
46
|
+
name: str = Field(
|
|
47
|
+
default="", description="Unique identifier for the module"
|
|
48
|
+
)
|
|
47
49
|
config: FlockModuleConfig = Field(
|
|
48
50
|
default_factory=FlockModuleConfig, description="Module configuration"
|
|
49
51
|
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Base router class for the Flock framework."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from flock.core.context.context import FlockContext
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HandOffRequest(BaseModel):
|
|
12
|
+
"""Base class for handoff returns."""
|
|
13
|
+
|
|
14
|
+
next_agent: str = Field(default="", description="Next agent to invoke")
|
|
15
|
+
# match = use the output fields of the current agent that also exists as input field of the next agent
|
|
16
|
+
# add = add the output of the current agent to the input of the next agent
|
|
17
|
+
hand_off_mode: Literal["match", "add"] = Field(default="match")
|
|
18
|
+
override_next_agent: Any | None = Field(
|
|
19
|
+
default=None,
|
|
20
|
+
description="Override the next agent to hand off to",
|
|
21
|
+
)
|
|
22
|
+
override_context: FlockContext | None = Field(
|
|
23
|
+
default=None, descrio="Override context parameters"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FlockRouterConfig(BaseModel):
|
|
28
|
+
"""Configuration for a router.
|
|
29
|
+
|
|
30
|
+
This class defines the configuration parameters for a router.
|
|
31
|
+
Subclasses can extend this to add additional parameters.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
enabled: bool = Field(
|
|
35
|
+
default=True, description="Whether the router is enabled"
|
|
36
|
+
)
|
|
37
|
+
agents: list[str] | None = Field(
|
|
38
|
+
default=None,
|
|
39
|
+
description="List of agents to choose from",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FlockRouter(BaseModel, ABC):
|
|
44
|
+
"""Base class for all routers.
|
|
45
|
+
|
|
46
|
+
A router is responsible for determining the next agent in a workflow
|
|
47
|
+
based on the current agent's output.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str = Field(..., description="Name of the router")
|
|
51
|
+
config: FlockRouterConfig = Field(default_factory=FlockRouterConfig)
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def route(
|
|
55
|
+
self,
|
|
56
|
+
current_agent: Any,
|
|
57
|
+
result: dict[str, Any],
|
|
58
|
+
context: FlockContext,
|
|
59
|
+
) -> HandOffRequest:
|
|
60
|
+
"""Determine the next agent to hand off to based on the current agent's output.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
current_agent: The agent that just completed execution
|
|
64
|
+
result: The output from the current agent
|
|
65
|
+
context: The global execution context
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
A HandOff object containing the next agent and input data
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
@@ -40,6 +40,8 @@ class ZepEvaluator(FlockEvaluator, DSPyIntegrationMixin, PromptParserMixin):
|
|
|
40
40
|
zep_api_key=self.config.zep_api_key,
|
|
41
41
|
zep_url=self.config.zep_url,
|
|
42
42
|
min_fact_rating=self.config.min_fact_rating,
|
|
43
|
+
enable_read=True,
|
|
44
|
+
enable_write=True,
|
|
43
45
|
),
|
|
44
46
|
)
|
|
45
47
|
client = zep.get_client()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Routers for the Flock framework."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent-based router implementation for the Flock framework."""
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Agent-based router implementation for the Flock framework."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from flock.core.context.context import FlockContext
|
|
6
|
+
from flock.core.flock_agent import FlockAgent
|
|
7
|
+
from flock.core.flock_router import (
|
|
8
|
+
FlockRouter,
|
|
9
|
+
FlockRouterConfig,
|
|
10
|
+
HandOffRequest,
|
|
11
|
+
)
|
|
12
|
+
from flock.core.logging.formatters.themes import OutputTheme
|
|
13
|
+
from flock.core.logging.logging import get_logger
|
|
14
|
+
from flock.evaluators.declarative.declarative_evaluator import (
|
|
15
|
+
DeclarativeEvaluator,
|
|
16
|
+
DeclarativeEvaluatorConfig,
|
|
17
|
+
)
|
|
18
|
+
from flock.modules.output.output_module import OutputModule, OutputModuleConfig
|
|
19
|
+
from flock.routers.agent.handoff_agent import (
|
|
20
|
+
AgentInfo,
|
|
21
|
+
HandoffAgent,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = get_logger("agent_router")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentRouterConfig(FlockRouterConfig):
|
|
28
|
+
"""Configuration for the agent router.
|
|
29
|
+
|
|
30
|
+
This class extends FlockRouterConfig with parameters specific to the agent router.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
with_output: bool = False
|
|
34
|
+
confidence_threshold: float = 0.5 # No additional parameters needed for now
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentRouter(FlockRouter):
|
|
38
|
+
"""Router that uses a FlockAgent to determine the next agent in a workflow.
|
|
39
|
+
|
|
40
|
+
This class is responsible for:
|
|
41
|
+
1. Creating and managing a HandoffAgent
|
|
42
|
+
2. Analyzing available agents in the registry
|
|
43
|
+
3. Using the HandoffAgent to determine the best next agent
|
|
44
|
+
4. Creating a HandOff object with the selected agent
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
name: str = "agent_router",
|
|
50
|
+
config: AgentRouterConfig | None = None,
|
|
51
|
+
):
|
|
52
|
+
"""Initialize the AgentRouter.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
registry: The agent registry containing all available agents
|
|
56
|
+
name: The name of the router
|
|
57
|
+
config: The router configuration
|
|
58
|
+
"""
|
|
59
|
+
super().__init__(
|
|
60
|
+
name=name, config=config or AgentRouterConfig(name=name)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def route(
|
|
64
|
+
self,
|
|
65
|
+
current_agent: FlockAgent,
|
|
66
|
+
result: dict[str, Any],
|
|
67
|
+
context: FlockContext,
|
|
68
|
+
) -> HandOffRequest:
|
|
69
|
+
"""Determine the next agent to hand off to based on the current agent's output.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
current_agent: The agent that just completed execution
|
|
73
|
+
result: The output from the current agent
|
|
74
|
+
context: The global execution context
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A HandOff object containing the next agent and input data
|
|
78
|
+
"""
|
|
79
|
+
# Get all available agents from context.agent_definitions
|
|
80
|
+
agent_definitions = context.agent_definitions
|
|
81
|
+
handoff_agent = HandoffAgent(model=current_agent.model)
|
|
82
|
+
handoff_agent.evaluator = DeclarativeEvaluator(
|
|
83
|
+
name="evaluator",
|
|
84
|
+
config=DeclarativeEvaluatorConfig(
|
|
85
|
+
model=current_agent.model,
|
|
86
|
+
use_cache=True,
|
|
87
|
+
max_tokens=1000,
|
|
88
|
+
temperature=0.0,
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
if self.config.with_output:
|
|
92
|
+
handoff_agent.add_module(
|
|
93
|
+
OutputModule(
|
|
94
|
+
name="output",
|
|
95
|
+
config=OutputModuleConfig(
|
|
96
|
+
theme=OutputTheme.abernathy,
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
available_agents = self._get_available_agents(
|
|
101
|
+
agent_definitions, current_agent.name
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not available_agents:
|
|
105
|
+
logger.warning("No available agents for agent-based routing")
|
|
106
|
+
return HandOffRequest(
|
|
107
|
+
next_agent="",
|
|
108
|
+
hand_off_mode="add",
|
|
109
|
+
override_next_agent=None,
|
|
110
|
+
override_context=None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Prepare input for the handoff agent
|
|
114
|
+
handoff_input = {
|
|
115
|
+
"current_agent_name": current_agent.name,
|
|
116
|
+
"current_agent_description": current_agent.description,
|
|
117
|
+
"current_agent_input": current_agent.input,
|
|
118
|
+
"current_agent_output": current_agent.output,
|
|
119
|
+
"current_result": result,
|
|
120
|
+
"available_agents": available_agents,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# Run the handoff agent to determine the next agent
|
|
125
|
+
handoff_result = await handoff_agent.run_async(handoff_input)
|
|
126
|
+
|
|
127
|
+
# Extract the decision
|
|
128
|
+
next_agent_name = handoff_result.get("agent_name")
|
|
129
|
+
confidence = handoff_result.get("confidence")
|
|
130
|
+
reasoning = handoff_result.get("reasoning")
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Agent router selected agent '{next_agent_name}' with confidence {confidence} and reasoning: {reasoning}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if confidence < self.config.confidence_threshold:
|
|
136
|
+
logger.info(
|
|
137
|
+
f"No suitable next agent found (best score: {confidence})"
|
|
138
|
+
)
|
|
139
|
+
return HandOffRequest(
|
|
140
|
+
next_agent="",
|
|
141
|
+
hand_off_mode="add",
|
|
142
|
+
override_next_agent=None,
|
|
143
|
+
override_context=None,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
next_agent = agent_definitions.get(next_agent_name)
|
|
147
|
+
if not next_agent:
|
|
148
|
+
logger.error(
|
|
149
|
+
f"Selected agent '{next_agent_name}' not found in agent definitions"
|
|
150
|
+
)
|
|
151
|
+
return HandOffRequest(
|
|
152
|
+
next_agent="",
|
|
153
|
+
hand_off_mode="add",
|
|
154
|
+
override_next_agent=None,
|
|
155
|
+
override_context=None,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
logger.info(
|
|
159
|
+
f"Agent router selected agent '{next_agent_name}' with confidence {confidence}"
|
|
160
|
+
)
|
|
161
|
+
return HandOffRequest(
|
|
162
|
+
next_agent=next_agent_name,
|
|
163
|
+
hand_off_mode="add",
|
|
164
|
+
override_next_agent=None,
|
|
165
|
+
override_context=None,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.error(f"Error in agent-based routing: {e}")
|
|
170
|
+
return HandOffRequest(
|
|
171
|
+
next_agent="",
|
|
172
|
+
hand_off_mode="add",
|
|
173
|
+
override_next_agent=None,
|
|
174
|
+
override_context=None,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def _get_available_agents(
|
|
178
|
+
self, agent_definitions: dict[str, Any], current_agent_name: str
|
|
179
|
+
) -> list[AgentInfo]:
|
|
180
|
+
"""Get all available agents except the current one and the handoff agent.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
agent_definitions: Dictionary of available agents
|
|
184
|
+
current_agent_name: Name of the current agent to exclude
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
List of available agents as AgentInfo objects
|
|
188
|
+
"""
|
|
189
|
+
agents = []
|
|
190
|
+
for agent_name in agent_definitions:
|
|
191
|
+
if agent_name != current_agent_name:
|
|
192
|
+
agent = agent_definitions[agent_name]
|
|
193
|
+
agent_info = AgentInfo(
|
|
194
|
+
name=agent_name,
|
|
195
|
+
description=agent.agent_data["description"]
|
|
196
|
+
if agent.agent_data["description"]
|
|
197
|
+
else "",
|
|
198
|
+
input_schema=agent.agent_data["input"],
|
|
199
|
+
output_schema=agent.agent_data["output"],
|
|
200
|
+
)
|
|
201
|
+
agents.append(agent_info)
|
|
202
|
+
return agents
|
|
203
|
+
|
|
204
|
+
def _get_schema_from_agent(
|
|
205
|
+
self, agent: Any, schema_type: str
|
|
206
|
+
) -> dict[str, Any]:
|
|
207
|
+
"""Extract input or output schema from an agent.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
agent: The agent to extract schema from
|
|
211
|
+
schema_type: Either "input" or "output"
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary representation of the schema
|
|
215
|
+
"""
|
|
216
|
+
schema = {}
|
|
217
|
+
schema_str = agent.agent_data.get(schema_type, "")
|
|
218
|
+
|
|
219
|
+
# Parse the schema string to extract field names, types, and descriptions
|
|
220
|
+
if schema_str:
|
|
221
|
+
fields = schema_str.split(",")
|
|
222
|
+
for field in fields:
|
|
223
|
+
field = field.strip()
|
|
224
|
+
if ":" in field:
|
|
225
|
+
name, rest = field.split(":", 1)
|
|
226
|
+
name = name.strip()
|
|
227
|
+
schema[name] = rest.strip()
|
|
228
|
+
else:
|
|
229
|
+
schema[field] = "Any"
|
|
230
|
+
|
|
231
|
+
return schema
|
|
232
|
+
|
|
233
|
+
# The _create_next_input method is no longer needed since we're using hand_off_mode="add"
|
|
234
|
+
# instead of manually preparing inputs for the next agent
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Handoff agent for the agent-based router."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from flock.core.flock_agent import FlockAgent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentInfo(BaseModel):
|
|
9
|
+
"""Information about an agent for handoff decisions."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
description: str = ""
|
|
13
|
+
input_schema: str = ""
|
|
14
|
+
output_schema: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HandoffDecision(BaseModel):
|
|
18
|
+
"""Decision about which agent to hand off to."""
|
|
19
|
+
|
|
20
|
+
agent_name: str
|
|
21
|
+
confidence: float
|
|
22
|
+
reasoning: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HandoffAgent(FlockAgent):
|
|
26
|
+
"""Agent that decides which agent to hand off to next.
|
|
27
|
+
|
|
28
|
+
This agent analyzes the current agent's output and available agents
|
|
29
|
+
to determine the best next agent in the workflow.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
name: str = "handoff_agent",
|
|
35
|
+
model: str | None = None,
|
|
36
|
+
description: str = "Decides which agent to hand off to next",
|
|
37
|
+
):
|
|
38
|
+
"""Initialize the HandoffAgent.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: The name of the agent
|
|
42
|
+
model: The model to use (e.g., 'openai/gpt-4o')
|
|
43
|
+
description: A human-readable description of the agent
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(
|
|
46
|
+
name=name,
|
|
47
|
+
model=model,
|
|
48
|
+
description=description,
|
|
49
|
+
input=(
|
|
50
|
+
"current_agent_name: str | Name of the current agent, "
|
|
51
|
+
"current_agent_description: str | Description of the current agent, "
|
|
52
|
+
"current_agent_input: str | Input schema of the current agent, "
|
|
53
|
+
"current_agent_output: str | Output schema of the current agent, "
|
|
54
|
+
"current_result: dict | Output from the current agent, "
|
|
55
|
+
"available_agents: list[AgentInfo] | List of available agents"
|
|
56
|
+
),
|
|
57
|
+
output="agent_name: str | Name of the agent to hand off to, confidence: float | Confidence in the decision, reasoning: str | Reasoning for the decision",
|
|
58
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Default router implementation for the Flock framework."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Default router implementation for the Flock framework."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from flock.core.context.context import FlockContext
|
|
9
|
+
from flock.core.flock_agent import FlockAgent
|
|
10
|
+
from flock.core.flock_router import (
|
|
11
|
+
FlockRouter,
|
|
12
|
+
FlockRouterConfig,
|
|
13
|
+
HandOffRequest,
|
|
14
|
+
)
|
|
15
|
+
from flock.core.logging.logging import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger("default_router")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DefaultRouterConfig(FlockRouterConfig):
|
|
21
|
+
"""Configuration for the default router."""
|
|
22
|
+
|
|
23
|
+
hand_off: str | HandOffRequest | Callable[..., HandOffRequest] = Field(
|
|
24
|
+
default="", description="Next agent to hand off to"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DefaultRouter(FlockRouter):
|
|
29
|
+
"""Default router implementation.
|
|
30
|
+
|
|
31
|
+
This router simply uses the agent's hand_off property to determine the next agent.
|
|
32
|
+
It does not perform any dynamic routing.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name: str = "default_router"
|
|
36
|
+
config: DefaultRouterConfig = Field(
|
|
37
|
+
default_factory=DefaultRouterConfig, description="Output configuration"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
name: str = "default_router",
|
|
43
|
+
config: DefaultRouterConfig | None = None,
|
|
44
|
+
):
|
|
45
|
+
"""Initialize the DefaultRouter.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
name: The name of the router
|
|
49
|
+
config: The router configuration
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(
|
|
52
|
+
name=name, config=config or DefaultRouterConfig(name=name)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def route(
|
|
56
|
+
self,
|
|
57
|
+
current_agent: FlockAgent,
|
|
58
|
+
result: dict[str, Any],
|
|
59
|
+
context: FlockContext,
|
|
60
|
+
) -> HandOffRequest:
|
|
61
|
+
"""Determine the next agent to hand off to based on the current agent's output.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
current_agent: The agent that just completed execution
|
|
65
|
+
result: The output from the current agent
|
|
66
|
+
context: The global execution context
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A HandOff object containing the next agent and input data
|
|
70
|
+
"""
|
|
71
|
+
handoff = self.config.hand_off
|
|
72
|
+
if callable(handoff):
|
|
73
|
+
handoff = handoff(context, result)
|
|
74
|
+
if isinstance(handoff, str):
|
|
75
|
+
handoff = HandOffRequest(next_agent=handoff, hand_off_mode="match")
|
|
76
|
+
return handoff
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""LLM-based router implementation for the Flock framework."""
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""LLM-based router implementation for the Flock framework."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import litellm
|
|
7
|
+
|
|
8
|
+
from flock.core.context.context import FlockContext
|
|
9
|
+
from flock.core.flock_agent import FlockAgent
|
|
10
|
+
from flock.core.flock_router import (
|
|
11
|
+
FlockRouter,
|
|
12
|
+
FlockRouterConfig,
|
|
13
|
+
HandOffRequest,
|
|
14
|
+
)
|
|
15
|
+
from flock.core.logging.logging import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger("llm_router")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LLMRouterConfig(FlockRouterConfig):
|
|
21
|
+
"""Configuration for the LLM router.
|
|
22
|
+
|
|
23
|
+
This class extends FlockRouterConfig with parameters specific to the LLM router.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
temperature: float = 0.2
|
|
27
|
+
max_tokens: int = 500
|
|
28
|
+
confidence_threshold: float = 0.5
|
|
29
|
+
prompt: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LLMRouter(FlockRouter):
|
|
33
|
+
"""Router that uses an LLM to determine the next agent in a workflow.
|
|
34
|
+
|
|
35
|
+
This class is responsible for:
|
|
36
|
+
1. Analyzing available agents in the registry
|
|
37
|
+
2. Using an LLM to score each agent's suitability as the next step
|
|
38
|
+
3. Selecting the highest-scoring agent
|
|
39
|
+
4. Creating a HandOff object with the selected agent
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
name: str = "llm_router",
|
|
45
|
+
config: LLMRouterConfig | None = None,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize the LLMRouter.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
registry: The agent registry containing all available agents
|
|
51
|
+
name: The name of the router
|
|
52
|
+
config: The router configuration
|
|
53
|
+
"""
|
|
54
|
+
logger.info(f"Initializing LLM Router '{name}'")
|
|
55
|
+
super().__init__(name=name, config=config or LLMRouterConfig(name=name))
|
|
56
|
+
logger.debug(
|
|
57
|
+
"LLM Router configuration",
|
|
58
|
+
temperature=self.config.temperature,
|
|
59
|
+
max_tokens=self.config.max_tokens,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async def route(
|
|
63
|
+
self,
|
|
64
|
+
current_agent: FlockAgent,
|
|
65
|
+
result: dict[str, Any],
|
|
66
|
+
context: FlockContext,
|
|
67
|
+
) -> HandOffRequest:
|
|
68
|
+
"""Determine the next agent to hand off to based on the current agent's output.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
current_agent: The agent that just completed execution
|
|
72
|
+
result: The output from the current agent
|
|
73
|
+
context: The global execution context
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
A HandOff object containing the next agent and input data
|
|
77
|
+
"""
|
|
78
|
+
logger.info(
|
|
79
|
+
f"Routing from agent '{current_agent.name}'",
|
|
80
|
+
current_agent=current_agent.name,
|
|
81
|
+
)
|
|
82
|
+
logger.debug("Current agent result", result=result)
|
|
83
|
+
|
|
84
|
+
agent_definitions = context.agent_definitions
|
|
85
|
+
# Get all available agents from the registry
|
|
86
|
+
available_agents = self._get_available_agents(
|
|
87
|
+
agent_definitions, current_agent.name
|
|
88
|
+
)
|
|
89
|
+
logger.debug(
|
|
90
|
+
"Available agents for routing",
|
|
91
|
+
count=len(available_agents),
|
|
92
|
+
agents=[a.agent_data["name"] for a in available_agents],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if not available_agents:
|
|
96
|
+
logger.warning(
|
|
97
|
+
"No available agents for routing",
|
|
98
|
+
current_agent=current_agent.name,
|
|
99
|
+
)
|
|
100
|
+
return HandOffRequest(
|
|
101
|
+
next_agent="", override_next_agent={}, override_context=None
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Use LLM to determine the best next agent
|
|
105
|
+
next_agent_name, score = await self._select_next_agent(
|
|
106
|
+
current_agent, result, available_agents
|
|
107
|
+
)
|
|
108
|
+
logger.info(
|
|
109
|
+
"Agent selection result",
|
|
110
|
+
next_agent=next_agent_name,
|
|
111
|
+
score=score,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not next_agent_name or score < self.config.confidence_threshold:
|
|
115
|
+
logger.warning(
|
|
116
|
+
"No suitable next agent found",
|
|
117
|
+
best_score=score,
|
|
118
|
+
)
|
|
119
|
+
return HandOffRequest(
|
|
120
|
+
next_agent="", override_next_agent={}, override_context=None
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Get the next agent from the registry
|
|
124
|
+
next_agent = agent_definitions.get(next_agent_name)
|
|
125
|
+
if not next_agent:
|
|
126
|
+
logger.error(
|
|
127
|
+
"Selected agent not found in registry",
|
|
128
|
+
agent_name=next_agent_name,
|
|
129
|
+
)
|
|
130
|
+
return HandOffRequest(
|
|
131
|
+
next_agent="", override_next_agent={}, override_context=None
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Create input for the next agent
|
|
135
|
+
|
|
136
|
+
logger.success(
|
|
137
|
+
f"Successfully routed to agent '{next_agent_name}'",
|
|
138
|
+
score=score,
|
|
139
|
+
from_agent=current_agent.name,
|
|
140
|
+
)
|
|
141
|
+
return HandOffRequest(
|
|
142
|
+
next_agent=next_agent_name,
|
|
143
|
+
hand_off_mode="add",
|
|
144
|
+
override_next_agent=None,
|
|
145
|
+
override_context=None,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _get_available_agents(
|
|
149
|
+
self, agent_definitions: dict[str, Any], current_agent_name: str
|
|
150
|
+
) -> list[FlockAgent]:
|
|
151
|
+
"""Get all available agents except the current one.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
current_agent_name: Name of the current agent to exclude
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
List of available agents
|
|
158
|
+
"""
|
|
159
|
+
logger.debug(
|
|
160
|
+
"Getting available agents",
|
|
161
|
+
total_agents=len(agent_definitions),
|
|
162
|
+
current_agent=current_agent_name,
|
|
163
|
+
)
|
|
164
|
+
agents = []
|
|
165
|
+
for agent in agent_definitions:
|
|
166
|
+
if agent != current_agent_name:
|
|
167
|
+
agents.append(agent_definitions.get(agent))
|
|
168
|
+
return agents
|
|
169
|
+
|
|
170
|
+
async def _select_next_agent(
|
|
171
|
+
self,
|
|
172
|
+
current_agent: FlockAgent,
|
|
173
|
+
result: dict[str, Any],
|
|
174
|
+
available_agents: list[FlockAgent],
|
|
175
|
+
) -> tuple[str, float]:
|
|
176
|
+
"""Use an LLM to select the best next agent.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
current_agent: The agent that just completed execution
|
|
180
|
+
result: The output from the current agent
|
|
181
|
+
available_agents: List of available agents to choose from
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Tuple of (selected_agent_name, confidence_score)
|
|
185
|
+
"""
|
|
186
|
+
logger.debug(
|
|
187
|
+
"Selecting next agent",
|
|
188
|
+
current_agent=current_agent.name,
|
|
189
|
+
available_count=len(available_agents),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Prepare the prompt for the LLM
|
|
193
|
+
prompt = self._create_selection_prompt(
|
|
194
|
+
current_agent, result, available_agents
|
|
195
|
+
)
|
|
196
|
+
logger.debug("Generated selection prompt", prompt_length=len(prompt))
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
logger.info(
|
|
200
|
+
"Calling LLM for agent selection",
|
|
201
|
+
model=current_agent.model,
|
|
202
|
+
temperature=self.config.temperature,
|
|
203
|
+
)
|
|
204
|
+
# Call the LLM to get the next agent
|
|
205
|
+
response = await litellm.acompletion(
|
|
206
|
+
model=current_agent.model,
|
|
207
|
+
messages=[{"role": "user", "content": prompt}],
|
|
208
|
+
temperature=self.config.temperature
|
|
209
|
+
if isinstance(self.config, LLMRouterConfig)
|
|
210
|
+
else 0.2,
|
|
211
|
+
max_tokens=self.config.max_tokens
|
|
212
|
+
if isinstance(self.config, LLMRouterConfig)
|
|
213
|
+
else 500,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
content = response.choices[0].message.content
|
|
217
|
+
# Parse the response to get the agent name and score
|
|
218
|
+
try:
|
|
219
|
+
# extract the json object from the response
|
|
220
|
+
content = content.split("```json")[1].split("```")[0]
|
|
221
|
+
data = json.loads(content)
|
|
222
|
+
next_agent = data.get("next_agent", "")
|
|
223
|
+
score = float(data.get("score", 0))
|
|
224
|
+
reasoning = data.get("reasoning", "")
|
|
225
|
+
logger.info(
|
|
226
|
+
"Successfully parsed LLM response",
|
|
227
|
+
next_agent=next_agent,
|
|
228
|
+
score=score,
|
|
229
|
+
reasoning=reasoning,
|
|
230
|
+
)
|
|
231
|
+
return next_agent, score
|
|
232
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
233
|
+
logger.error(
|
|
234
|
+
"Failed to parse LLM response",
|
|
235
|
+
error=str(e),
|
|
236
|
+
raw_response=content,
|
|
237
|
+
)
|
|
238
|
+
logger.debug("Attempting fallback parsing")
|
|
239
|
+
|
|
240
|
+
# Fallback: try to extract the agent name from the text
|
|
241
|
+
for agent in available_agents:
|
|
242
|
+
if agent.agent_data["name"] in content:
|
|
243
|
+
logger.info(
|
|
244
|
+
"Found agent name in response using fallback",
|
|
245
|
+
agent=agent.agent_data["name"],
|
|
246
|
+
)
|
|
247
|
+
return agent.agent_data[
|
|
248
|
+
"name"
|
|
249
|
+
], 0.6 # Default score for fallback
|
|
250
|
+
|
|
251
|
+
return "", 0.0
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.error(
|
|
255
|
+
"Error calling LLM for agent selection",
|
|
256
|
+
error=str(e),
|
|
257
|
+
current_agent=current_agent.name,
|
|
258
|
+
)
|
|
259
|
+
return "", 0.0
|
|
260
|
+
|
|
261
|
+
def _create_selection_prompt(
|
|
262
|
+
self,
|
|
263
|
+
current_agent: FlockAgent,
|
|
264
|
+
result: dict[str, Any],
|
|
265
|
+
available_agents: list[FlockAgent],
|
|
266
|
+
) -> str:
|
|
267
|
+
"""Create a prompt for the LLM to select the next agent.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
current_agent: The agent that just completed execution
|
|
271
|
+
result: The output from the current agent
|
|
272
|
+
available_agents: List of available agents to choose from
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Prompt string for the LLM
|
|
276
|
+
"""
|
|
277
|
+
# Format the current agent's output
|
|
278
|
+
result_str = json.dumps(result, indent=2)
|
|
279
|
+
|
|
280
|
+
# Format the available agents' information
|
|
281
|
+
agents_info = []
|
|
282
|
+
for agent in available_agents:
|
|
283
|
+
agent_info = {
|
|
284
|
+
"name": agent.agent_data["name"],
|
|
285
|
+
"description": agent.agent_data["description"]
|
|
286
|
+
if agent.agent_data["description"]
|
|
287
|
+
else "",
|
|
288
|
+
"input": agent.agent_data["input"],
|
|
289
|
+
"output": agent.agent_data["output"],
|
|
290
|
+
}
|
|
291
|
+
agents_info.append(agent_info)
|
|
292
|
+
|
|
293
|
+
agents_str = json.dumps(agents_info, indent=2)
|
|
294
|
+
|
|
295
|
+
# Create the prompt
|
|
296
|
+
if self.config.prompt:
|
|
297
|
+
prompt = self.config.prompt
|
|
298
|
+
else:
|
|
299
|
+
prompt = f"""
|
|
300
|
+
You are a workflow router that determines the next agent to execute in a multi-agent system.
|
|
301
|
+
|
|
302
|
+
CURRENT AGENT:
|
|
303
|
+
Name: {current_agent.name}
|
|
304
|
+
Description: {current_agent.description}
|
|
305
|
+
Input: {current_agent.input}
|
|
306
|
+
Output: {current_agent.output}
|
|
307
|
+
|
|
308
|
+
CURRENT AGENT'S OUTPUT:
|
|
309
|
+
{result_str}
|
|
310
|
+
|
|
311
|
+
AVAILABLE AGENTS:
|
|
312
|
+
{agents_str}
|
|
313
|
+
|
|
314
|
+
Based on the current agent's output and the available agents, determine which agent should be executed next.
|
|
315
|
+
Consider the following:
|
|
316
|
+
1. Which agent's input requirements best match the current agent's output?
|
|
317
|
+
2. Which agent's purpose and description make it the most logical next step?
|
|
318
|
+
3. Which agent would provide the most value in continuing the workflow?
|
|
319
|
+
|
|
320
|
+
Respond with a JSON object containing:
|
|
321
|
+
1. "next_agent": The name of the selected agent
|
|
322
|
+
2. "score": A confidence score between 0 and 1 indicating how suitable this agent is
|
|
323
|
+
3. "reasoning": A brief explanation of why this agent was selected
|
|
324
|
+
|
|
325
|
+
If no agent is suitable, set "next_agent" to an empty string and "score" to 0.
|
|
326
|
+
|
|
327
|
+
JSON Response:
|
|
328
|
+
"""
|
|
329
|
+
return prompt
|
|
330
|
+
|
|
331
|
+
def _create_next_input(
|
|
332
|
+
self,
|
|
333
|
+
current_agent: FlockAgent,
|
|
334
|
+
result: dict[str, Any],
|
|
335
|
+
next_agent: FlockAgent,
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
"""Create the input for the next agent, including the previous agent's output.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
current_agent: The agent that just completed execution
|
|
341
|
+
result: The output from the current agent
|
|
342
|
+
next_agent: The next agent to execute
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Input dictionary for the next agent
|
|
346
|
+
"""
|
|
347
|
+
# Start with an empty input
|
|
348
|
+
next_input = {}
|
|
349
|
+
|
|
350
|
+
# Add a special field for the previous agent's output
|
|
351
|
+
next_input["previous_agent_output"] = {
|
|
352
|
+
"agent_name": current_agent.name,
|
|
353
|
+
"result": result,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Try to map the current agent's output to the next agent's input
|
|
357
|
+
# This is a simple implementation that could be enhanced with more sophisticated mapping
|
|
358
|
+
for key in result:
|
|
359
|
+
# If the next agent expects this key, add it directly
|
|
360
|
+
if key in next_agent.input:
|
|
361
|
+
next_input[key] = result[key]
|
|
362
|
+
|
|
363
|
+
return next_input
|
flock/workflow/activities.py
CHANGED
|
@@ -7,7 +7,8 @@ from temporalio import activity
|
|
|
7
7
|
|
|
8
8
|
from flock.core.context.context import FlockContext
|
|
9
9
|
from flock.core.context.context_vars import FLOCK_CURRENT_AGENT
|
|
10
|
-
from flock.core.flock_agent import FlockAgent
|
|
10
|
+
from flock.core.flock_agent import FlockAgent
|
|
11
|
+
from flock.core.flock_router import HandOffRequest
|
|
11
12
|
from flock.core.logging.logging import get_logger
|
|
12
13
|
from flock.core.registry.agent_registry import Registry
|
|
13
14
|
from flock.core.util.input_resolver import resolve_inputs
|
|
@@ -74,49 +75,83 @@ async def run_agent(context: FlockContext) -> dict:
|
|
|
74
75
|
exec_span.record_exception(e)
|
|
75
76
|
raise
|
|
76
77
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
result,
|
|
82
|
-
timestamp=datetime.now().isoformat(),
|
|
83
|
-
hand_off=None,
|
|
84
|
-
called_from=previous_agent_name,
|
|
85
|
-
)
|
|
78
|
+
# Determine the next agent using the handoff router if available
|
|
79
|
+
handoff_data = HandOffRequest()
|
|
80
|
+
|
|
81
|
+
if agent.handoff_router:
|
|
86
82
|
logger.info(
|
|
87
|
-
"
|
|
83
|
+
f"Using handoff router: {agent.handoff_router.__class__.__name__}",
|
|
84
|
+
agent=agent.name,
|
|
88
85
|
)
|
|
89
|
-
iter_span.add_event("chain completed")
|
|
90
|
-
return result
|
|
91
|
-
|
|
92
|
-
# Determine the next agent.
|
|
93
|
-
handoff_data = HandOff()
|
|
94
|
-
if callable(agent.hand_off):
|
|
95
|
-
logger.debug("Executing handoff function", agent=agent.name)
|
|
96
86
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
# Route to the next agent
|
|
88
|
+
handoff_data = await agent.handoff_router.route(
|
|
89
|
+
agent, result, context
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if callable(handoff_data):
|
|
93
|
+
logger.debug(
|
|
94
|
+
"Executing handoff function", agent=agent.name
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
handoff_data = handoff_data(context, result)
|
|
98
|
+
if isinstance(
|
|
99
|
+
handoff_data.next_agent, FlockAgent
|
|
100
|
+
):
|
|
101
|
+
handoff_data.next_agent = (
|
|
102
|
+
handoff_data.next_agent.name
|
|
103
|
+
)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(
|
|
106
|
+
"Handoff function error {} {}",
|
|
107
|
+
agent=agent.name,
|
|
108
|
+
error=str(e),
|
|
109
|
+
)
|
|
110
|
+
iter_span.record_exception(e)
|
|
111
|
+
return {"error": f"Handoff function error: {e}"}
|
|
112
|
+
elif isinstance(handoff_data.next_agent, FlockAgent):
|
|
99
113
|
handoff_data.next_agent = (
|
|
100
114
|
handoff_data.next_agent.name
|
|
101
115
|
)
|
|
116
|
+
|
|
117
|
+
if not handoff_data.next_agent:
|
|
118
|
+
logger.info(
|
|
119
|
+
"Router found no suitable next agent",
|
|
120
|
+
agent=agent.name,
|
|
121
|
+
)
|
|
122
|
+
context.record(
|
|
123
|
+
agent.name,
|
|
124
|
+
result,
|
|
125
|
+
timestamp=datetime.now().isoformat(),
|
|
126
|
+
hand_off=None,
|
|
127
|
+
called_from=previous_agent_name,
|
|
128
|
+
)
|
|
129
|
+
logger.info("Completing chain", agent=agent.name)
|
|
130
|
+
iter_span.add_event("chain completed")
|
|
131
|
+
return result
|
|
102
132
|
except Exception as e:
|
|
103
133
|
logger.error(
|
|
104
|
-
"
|
|
105
|
-
agent
|
|
106
|
-
|
|
134
|
+
"Router error {} {}",
|
|
135
|
+
agent.name,
|
|
136
|
+
str(e),
|
|
107
137
|
)
|
|
108
138
|
iter_span.record_exception(e)
|
|
109
|
-
return {"error": f"
|
|
110
|
-
elif isinstance(agent.hand_off, str | FlockAgent):
|
|
111
|
-
handoff_data.next_agent = (
|
|
112
|
-
agent.hand_off
|
|
113
|
-
if isinstance(agent.hand_off, str)
|
|
114
|
-
else agent.hand_off.name
|
|
115
|
-
)
|
|
139
|
+
return {"error": f"Router error: {e}"}
|
|
116
140
|
else:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
# No router, so no handoff
|
|
142
|
+
logger.info(
|
|
143
|
+
"No handoff router defined, completing chain",
|
|
144
|
+
agent=agent.name,
|
|
145
|
+
)
|
|
146
|
+
context.record(
|
|
147
|
+
agent.name,
|
|
148
|
+
result,
|
|
149
|
+
timestamp=datetime.now().isoformat(),
|
|
150
|
+
hand_off=None,
|
|
151
|
+
called_from=previous_agent_name,
|
|
152
|
+
)
|
|
153
|
+
iter_span.add_event("chain completed")
|
|
154
|
+
return result
|
|
120
155
|
|
|
121
156
|
# Record the agent run in the context.
|
|
122
157
|
context.record(
|
|
@@ -127,10 +162,15 @@ async def run_agent(context: FlockContext) -> dict:
|
|
|
127
162
|
called_from=previous_agent_name,
|
|
128
163
|
)
|
|
129
164
|
previous_agent_name = agent.name
|
|
165
|
+
previous_agent_output = agent.output
|
|
166
|
+
if handoff_data.override_context:
|
|
167
|
+
context.update(handoff_data.override_context)
|
|
130
168
|
|
|
131
169
|
# Prepare the next agent.
|
|
132
170
|
try:
|
|
133
171
|
agent = registry.get_agent(handoff_data.next_agent)
|
|
172
|
+
if handoff_data.hand_off_mode == "add":
|
|
173
|
+
agent.input = previous_agent_output + ", " + agent.input
|
|
134
174
|
agent.resolve_callables(context=context)
|
|
135
175
|
if not agent:
|
|
136
176
|
logger.error(
|
|
@@ -147,6 +187,7 @@ async def run_agent(context: FlockContext) -> dict:
|
|
|
147
187
|
}
|
|
148
188
|
|
|
149
189
|
context.set_variable(FLOCK_CURRENT_AGENT, agent.name)
|
|
190
|
+
|
|
150
191
|
logger.info("Handing off to next agent", next=agent.name)
|
|
151
192
|
iter_span.set_attribute("next.agent", agent.name)
|
|
152
193
|
except Exception as e:
|
|
@@ -8,14 +8,15 @@ flock/cli/load_examples.py,sha256=DkeLUlrb7rGx3nZ04aADU9HXXu5mZTf_DBwT0xhzIv4,7
|
|
|
8
8
|
flock/cli/load_flock.py,sha256=3JdECvt5X7uyOG2vZS3-Zk5C5SI_84_QZjcsB3oJmfA,932
|
|
9
9
|
flock/cli/load_release_notes.py,sha256=qFcgUrMddAE_TP6x1P-6ZywTUjTknfhTDW5LTxtg1yk,599
|
|
10
10
|
flock/cli/settings.py,sha256=DkeLUlrb7rGx3nZ04aADU9HXXu5mZTf_DBwT0xhzIv4,7
|
|
11
|
-
flock/cli/assets/release_notes.md,sha256
|
|
11
|
+
flock/cli/assets/release_notes.md,sha256=-RuE-G9Sn8z1LWEdr9iqjuQN7N1K_JMaCzHYoyLR42U,4793
|
|
12
12
|
flock/core/__init__.py,sha256=mPlvKc0SxC2qCvSlgYeP_7EyV8ptmdn24NO8mlQoCSo,559
|
|
13
13
|
flock/core/flock.py,sha256=1LPMblsvT90Na35LXx0w3Us66yIaTzsokL7lF5fsVX8,19228
|
|
14
|
-
flock/core/flock_agent.py,sha256=
|
|
14
|
+
flock/core/flock_agent.py,sha256=LjSuAYRFh4UWV8Z4_mguMme2GnMQwteNFyion4Memt8,11705
|
|
15
15
|
flock/core/flock_api.py,sha256=SKQVKgFCaNCqHtwvIcksnpqG6ajHodVhs3oaKUw-d8c,7192
|
|
16
16
|
flock/core/flock_evaluator.py,sha256=j7riJj_KsWoBnKmLiGp-U0CRhxDyJbgEdLGN26tfKm8,1588
|
|
17
|
-
flock/core/flock_factory.py,sha256=
|
|
18
|
-
flock/core/flock_module.py,sha256=
|
|
17
|
+
flock/core/flock_factory.py,sha256=xTz2VqcLjaHn60b3Oaz8JCJdcIQra0vc5NHqh3Dy44s,2617
|
|
18
|
+
flock/core/flock_module.py,sha256=3DmxOc39gQS-tiJcgUCjMaLr8QDDJR4acV_M76Xcf6I,2602
|
|
19
|
+
flock/core/flock_router.py,sha256=A5GaxcGvtiFlRLHBTW7okh5RDm3BdKam2uXvRHRaj7k,2187
|
|
19
20
|
flock/core/context/context.py,sha256=AW0qKIAkgZucVroGsulrPVPc4WmWuqWIrVPHf2qaOLI,6380
|
|
20
21
|
flock/core/context/context_manager.py,sha256=qMySVny_dbTNLh21RHK_YT0mNKIOrqJDZpi9ZVdBsxU,1103
|
|
21
22
|
flock/core/context/context_vars.py,sha256=0Hn6fM2iNc0_jIIU0B7KX-K2o8qXqtZ5EYtwujETQ7U,272
|
|
@@ -46,7 +47,7 @@ flock/core/util/hydrator.py,sha256=6qNwOwCZB7r6y25BZ--0PGofrAlfMaXbDKFQeP5NLts,1
|
|
|
46
47
|
flock/core/util/input_resolver.py,sha256=g9vDPdY4OH-G7qjas5ksGEHueokHGFPMoLOvC-ngeLo,5984
|
|
47
48
|
flock/evaluators/declarative/declarative_evaluator.py,sha256=f8ldgZZp94zC4CoGzBufKvbvtckCGBe9EHTOoAZfZK0,1695
|
|
48
49
|
flock/evaluators/natural_language/natural_language_evaluator.py,sha256=6nVEeh8_uwv_h-d3FWlA0GbzDzRtdhvxCGKirHtyvOU,2012
|
|
49
|
-
flock/evaluators/zep/zep_evaluator.py,sha256=
|
|
50
|
+
flock/evaluators/zep/zep_evaluator.py,sha256=9NOELl7JAuUcx_FQrxY6b-_vN3MjwDyW7ZppPIGeCFc,1954
|
|
50
51
|
flock/modules/callback/callback_module.py,sha256=hCCw-HNYjK4aHnUQfvw26ZP1Q_jdlKb9kDh3BHzbCQA,2916
|
|
51
52
|
flock/modules/memory/memory_module.py,sha256=2grdmvw7FJWZvz0IjgASbDPCfyS1w4gWkRzOWtK7BFM,8214
|
|
52
53
|
flock/modules/memory/memory_parser.py,sha256=2S7CmVEsm22gD7-MiFj4318FTg8wd_jB-RKMwXI14WM,4369
|
|
@@ -56,6 +57,14 @@ flock/modules/performance/metrics_module.py,sha256=K5z5bizIjA4ZEUjBk5ShwTR9ZElR-
|
|
|
56
57
|
flock/modules/zep/zep_module.py,sha256=BIJ5K-hg2bLeJmGKoDcVY1rVN7_0yYETiSaVrO-gtMI,5830
|
|
57
58
|
flock/platform/docker_tools.py,sha256=fpA7-6rJBjPOUBLdQP4ny2QPgJ_042nmqRn5GtKnoYw,1445
|
|
58
59
|
flock/platform/jaeger_install.py,sha256=MyOMJQx4TQSMYvdUJxfiGSo3YCtsfkbNXcAcQ9bjETA,2898
|
|
60
|
+
flock/routers/__init__.py,sha256=w9uL34Auuo26-q_EGlE8Z9iHsw6S8qutTAH_ZI7pn7M,39
|
|
61
|
+
flock/routers/agent/__init__.py,sha256=0ZOYpR8BMnR5iCGfcUiv99g7aT_g13xvm2Shl-XzybY,65
|
|
62
|
+
flock/routers/agent/agent_router.py,sha256=9s3AwcBqpyhpPXOTqyMSVtS8Bcme1RDdqSUfWIqEBfc,8139
|
|
63
|
+
flock/routers/agent/handoff_agent.py,sha256=p-0XEPXIyv1T3DGAhhXg2SYXmrwEaJ5pnuLgRSvbiZg,1903
|
|
64
|
+
flock/routers/default/__init__.py,sha256=DOatGX_aE2DWvf55a0Tv7qDK05QFD-hL3sm7g58hmLU,61
|
|
65
|
+
flock/routers/default/default_router.py,sha256=D9TCAAeNfzt3Se6QduGO2TmZ6038XlQLV6Y1u5IGI-0,2232
|
|
66
|
+
flock/routers/llm/__init__.py,sha256=OV89ebq8RPWZwCJTS2_P46Q0yKD_03rwq_fBOsETd08,63
|
|
67
|
+
flock/routers/llm/llm_router.py,sha256=3WXUK2TqZENYXSFb7o_WtpONq0SsebaZZpytCRr1daw,12217
|
|
59
68
|
flock/themes/3024-day.toml,sha256=uOVHqEzSyHx0WlUk3D0lne4RBsNBAPCTy3C58yU7kEY,667
|
|
60
69
|
flock/themes/3024-night.toml,sha256=qsXUwd6ZYz6J-R129_Ao2TKlvvK60svhZJJjB5c8Tfo,1667
|
|
61
70
|
flock/themes/aardvark-blue.toml,sha256=5ZgsxP3pWLPN3yJ2Wd9ErCo7fy_VJpIfje4kriDKlqo,1667
|
|
@@ -393,12 +402,12 @@ flock/themes/zenburned.toml,sha256=UEmquBbcAO3Zj652XKUwCsNoC2iQSlIh-q5c6DH-7Kc,1
|
|
|
393
402
|
flock/themes/zenwritten-dark.toml,sha256=To5l6520_3UqAGiEumpzGWsHhXxqu9ThrMildXKgIO0,1669
|
|
394
403
|
flock/themes/zenwritten-light.toml,sha256=G1iEheCPfBNsMTGaVpEVpDzYBHA_T-MV27rolUYolmE,1666
|
|
395
404
|
flock/workflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
396
|
-
flock/workflow/activities.py,sha256=
|
|
405
|
+
flock/workflow/activities.py,sha256=PqUc1jecg-7qq4KQr0Gr2XLNCABZ79fvD1MqazT--YU,8866
|
|
397
406
|
flock/workflow/agent_activities.py,sha256=NhBZscflEf2IMfSRa_pBM_TRP7uVEF_O0ROvWZ33eDc,963
|
|
398
407
|
flock/workflow/temporal_setup.py,sha256=VWBgmBgfTBjwM5ruS_dVpA5AVxx6EZ7oFPGw4j3m0l0,1091
|
|
399
408
|
flock/workflow/workflow.py,sha256=I9MryXW_bqYVTHx-nl2epbTqeRy27CAWHHA7ZZA0nAk,1696
|
|
400
|
-
flock_core-0.3.
|
|
401
|
-
flock_core-0.3.
|
|
402
|
-
flock_core-0.3.
|
|
403
|
-
flock_core-0.3.
|
|
404
|
-
flock_core-0.3.
|
|
409
|
+
flock_core-0.3.8.dist-info/METADATA,sha256=0ImaV46N5uSNlZPP4kF2q-pjfY3nsLLHzDvicvv7_gA,20494
|
|
410
|
+
flock_core-0.3.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
411
|
+
flock_core-0.3.8.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
|
|
412
|
+
flock_core-0.3.8.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
|
|
413
|
+
flock_core-0.3.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|