flock-core 0.4.520__py3-none-any.whl → 0.5.0b2__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/manage_agents.py +3 -3
- flock/components/__init__.py +28 -0
- flock/components/evaluation/__init__.py +9 -0
- flock/components/evaluation/declarative_evaluation_component.py +198 -0
- flock/components/routing/__init__.py +15 -0
- flock/{routers/conditional/conditional_router.py → components/routing/conditional_routing_component.py} +60 -49
- flock/components/routing/default_routing_component.py +103 -0
- flock/components/routing/llm_routing_component.py +208 -0
- flock/components/utility/__init__.py +15 -0
- flock/{modules/enterprise_memory/enterprise_memory_module.py → components/utility/memory_utility_component.py} +195 -173
- flock/{modules/performance/metrics_module.py → components/utility/metrics_utility_component.py} +101 -86
- flock/{modules/output/output_module.py → components/utility/output_utility_component.py} +49 -49
- flock/core/__init__.py +2 -8
- flock/core/agent/__init__.py +16 -0
- flock/core/agent/flock_agent_components.py +104 -0
- flock/core/agent/flock_agent_execution.py +101 -0
- flock/core/agent/flock_agent_integration.py +147 -0
- flock/core/agent/flock_agent_lifecycle.py +177 -0
- flock/core/agent/flock_agent_serialization.py +378 -0
- flock/core/component/__init__.py +15 -0
- flock/core/{flock_module.py → component/agent_component_base.py} +136 -35
- flock/core/component/evaluation_component_base.py +56 -0
- flock/core/component/routing_component_base.py +75 -0
- flock/core/component/utility_component_base.py +69 -0
- flock/core/config/flock_agent_config.py +49 -2
- flock/core/evaluation/utils.py +1 -1
- flock/core/execution/evaluation_executor.py +1 -1
- flock/core/flock.py +137 -483
- flock/core/flock_agent.py +151 -1018
- flock/core/flock_factory.py +94 -73
- flock/core/{flock_registry.py → flock_registry.py.backup} +3 -17
- flock/core/logging/logging.py +1 -0
- flock/core/mcp/flock_mcp_server.py +42 -37
- flock/core/mixin/dspy_integration.py +5 -5
- flock/core/orchestration/__init__.py +18 -0
- flock/core/orchestration/flock_batch_processor.py +94 -0
- flock/core/orchestration/flock_evaluator.py +113 -0
- flock/core/orchestration/flock_execution.py +288 -0
- flock/core/orchestration/flock_initialization.py +125 -0
- flock/core/orchestration/flock_server_manager.py +65 -0
- flock/core/orchestration/flock_web_server.py +117 -0
- flock/core/registry/__init__.py +39 -0
- flock/core/registry/agent_registry.py +69 -0
- flock/core/registry/callable_registry.py +139 -0
- flock/core/registry/component_discovery.py +142 -0
- flock/core/registry/component_registry.py +64 -0
- flock/core/registry/config_mapping.py +64 -0
- flock/core/registry/decorators.py +137 -0
- flock/core/registry/registry_hub.py +202 -0
- flock/core/registry/server_registry.py +57 -0
- flock/core/registry/type_registry.py +86 -0
- flock/core/serialization/flock_serializer.py +33 -30
- flock/core/serialization/serialization_utils.py +28 -25
- flock/core/util/input_resolver.py +29 -2
- flock/platform/docker_tools.py +3 -3
- flock/tools/markdown_tools.py +1 -2
- flock/tools/text_tools.py +1 -2
- flock/webapp/app/main.py +9 -5
- flock/workflow/activities.py +59 -84
- flock/workflow/activities_unified.py +230 -0
- flock/workflow/agent_execution_activity.py +6 -6
- flock/workflow/flock_workflow.py +1 -1
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/METADATA +2 -2
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/RECORD +67 -68
- flock/core/flock_evaluator.py +0 -60
- flock/core/flock_router.py +0 -83
- flock/evaluators/__init__.py +0 -1
- flock/evaluators/declarative/__init__.py +0 -1
- flock/evaluators/declarative/declarative_evaluator.py +0 -194
- flock/evaluators/memory/memory_evaluator.py +0 -90
- flock/evaluators/test/test_case_evaluator.py +0 -38
- flock/evaluators/zep/zep_evaluator.py +0 -59
- flock/modules/__init__.py +0 -1
- flock/modules/assertion/__init__.py +0 -1
- flock/modules/assertion/assertion_module.py +0 -286
- flock/modules/callback/__init__.py +0 -1
- flock/modules/callback/callback_module.py +0 -91
- flock/modules/enterprise_memory/README.md +0 -99
- flock/modules/mem0/__init__.py +0 -1
- flock/modules/mem0/mem0_module.py +0 -126
- flock/modules/mem0_async/__init__.py +0 -1
- flock/modules/mem0_async/async_mem0_module.py +0 -126
- flock/modules/memory/__init__.py +0 -1
- flock/modules/memory/memory_module.py +0 -429
- flock/modules/memory/memory_parser.py +0 -125
- flock/modules/memory/memory_storage.py +0 -736
- flock/modules/output/__init__.py +0 -1
- flock/modules/performance/__init__.py +0 -1
- flock/modules/zep/__init__.py +0 -1
- flock/modules/zep/zep_module.py +0 -192
- flock/routers/__init__.py +0 -1
- flock/routers/agent/__init__.py +0 -1
- flock/routers/agent/agent_router.py +0 -236
- flock/routers/agent/handoff_agent.py +0 -58
- flock/routers/default/__init__.py +0 -1
- flock/routers/default/default_router.py +0 -80
- flock/routers/feedback/feedback_router.py +0 -114
- flock/routers/list_generator/list_generator_router.py +0 -166
- flock/routers/llm/__init__.py +0 -1
- flock/routers/llm/llm_router.py +0 -365
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/WHEEL +0 -0
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/licenses/LICENSE +0 -0
flock/cli/manage_agents.py
CHANGED
|
@@ -168,8 +168,8 @@ def _view_agent_details(agent: FlockAgent):
|
|
|
168
168
|
basic_info.add_row("Description", agent.resolved_description if agent.resolved_description else "N/A")
|
|
169
169
|
basic_info.add_row("Input", str(agent.input))
|
|
170
170
|
basic_info.add_row("Output", str(agent.output))
|
|
171
|
-
basic_info.add_row("Write to File", str(agent.write_to_file))
|
|
172
|
-
basic_info.add_row("Wait for input", str(agent.wait_for_input))
|
|
171
|
+
basic_info.add_row("Write to File", str(agent.config.write_to_file))
|
|
172
|
+
basic_info.add_row("Wait for input", str(agent.config.wait_for_input))
|
|
173
173
|
|
|
174
174
|
console.print(Panel(basic_info, title="Basic Information"))
|
|
175
175
|
|
|
@@ -180,7 +180,7 @@ def _view_agent_details(agent: FlockAgent):
|
|
|
180
180
|
console.print(Panel(evaluator_info, title="Evaluator"))
|
|
181
181
|
|
|
182
182
|
# Router info
|
|
183
|
-
router_info = f"Type: {type(agent.
|
|
183
|
+
router_info = f"Type: {type(agent.router).__name__ if agent.router else 'None'}"
|
|
184
184
|
console.print(Panel(router_info, title="Router"))
|
|
185
185
|
|
|
186
186
|
# Tools
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# src/flock/components/__init__.py
|
|
2
|
+
"""Unified component implementations for Flock agents."""
|
|
3
|
+
|
|
4
|
+
# Evaluation components
|
|
5
|
+
from .evaluation.declarative_evaluation_component import DeclarativeEvaluationComponent
|
|
6
|
+
|
|
7
|
+
# Routing components
|
|
8
|
+
from .routing.conditional_routing_component import ConditionalRoutingComponent
|
|
9
|
+
from .routing.default_routing_component import DefaultRoutingComponent
|
|
10
|
+
from .routing.llm_routing_component import LLMRoutingComponent
|
|
11
|
+
|
|
12
|
+
# Utility components
|
|
13
|
+
from .utility.memory_utility_component import MemoryUtilityComponent
|
|
14
|
+
from .utility.metrics_utility_component import MetricsUtilityComponent
|
|
15
|
+
from .utility.output_utility_component import OutputUtilityComponent
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Evaluation
|
|
19
|
+
"DeclarativeEvaluationComponent",
|
|
20
|
+
# Routing
|
|
21
|
+
"ConditionalRoutingComponent",
|
|
22
|
+
"DefaultRoutingComponent",
|
|
23
|
+
"LLMRoutingComponent",
|
|
24
|
+
# Utility
|
|
25
|
+
"MemoryUtilityComponent",
|
|
26
|
+
"MetricsUtilityComponent",
|
|
27
|
+
"OutputUtilityComponent",
|
|
28
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# src/flock/components/evaluation/__init__.py
|
|
2
|
+
"""Evaluation components for the unified component system."""
|
|
3
|
+
|
|
4
|
+
from .declarative_evaluation_component import DeclarativeEvaluationComponent, DeclarativeEvaluationConfig
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"DeclarativeEvaluationComponent",
|
|
8
|
+
"DeclarativeEvaluationConfig",
|
|
9
|
+
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# src/flock/components/evaluation/declarative_evaluation_component.py
|
|
2
|
+
"""DeclarativeEvaluationComponent - DSPy-based evaluation using the unified component system."""
|
|
3
|
+
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from temporalio import workflow
|
|
8
|
+
|
|
9
|
+
with workflow.unsafe.imports_passed_through():
|
|
10
|
+
import dspy
|
|
11
|
+
|
|
12
|
+
from pydantic import Field, PrivateAttr
|
|
13
|
+
|
|
14
|
+
from flock.core.component.agent_component_base import AgentComponentConfig
|
|
15
|
+
from flock.core.component.evaluation_component_base import EvaluationComponentBase
|
|
16
|
+
from flock.core.context.context import FlockContext
|
|
17
|
+
from flock.core.registry import flock_component
|
|
18
|
+
from flock.core.logging.logging import get_logger
|
|
19
|
+
from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
|
|
20
|
+
from flock.core.mixin.prompt_parser import PromptParserMixin
|
|
21
|
+
|
|
22
|
+
logger = get_logger("components.evaluation.declarative")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeclarativeEvaluationConfig(AgentComponentConfig):
|
|
26
|
+
"""Configuration for the DeclarativeEvaluationComponent."""
|
|
27
|
+
|
|
28
|
+
override_evaluator_type: str | None = None
|
|
29
|
+
model: str | None = "openai/gpt-4o"
|
|
30
|
+
use_cache: bool = True
|
|
31
|
+
temperature: float = 0.0
|
|
32
|
+
max_tokens: int = 4096
|
|
33
|
+
max_retries: int = 3
|
|
34
|
+
max_tool_calls: int = 10
|
|
35
|
+
stream: bool = Field(
|
|
36
|
+
default=False,
|
|
37
|
+
description="Enable streaming output from the underlying DSPy program.",
|
|
38
|
+
)
|
|
39
|
+
include_thought_process: bool = Field(
|
|
40
|
+
default=False,
|
|
41
|
+
description="Include the thought process in the output.",
|
|
42
|
+
)
|
|
43
|
+
kwargs: dict[str, Any] = Field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@flock_component(config_class=DeclarativeEvaluationConfig)
|
|
47
|
+
class DeclarativeEvaluationComponent(
|
|
48
|
+
EvaluationComponentBase, DSPyIntegrationMixin, PromptParserMixin
|
|
49
|
+
):
|
|
50
|
+
"""Evaluation component that uses DSPy for generation.
|
|
51
|
+
|
|
52
|
+
This component provides the core intelligence for agents using DSPy's
|
|
53
|
+
declarative programming model. It handles LLM interactions, tool usage,
|
|
54
|
+
and prompt management through DSPy's framework.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
config: DeclarativeEvaluationConfig = Field(
|
|
58
|
+
default_factory=DeclarativeEvaluationConfig,
|
|
59
|
+
description="Evaluation configuration",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
_cost: float = PrivateAttr(default=0.0)
|
|
63
|
+
_lm_history: list = PrivateAttr(default_factory=list)
|
|
64
|
+
|
|
65
|
+
def __init__(self, **data):
|
|
66
|
+
super().__init__(**data)
|
|
67
|
+
|
|
68
|
+
async def evaluate_core(
|
|
69
|
+
self,
|
|
70
|
+
agent: Any,
|
|
71
|
+
inputs: dict[str, Any],
|
|
72
|
+
context: FlockContext | None = None,
|
|
73
|
+
tools: list[Any] | None = None,
|
|
74
|
+
mcp_tools: list[Any] | None = None,
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
"""Core evaluation logic using DSPy - migrated from DeclarativeEvaluator."""
|
|
77
|
+
logger.debug(f"Starting declarative evaluation for component '{self.name}'")
|
|
78
|
+
|
|
79
|
+
# Setup DSPy context with LM (directly from original implementation)
|
|
80
|
+
with dspy.context(
|
|
81
|
+
lm=dspy.LM(
|
|
82
|
+
model=self.config.model or agent.model,
|
|
83
|
+
cache=self.config.use_cache,
|
|
84
|
+
temperature=self.config.temperature,
|
|
85
|
+
max_tokens=self.config.max_tokens,
|
|
86
|
+
num_retries=self.config.max_retries,
|
|
87
|
+
)
|
|
88
|
+
):
|
|
89
|
+
try:
|
|
90
|
+
from rich.console import Console
|
|
91
|
+
console = Console()
|
|
92
|
+
|
|
93
|
+
# Create DSPy signature from agent definition
|
|
94
|
+
_dspy_signature = self.create_dspy_signature_class(
|
|
95
|
+
agent.name,
|
|
96
|
+
agent.description,
|
|
97
|
+
f"{agent.input} -> {agent.output}",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Get output field names for streaming
|
|
101
|
+
output_field_names = list(_dspy_signature.output_fields.keys())
|
|
102
|
+
if not output_field_names:
|
|
103
|
+
logger.warning(
|
|
104
|
+
f"DSPy signature for agent '{agent.name}' has no defined output fields. Streaming might not produce text."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Select appropriate DSPy task
|
|
108
|
+
agent_task = self._select_task(
|
|
109
|
+
_dspy_signature,
|
|
110
|
+
override_evaluator_type=self.config.override_evaluator_type,
|
|
111
|
+
tools=tools or [],
|
|
112
|
+
max_tool_calls=self.config.max_tool_calls,
|
|
113
|
+
mcp_tools=mcp_tools or [],
|
|
114
|
+
kwargs=self.config.kwargs,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
except Exception as setup_error:
|
|
118
|
+
logger.error(
|
|
119
|
+
f"Error setting up DSPy task for agent '{agent.name}': {setup_error}",
|
|
120
|
+
exc_info=True,
|
|
121
|
+
)
|
|
122
|
+
raise RuntimeError(
|
|
123
|
+
f"DSPy task setup failed: {setup_error}"
|
|
124
|
+
) from setup_error
|
|
125
|
+
|
|
126
|
+
# Execute with streaming or non-streaming
|
|
127
|
+
if self.config.stream:
|
|
128
|
+
return await self._execute_streaming(agent_task, inputs, agent, console)
|
|
129
|
+
else:
|
|
130
|
+
return await self._execute_standard(agent_task, inputs, agent)
|
|
131
|
+
|
|
132
|
+
async def _execute_streaming(self, agent_task, inputs: dict[str, Any], agent: Any, console) -> dict[str, Any]:
|
|
133
|
+
"""Execute DSPy program in streaming mode (from original implementation)."""
|
|
134
|
+
logger.info(f"Evaluating agent '{agent.name}' with async streaming.")
|
|
135
|
+
|
|
136
|
+
if not callable(agent_task):
|
|
137
|
+
logger.error("agent_task is not callable, cannot stream.")
|
|
138
|
+
raise TypeError("DSPy task could not be created or is not callable.")
|
|
139
|
+
|
|
140
|
+
streaming_task = dspy.streamify(agent_task, is_async_program=True)
|
|
141
|
+
stream_generator: Generator = streaming_task(**inputs)
|
|
142
|
+
delta_content = ""
|
|
143
|
+
|
|
144
|
+
console.print("\n")
|
|
145
|
+
async for chunk in stream_generator:
|
|
146
|
+
if (
|
|
147
|
+
hasattr(chunk, "choices")
|
|
148
|
+
and chunk.choices
|
|
149
|
+
and hasattr(chunk.choices[0], "delta")
|
|
150
|
+
and chunk.choices[0].delta
|
|
151
|
+
and hasattr(chunk.choices[0].delta, "content")
|
|
152
|
+
):
|
|
153
|
+
delta_content = chunk.choices[0].delta.content
|
|
154
|
+
|
|
155
|
+
if delta_content:
|
|
156
|
+
console.print(delta_content, end="")
|
|
157
|
+
|
|
158
|
+
result_dict, cost, lm_history = self._process_result(chunk, inputs)
|
|
159
|
+
self._cost = cost
|
|
160
|
+
self._lm_history = lm_history
|
|
161
|
+
|
|
162
|
+
console.print("\n")
|
|
163
|
+
return self.filter_thought_process(
|
|
164
|
+
result_dict, self.config.include_thought_process
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def _execute_standard(self, agent_task, inputs: dict[str, Any], agent: Any) -> dict[str, Any]:
|
|
168
|
+
"""Execute DSPy program in standard mode (from original implementation)."""
|
|
169
|
+
logger.info(f"Evaluating agent '{agent.name}' without streaming.")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
# Ensure the call is awaited if the underlying task is async
|
|
173
|
+
result_obj = await agent_task.acall(**inputs)
|
|
174
|
+
result_dict, cost, lm_history = self._process_result(result_obj, inputs)
|
|
175
|
+
self._cost = cost
|
|
176
|
+
self._lm_history = lm_history
|
|
177
|
+
return self.filter_thought_process(
|
|
178
|
+
result_dict, self.config.include_thought_process
|
|
179
|
+
)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(
|
|
182
|
+
f"Error during non-streaming evaluation for agent '{agent.name}': {e}",
|
|
183
|
+
exc_info=True,
|
|
184
|
+
)
|
|
185
|
+
raise RuntimeError(f"Evaluation failed: {e}") from e
|
|
186
|
+
|
|
187
|
+
def filter_thought_process(
|
|
188
|
+
self, result_dict: dict[str, Any], include_thought_process: bool
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
"""Filter out thought process from the result dictionary (from original implementation)."""
|
|
191
|
+
if include_thought_process:
|
|
192
|
+
return result_dict
|
|
193
|
+
else:
|
|
194
|
+
return {
|
|
195
|
+
k: v
|
|
196
|
+
for k, v in result_dict.items()
|
|
197
|
+
if not (k.startswith("reasoning") or k.startswith("trajectory"))
|
|
198
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# src/flock/components/routing/__init__.py
|
|
2
|
+
"""Routing components for the Flock framework."""
|
|
3
|
+
|
|
4
|
+
from .conditional_routing_component import ConditionalRoutingComponent, ConditionalRoutingConfig
|
|
5
|
+
from .default_routing_component import DefaultRoutingComponent, DefaultRoutingConfig
|
|
6
|
+
from .llm_routing_component import LLMRoutingComponent, LLMRoutingConfig
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ConditionalRoutingComponent",
|
|
10
|
+
"ConditionalRoutingConfig",
|
|
11
|
+
"DefaultRoutingComponent",
|
|
12
|
+
"DefaultRoutingConfig",
|
|
13
|
+
"LLMRoutingComponent",
|
|
14
|
+
"LLMRoutingConfig",
|
|
15
|
+
]
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
# src/flock/
|
|
1
|
+
# src/flock/components/routing/conditional_routing_component.py
|
|
2
|
+
"""Conditional routing component implementation for the unified component architecture."""
|
|
2
3
|
|
|
3
4
|
import re
|
|
4
5
|
from collections.abc import Callable
|
|
5
|
-
from typing import Any, Literal
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
6
7
|
|
|
7
8
|
from pydantic import Field, model_validator
|
|
8
9
|
|
|
10
|
+
from flock.core.component.agent_component_base import AgentComponentConfig
|
|
11
|
+
from flock.core.component.routing_component_base import RoutingComponentBase
|
|
9
12
|
from flock.core.context.context import FlockContext
|
|
10
|
-
from flock.core.
|
|
11
|
-
|
|
12
|
-
from flock.core.flock_router import (
|
|
13
|
-
FlockRouter,
|
|
14
|
-
FlockRouterConfig,
|
|
15
|
-
HandOffRequest,
|
|
16
|
-
)
|
|
13
|
+
from flock.core.registry import flock_component, get_registry
|
|
14
|
+
# HandOffRequest removed - using agent.next_agent directly
|
|
17
15
|
from flock.core.logging.logging import get_logger
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from flock.core.flock_agent import FlockAgent
|
|
20
19
|
|
|
20
|
+
logger = get_logger("components.routing.conditional")
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
|
|
23
|
+
class ConditionalRoutingConfig(AgentComponentConfig):
|
|
24
|
+
"""Configuration for the ConditionalRoutingComponent."""
|
|
24
25
|
|
|
25
26
|
condition_context_key: str = Field(
|
|
26
27
|
default="flock.condition",
|
|
@@ -122,7 +123,7 @@ class ConditionalRouterConfig(FlockRouterConfig):
|
|
|
122
123
|
|
|
123
124
|
# --- Validator to ensure only one condition type is set ---
|
|
124
125
|
@model_validator(mode="after")
|
|
125
|
-
def check_exclusive_condition(self) -> "
|
|
126
|
+
def check_exclusive_condition(self) -> "ConditionalRoutingConfig":
|
|
126
127
|
conditions_set = [
|
|
127
128
|
self.condition_callable is not None,
|
|
128
129
|
self.expected_string is not None
|
|
@@ -137,27 +138,37 @@ class ConditionalRouterConfig(FlockRouterConfig):
|
|
|
137
138
|
]
|
|
138
139
|
if sum(conditions_set) > 1:
|
|
139
140
|
raise ValueError(
|
|
140
|
-
"Only one type of condition (callable, string/length, number, list size, type, boolean, exists) can be configured per
|
|
141
|
+
"Only one type of condition (callable, string/length, number, list size, type, boolean, exists) can be configured per ConditionalRoutingComponent."
|
|
141
142
|
)
|
|
142
143
|
if sum(conditions_set) == 0:
|
|
143
144
|
raise ValueError(
|
|
144
|
-
"At least one condition type must be configured for
|
|
145
|
+
"At least one condition type must be configured for ConditionalRoutingComponent."
|
|
145
146
|
)
|
|
146
147
|
return self
|
|
147
148
|
|
|
148
149
|
|
|
149
|
-
@flock_component(config_class=
|
|
150
|
-
class
|
|
150
|
+
@flock_component(config_class=ConditionalRoutingConfig)
|
|
151
|
+
class ConditionalRoutingComponent(RoutingComponentBase):
|
|
151
152
|
"""Routes workflow based on evaluating a condition against a value in the FlockContext.
|
|
153
|
+
|
|
152
154
|
Supports various built-in checks (string, number, list, type, bool, existence)
|
|
153
155
|
or a custom callable. Can optionally retry the current agent on failure.
|
|
154
156
|
"""
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
default_factory=ConditionalRouterConfig
|
|
158
|
+
config: ConditionalRoutingConfig = Field(
|
|
159
|
+
default_factory=ConditionalRoutingConfig
|
|
159
160
|
)
|
|
160
161
|
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
name: str = "conditional_router",
|
|
165
|
+
config: ConditionalRoutingConfig | None = None,
|
|
166
|
+
**data,
|
|
167
|
+
):
|
|
168
|
+
if config is None:
|
|
169
|
+
config = ConditionalRoutingConfig()
|
|
170
|
+
super().__init__(name=name, config=config, **data)
|
|
171
|
+
|
|
161
172
|
def _evaluate_condition(self, value: Any) -> tuple[bool, str | None]:
|
|
162
173
|
"""Evaluates the condition based on the router's configuration.
|
|
163
174
|
|
|
@@ -168,7 +179,7 @@ class ConditionalRouter(FlockRouter):
|
|
|
168
179
|
"""
|
|
169
180
|
cfg = self.config
|
|
170
181
|
condition_passed = False
|
|
171
|
-
feedback =
|
|
182
|
+
feedback = None # Default feedback
|
|
172
183
|
condition_type = "unknown"
|
|
173
184
|
|
|
174
185
|
try:
|
|
@@ -373,12 +384,17 @@ class ConditionalRouter(FlockRouter):
|
|
|
373
384
|
feedback,
|
|
374
385
|
) # Treat evaluation errors as condition failure
|
|
375
386
|
|
|
376
|
-
async def
|
|
387
|
+
async def determine_next_step(
|
|
377
388
|
self,
|
|
378
|
-
|
|
389
|
+
agent: "FlockAgent",
|
|
379
390
|
result: dict[str, Any],
|
|
380
|
-
context: FlockContext,
|
|
381
|
-
) ->
|
|
391
|
+
context: FlockContext | None = None,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Determine next step based on evaluating a condition against context value."""
|
|
394
|
+
if not context:
|
|
395
|
+
logger.warning("No context provided for conditional routing")
|
|
396
|
+
return
|
|
397
|
+
|
|
382
398
|
cfg = self.config
|
|
383
399
|
condition_value = context.get_variable(cfg.condition_context_key, None)
|
|
384
400
|
feedback_value = context.get_variable(cfg.feedback_context_key, None)
|
|
@@ -395,17 +411,17 @@ class ConditionalRouter(FlockRouter):
|
|
|
395
411
|
if condition_passed:
|
|
396
412
|
# --- Success Path ---
|
|
397
413
|
logger.info(
|
|
398
|
-
f"Condition PASSED for agent '{
|
|
414
|
+
f"Condition PASSED for agent '{agent.name}'. Routing to success path."
|
|
399
415
|
)
|
|
400
416
|
# Reset retry count if applicable
|
|
401
417
|
if cfg.retry_on_failure:
|
|
402
418
|
retry_key = (
|
|
403
|
-
f"{cfg.retry_count_context_key_prefix}{
|
|
419
|
+
f"{cfg.retry_count_context_key_prefix}{agent.name}"
|
|
404
420
|
)
|
|
405
421
|
if retry_key in context.state:
|
|
406
422
|
del context.state[retry_key]
|
|
407
423
|
logger.debug(
|
|
408
|
-
f"Reset retry count for agent '{
|
|
424
|
+
f"Reset retry count for agent '{agent.name}'."
|
|
409
425
|
)
|
|
410
426
|
|
|
411
427
|
# Clear feedback from context on success
|
|
@@ -418,20 +434,21 @@ class ConditionalRouter(FlockRouter):
|
|
|
418
434
|
f"Cleared feedback key '{cfg.feedback_context_key}' on success."
|
|
419
435
|
)
|
|
420
436
|
|
|
421
|
-
next_agent = cfg.success_agent or
|
|
437
|
+
next_agent = cfg.success_agent or None # Stop chain if None
|
|
422
438
|
logger.debug(f"Success route target: '{next_agent}'")
|
|
423
|
-
|
|
439
|
+
|
|
440
|
+
agent.next_agent = next_agent # Set directly on agent
|
|
424
441
|
|
|
425
442
|
else:
|
|
426
443
|
# --- Failure Path ---
|
|
427
444
|
logger.warning(
|
|
428
|
-
f"Condition FAILED for agent '{
|
|
445
|
+
f"Condition FAILED for agent '{agent.name}'. Reason: {feedback_msg}"
|
|
429
446
|
)
|
|
430
447
|
|
|
431
448
|
if cfg.retry_on_failure:
|
|
432
449
|
# --- Retry Logic ---
|
|
433
450
|
retry_key = (
|
|
434
|
-
f"{cfg.retry_count_context_key_prefix}{
|
|
451
|
+
f"{cfg.retry_count_context_key_prefix}{agent.name}"
|
|
435
452
|
)
|
|
436
453
|
retry_count = context.get_variable(retry_key, 0)
|
|
437
454
|
|
|
@@ -439,44 +456,38 @@ class ConditionalRouter(FlockRouter):
|
|
|
439
456
|
next_retry_count = retry_count + 1
|
|
440
457
|
context.set_variable(retry_key, next_retry_count)
|
|
441
458
|
logger.info(
|
|
442
|
-
f"Routing back to agent '{
|
|
459
|
+
f"Routing back to agent '{agent.name}' for retry #{next_retry_count}/{cfg.max_retries}."
|
|
443
460
|
)
|
|
444
461
|
|
|
445
462
|
# Add specific feedback to context if retry is enabled
|
|
446
463
|
if cfg.feedback_context_key:
|
|
447
464
|
context.set_variable(
|
|
448
465
|
cfg.feedback_context_key,
|
|
449
|
-
feedback_msg or
|
|
466
|
+
feedback_msg or "Condition failed",
|
|
450
467
|
)
|
|
451
468
|
logger.debug(
|
|
452
|
-
f"Set feedback key '{cfg.feedback_context_key}': {feedback_msg or
|
|
469
|
+
f"Set feedback key '{cfg.feedback_context_key}': {feedback_msg or 'Condition failed'}"
|
|
453
470
|
)
|
|
454
471
|
|
|
455
|
-
|
|
456
|
-
next_agent=current_agent.name, # Route back to self
|
|
457
|
-
output_to_input_merge_strategy="add", # Make feedback available
|
|
458
|
-
)
|
|
472
|
+
agent.next_agent = agent.name # Route back to self
|
|
459
473
|
else:
|
|
460
474
|
# --- Max Retries Exceeded ---
|
|
461
475
|
logger.error(
|
|
462
|
-
f"Max retries ({cfg.max_retries}) exceeded for agent '{
|
|
476
|
+
f"Max retries ({cfg.max_retries}) exceeded for agent '{agent.name}'."
|
|
463
477
|
)
|
|
464
478
|
if retry_key in context.state:
|
|
465
479
|
del context.state[retry_key] # Reset count
|
|
466
|
-
|
|
467
|
-
# if cfg.feedback_context_key in context.state: del context.state[cfg.feedback_context_key]
|
|
468
|
-
next_agent = cfg.failure_agent or ""
|
|
480
|
+
next_agent = cfg.failure_agent or None
|
|
469
481
|
logger.debug(
|
|
470
482
|
f"Failure route target (after retries): '{next_agent}'"
|
|
471
483
|
)
|
|
472
|
-
|
|
484
|
+
|
|
485
|
+
agent.next_agent = next_agent
|
|
473
486
|
else:
|
|
474
487
|
# --- No Retry Logic ---
|
|
475
488
|
next_agent = (
|
|
476
|
-
cfg.failure_agent or
|
|
489
|
+
cfg.failure_agent or None
|
|
477
490
|
) # Use failure agent or stop
|
|
478
491
|
logger.debug(f"Failure route target (no retry): '{next_agent}'")
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
# context.set_variable(cfg.feedback_context_key, feedback_msg or cfg.feedback_on_failure)
|
|
482
|
-
return HandOffRequest(next_agent=next_agent)
|
|
492
|
+
|
|
493
|
+
agent.next_agent = next_agent
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# src/flock/components/routing/default_routing_component.py
|
|
2
|
+
"""Default routing component implementation for the unified component architecture."""
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from flock.core.component.agent_component_base import AgentComponentConfig
|
|
10
|
+
from flock.core.component.routing_component_base import RoutingComponentBase
|
|
11
|
+
from flock.core.context.context import FlockContext
|
|
12
|
+
from flock.core.registry import flock_component
|
|
13
|
+
from flock.core.logging.logging import get_logger
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from flock.core.flock_agent import FlockAgent
|
|
17
|
+
|
|
18
|
+
logger = get_logger("components.routing.default")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DefaultRoutingConfig(AgentComponentConfig):
|
|
22
|
+
"""Configuration for the default routing component."""
|
|
23
|
+
|
|
24
|
+
next_agent: str | Callable[..., str] = Field(
|
|
25
|
+
default="", description="Next agent to hand off to"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@flock_component(config_class=DefaultRoutingConfig)
|
|
30
|
+
class DefaultRoutingComponent(RoutingComponentBase):
|
|
31
|
+
"""Default routing component implementation.
|
|
32
|
+
|
|
33
|
+
This router simply uses the configured hand_off property to determine the next agent.
|
|
34
|
+
It does not perform any dynamic routing based on agent results.
|
|
35
|
+
|
|
36
|
+
Configuration can be:
|
|
37
|
+
- A string: Simple agent name to route to
|
|
38
|
+
- A callable: Function that takes (context, result) and returns agent name
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
config: DefaultRoutingConfig = Field(
|
|
42
|
+
default_factory=DefaultRoutingConfig,
|
|
43
|
+
description="Default routing configuration",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
name: str = "default_router",
|
|
49
|
+
config: DefaultRoutingConfig | None = None,
|
|
50
|
+
**data,
|
|
51
|
+
):
|
|
52
|
+
"""Initialize the DefaultRoutingComponent.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
name: The name of the routing component
|
|
56
|
+
config: The routing configuration
|
|
57
|
+
"""
|
|
58
|
+
if config is None:
|
|
59
|
+
config = DefaultRoutingConfig()
|
|
60
|
+
super().__init__(name=name, config=config, **data)
|
|
61
|
+
|
|
62
|
+
async def determine_next_step(
|
|
63
|
+
self,
|
|
64
|
+
agent: "FlockAgent",
|
|
65
|
+
result: dict[str, Any],
|
|
66
|
+
context: FlockContext | None = None,
|
|
67
|
+
) -> str | None:
|
|
68
|
+
"""Determine the next agent to hand off to based on configuration.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
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
|
+
String agent name to route to, or None to end workflow
|
|
77
|
+
"""
|
|
78
|
+
handoff = self.config.next_agent
|
|
79
|
+
|
|
80
|
+
# If empty string, end the workflow
|
|
81
|
+
if handoff == "":
|
|
82
|
+
logger.debug("No handoff configured, ending workflow")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# If callable, invoke it with context and result
|
|
86
|
+
if callable(handoff):
|
|
87
|
+
logger.debug("Invoking handoff callable")
|
|
88
|
+
try:
|
|
89
|
+
handoff = handoff(context, result)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error("Error invoking handoff callable: %s", e)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
# Validate it's a string
|
|
95
|
+
if not isinstance(handoff, str):
|
|
96
|
+
logger.error(
|
|
97
|
+
"Invalid handoff type: %s. Expected str or callable returning str",
|
|
98
|
+
type(handoff),
|
|
99
|
+
)
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
logger.debug("Routing to agent: %s", handoff)
|
|
103
|
+
return handoff
|