flock-core 0.4.528__py3-none-any.whl → 0.5.0b0__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.

Files changed (130) hide show
  1. flock/cli/execute_flock.py +1 -1
  2. flock/cli/manage_agents.py +6 -6
  3. flock/components/__init__.py +30 -0
  4. flock/components/evaluation/__init__.py +9 -0
  5. flock/components/evaluation/declarative_evaluation_component.py +222 -0
  6. flock/components/routing/__init__.py +15 -0
  7. flock/{routers/conditional/conditional_router.py → components/routing/conditional_routing_component.py} +61 -53
  8. flock/components/routing/default_routing_component.py +103 -0
  9. flock/components/routing/llm_routing_component.py +206 -0
  10. flock/components/utility/__init__.py +15 -0
  11. flock/{modules/enterprise_memory/enterprise_memory_module.py → components/utility/memory_utility_component.py} +195 -173
  12. flock/{modules/performance/metrics_module.py → components/utility/metrics_utility_component.py} +110 -95
  13. flock/{modules/output/output_module.py → components/utility/output_utility_component.py} +47 -45
  14. flock/core/__init__.py +26 -18
  15. flock/core/agent/__init__.py +16 -0
  16. flock/core/agent/flock_agent_components.py +104 -0
  17. flock/core/agent/flock_agent_execution.py +101 -0
  18. flock/core/agent/flock_agent_integration.py +206 -0
  19. flock/core/agent/flock_agent_lifecycle.py +177 -0
  20. flock/core/agent/flock_agent_serialization.py +381 -0
  21. flock/core/api/endpoints.py +2 -2
  22. flock/core/api/service.py +2 -2
  23. flock/core/component/__init__.py +15 -0
  24. flock/core/{flock_module.py → component/agent_component_base.py} +136 -34
  25. flock/core/component/evaluation_component.py +56 -0
  26. flock/core/component/routing_component.py +74 -0
  27. flock/core/component/utility_component.py +69 -0
  28. flock/core/config/flock_agent_config.py +49 -2
  29. flock/core/evaluation/utils.py +3 -2
  30. flock/core/execution/batch_executor.py +1 -1
  31. flock/core/execution/evaluation_executor.py +2 -2
  32. flock/core/execution/opik_executor.py +1 -1
  33. flock/core/flock.py +147 -493
  34. flock/core/flock_agent.py +195 -1032
  35. flock/core/flock_factory.py +114 -90
  36. flock/core/flock_scheduler.py +1 -1
  37. flock/core/flock_server_manager.py +8 -8
  38. flock/core/logging/logging.py +1 -0
  39. flock/core/mcp/flock_mcp_server.py +53 -48
  40. flock/core/mcp/{flock_mcp_tool_base.py → flock_mcp_tool.py} +2 -2
  41. flock/core/mcp/mcp_client.py +9 -9
  42. flock/core/mcp/mcp_client_manager.py +9 -9
  43. flock/core/mcp/mcp_config.py +24 -24
  44. flock/core/mixin/dspy_integration.py +5 -5
  45. flock/core/orchestration/__init__.py +18 -0
  46. flock/core/orchestration/flock_batch_processor.py +94 -0
  47. flock/core/orchestration/flock_evaluator.py +113 -0
  48. flock/core/orchestration/flock_execution.py +288 -0
  49. flock/core/orchestration/flock_initialization.py +125 -0
  50. flock/core/orchestration/flock_server_manager.py +67 -0
  51. flock/core/orchestration/flock_web_server.py +117 -0
  52. flock/core/registry/__init__.py +45 -0
  53. flock/core/registry/agent_registry.py +69 -0
  54. flock/core/registry/callable_registry.py +139 -0
  55. flock/core/registry/component_discovery.py +142 -0
  56. flock/core/registry/component_registry.py +64 -0
  57. flock/core/registry/config_mapping.py +64 -0
  58. flock/core/registry/decorators.py +137 -0
  59. flock/core/registry/registry_hub.py +205 -0
  60. flock/core/registry/server_registry.py +57 -0
  61. flock/core/registry/type_registry.py +86 -0
  62. flock/core/serialization/flock_serializer.py +36 -32
  63. flock/core/serialization/serialization_utils.py +28 -25
  64. flock/core/util/hydrator.py +1 -1
  65. flock/core/util/input_resolver.py +29 -2
  66. flock/mcp/servers/sse/flock_sse_server.py +10 -10
  67. flock/mcp/servers/stdio/flock_stdio_server.py +10 -10
  68. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +10 -10
  69. flock/mcp/servers/websockets/flock_websocket_server.py +10 -10
  70. flock/platform/docker_tools.py +3 -3
  71. flock/webapp/app/chat.py +1 -1
  72. flock/webapp/app/main.py +9 -5
  73. flock/webapp/app/services/flock_service.py +1 -1
  74. flock/webapp/app/services/sharing_store.py +1 -0
  75. flock/workflow/activities.py +67 -92
  76. flock/workflow/agent_execution_activity.py +6 -6
  77. flock/workflow/flock_workflow.py +1 -1
  78. flock_core-0.5.0b0.dist-info/METADATA +272 -0
  79. {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/RECORD +82 -95
  80. flock/core/flock_evaluator.py +0 -60
  81. flock/core/flock_registry.py +0 -702
  82. flock/core/flock_router.py +0 -83
  83. flock/evaluators/__init__.py +0 -1
  84. flock/evaluators/declarative/__init__.py +0 -1
  85. flock/evaluators/declarative/declarative_evaluator.py +0 -217
  86. flock/evaluators/memory/memory_evaluator.py +0 -90
  87. flock/evaluators/test/test_case_evaluator.py +0 -38
  88. flock/evaluators/zep/zep_evaluator.py +0 -59
  89. flock/modules/__init__.py +0 -1
  90. flock/modules/assertion/__init__.py +0 -1
  91. flock/modules/assertion/assertion_module.py +0 -286
  92. flock/modules/callback/__init__.py +0 -1
  93. flock/modules/callback/callback_module.py +0 -91
  94. flock/modules/enterprise_memory/README.md +0 -99
  95. flock/modules/mem0/__init__.py +0 -1
  96. flock/modules/mem0/mem0_module.py +0 -126
  97. flock/modules/mem0_async/__init__.py +0 -1
  98. flock/modules/mem0_async/async_mem0_module.py +0 -126
  99. flock/modules/memory/__init__.py +0 -1
  100. flock/modules/memory/memory_module.py +0 -429
  101. flock/modules/memory/memory_parser.py +0 -125
  102. flock/modules/memory/memory_storage.py +0 -736
  103. flock/modules/output/__init__.py +0 -1
  104. flock/modules/performance/__init__.py +0 -1
  105. flock/modules/zep/__init__.py +0 -1
  106. flock/modules/zep/zep_module.py +0 -192
  107. flock/routers/__init__.py +0 -1
  108. flock/routers/agent/__init__.py +0 -1
  109. flock/routers/agent/agent_router.py +0 -236
  110. flock/routers/agent/handoff_agent.py +0 -58
  111. flock/routers/default/__init__.py +0 -1
  112. flock/routers/default/default_router.py +0 -80
  113. flock/routers/feedback/feedback_router.py +0 -114
  114. flock/routers/list_generator/list_generator_router.py +0 -166
  115. flock/routers/llm/__init__.py +0 -1
  116. flock/routers/llm/llm_router.py +0 -365
  117. flock/tools/__init__.py +0 -0
  118. flock/tools/azure_tools.py +0 -781
  119. flock/tools/code_tools.py +0 -167
  120. flock/tools/file_tools.py +0 -149
  121. flock/tools/github_tools.py +0 -157
  122. flock/tools/markdown_tools.py +0 -205
  123. flock/tools/system_tools.py +0 -9
  124. flock/tools/text_tools.py +0 -810
  125. flock/tools/web_tools.py +0 -90
  126. flock/tools/zendesk_tools.py +0 -147
  127. flock_core-0.4.528.dist-info/METADATA +0 -675
  128. {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/WHEEL +0 -0
  129. {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/entry_points.txt +0 -0
  130. {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/licenses/LICENSE +0 -0
@@ -142,7 +142,7 @@ def execute_flock(flock: Flock):
142
142
 
143
143
  # Run the Flock
144
144
  result = flock.run(
145
- start_agent=start_agent_name,
145
+ agent=start_agent_name,
146
146
  input=input_data,
147
147
  )
148
148
 
@@ -105,7 +105,7 @@ def _list_agents(flock: Flock):
105
105
  model = agent.model or flock.model or "Default"
106
106
 
107
107
  # Format description (truncate if too long)
108
- description = agent.resolved_description
108
+ description = agent.description
109
109
  if description and len(description) > 30:
110
110
  description = description[:27] + "..."
111
111
  elif not description:
@@ -165,11 +165,11 @@ def _view_agent_details(agent: FlockAgent):
165
165
 
166
166
  basic_info.add_row("Name", agent.name)
167
167
  basic_info.add_row("Model", str(agent.model or "Default"))
168
- basic_info.add_row("Description", agent.resolved_description if agent.resolved_description else "N/A")
168
+ basic_info.add_row("Description", agent.description if agent.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.handoff_router).__name__ if agent.handoff_router else 'None'}"
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
@@ -339,7 +339,7 @@ def _edit_agent(flock: Flock):
339
339
  console.print(f"\n[bold underline]Details for Agent: {agent.name}[/]")
340
340
  basic_info = Table(show_header=False, box=Box.ROUNDED, padding=(0, 2))
341
341
  basic_info.add_row("Name", agent.name)
342
- description = agent.resolved_description
342
+ description = agent.description
343
343
  basic_info.add_row("Description", description if description else "N/A")
344
344
  basic_info.add_row("Model", agent.model or "Flock Default")
345
345
  basic_info.add_row("Input Signature", str(agent.input))
@@ -0,0 +1,30 @@
1
+ # src/flock/components/__init__.py
2
+ """Unified component implementations for Flock agents."""
3
+
4
+ # Evaluation components
5
+ from .evaluation.declarative_evaluation_component import (
6
+ DeclarativeEvaluationComponent,
7
+ )
8
+
9
+ # Routing components
10
+ from .routing.conditional_routing_component import ConditionalRoutingComponent
11
+ from .routing.default_routing_component import DefaultRoutingComponent
12
+ from .routing.llm_routing_component import LLMRoutingComponent
13
+
14
+ # Utility components
15
+ from .utility.memory_utility_component import MemoryUtilityComponent
16
+ from .utility.metrics_utility_component import MetricsUtilityComponent
17
+ from .utility.output_utility_component import OutputUtilityComponent
18
+
19
+ __all__ = [
20
+ # Routing
21
+ "ConditionalRoutingComponent",
22
+ # Evaluation
23
+ "DeclarativeEvaluationComponent",
24
+ "DefaultRoutingComponent",
25
+ "LLMRoutingComponent",
26
+ # Utility
27
+ "MemoryUtilityComponent",
28
+ "MetricsUtilityComponent",
29
+ "OutputUtilityComponent",
30
+ ]
@@ -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,222 @@
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 import EvaluationComponent
16
+ from flock.core.context.context import FlockContext
17
+ from flock.core.logging.logging import get_logger
18
+ from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
19
+ from flock.core.mixin.prompt_parser import PromptParserMixin
20
+ from flock.core.registry import flock_component
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
+ include_reasoning: bool = Field(
44
+ default=False,
45
+ description="Include the reasoning in the output.",
46
+ )
47
+ kwargs: dict[str, Any] = Field(default_factory=dict)
48
+
49
+
50
+ @flock_component(config_class=DeclarativeEvaluationConfig)
51
+ class DeclarativeEvaluationComponent(
52
+ EvaluationComponent, DSPyIntegrationMixin, PromptParserMixin
53
+ ):
54
+ """Evaluation component that uses DSPy for generation.
55
+
56
+ This component provides the core intelligence for agents using DSPy's
57
+ declarative programming model. It handles LLM interactions, tool usage,
58
+ and prompt management through DSPy's framework.
59
+ """
60
+
61
+ config: DeclarativeEvaluationConfig = Field(
62
+ default_factory=DeclarativeEvaluationConfig,
63
+ description="Evaluation configuration",
64
+ )
65
+
66
+ _cost: float = PrivateAttr(default=0.0)
67
+ _lm_history: list = PrivateAttr(default_factory=list)
68
+
69
+ def __init__(self, **data):
70
+ super().__init__(**data)
71
+
72
+ async def evaluate_core(
73
+ self,
74
+ agent: Any,
75
+ inputs: dict[str, Any],
76
+ context: FlockContext | None = None,
77
+ tools: list[Any] | None = None,
78
+ mcp_tools: list[Any] | None = None,
79
+ ) -> dict[str, Any]:
80
+ """Core evaluation logic using DSPy - migrated from DeclarativeEvaluator."""
81
+ logger.debug(f"Starting declarative evaluation for component '{self.name}'")
82
+
83
+ # Setup DSPy context with LM (directly from original implementation)
84
+ with dspy.context(
85
+ lm=dspy.LM(
86
+ model=self.config.model or agent.model,
87
+ cache=self.config.use_cache,
88
+ temperature=self.config.temperature,
89
+ max_tokens=self.config.max_tokens,
90
+ num_retries=self.config.max_retries,
91
+ )
92
+ ):
93
+ try:
94
+ from rich.console import Console
95
+ console = Console()
96
+
97
+ # Create DSPy signature from agent definition
98
+ _dspy_signature = self.create_dspy_signature_class(
99
+ agent.name,
100
+ agent.description,
101
+ f"{agent.input} -> {agent.output}",
102
+ )
103
+
104
+ # Get output field names for streaming
105
+ output_field_names = list(_dspy_signature.output_fields.keys())
106
+ if not output_field_names:
107
+ logger.warning(
108
+ f"DSPy signature for agent '{agent.name}' has no defined output fields. Streaming might not produce text."
109
+ )
110
+
111
+ # Select appropriate DSPy task
112
+ agent_task = self._select_task(
113
+ _dspy_signature,
114
+ override_evaluator_type=self.config.override_evaluator_type,
115
+ tools=tools or [],
116
+ max_tool_calls=self.config.max_tool_calls,
117
+ mcp_tools=mcp_tools or [],
118
+ kwargs=self.config.kwargs,
119
+ )
120
+
121
+ except Exception as setup_error:
122
+ logger.error(
123
+ f"Error setting up DSPy task for agent '{agent.name}': {setup_error}",
124
+ exc_info=True,
125
+ )
126
+ raise RuntimeError(
127
+ f"DSPy task setup failed: {setup_error}"
128
+ ) from setup_error
129
+
130
+ # Execute with streaming or non-streaming
131
+ if self.config.stream:
132
+ return await self._execute_streaming(agent_task, inputs, agent, console)
133
+ else:
134
+ return await self._execute_standard(agent_task, inputs, agent)
135
+
136
+ async def _execute_streaming(self, agent_task, inputs: dict[str, Any], agent: Any, console) -> dict[str, Any]:
137
+ """Execute DSPy program in streaming mode (from original implementation)."""
138
+ logger.info(f"Evaluating agent '{agent.name}' with async streaming.")
139
+
140
+ if not callable(agent_task):
141
+ logger.error("agent_task is not callable, cannot stream.")
142
+ raise TypeError("DSPy task could not be created or is not callable.")
143
+
144
+ streaming_task = dspy.streamify(agent_task, is_async_program=True)
145
+ stream_generator: Generator = streaming_task(**inputs)
146
+ delta_content = ""
147
+
148
+ console.print("\n")
149
+ async for chunk in stream_generator:
150
+ if (
151
+ hasattr(chunk, "choices")
152
+ and chunk.choices
153
+ and hasattr(chunk.choices[0], "delta")
154
+ and chunk.choices[0].delta
155
+ and hasattr(chunk.choices[0].delta, "content")
156
+ ):
157
+ delta_content = chunk.choices[0].delta.content
158
+
159
+ if delta_content:
160
+ console.print(delta_content, end="")
161
+
162
+ result_dict, cost, lm_history = self._process_result(chunk, inputs)
163
+ self._cost = cost
164
+ self._lm_history = lm_history
165
+
166
+ console.print("\n")
167
+ result_dict = self.filter_reasoning(
168
+ result_dict, self.config.include_reasoning
169
+ )
170
+ return self.filter_thought_process(
171
+ result_dict, self.config.include_thought_process
172
+ )
173
+
174
+ async def _execute_standard(self, agent_task, inputs: dict[str, Any], agent: Any) -> dict[str, Any]:
175
+ """Execute DSPy program in standard mode (from original implementation)."""
176
+ logger.info(f"Evaluating agent '{agent.name}' without streaming.")
177
+
178
+ try:
179
+ # Ensure the call is awaited if the underlying task is async
180
+ result_obj = await agent_task.acall(**inputs)
181
+ result_dict, cost, lm_history = self._process_result(result_obj, inputs)
182
+ self._cost = cost
183
+ self._lm_history = lm_history
184
+ result_dict = self.filter_reasoning(
185
+ result_dict, self.config.include_reasoning
186
+ )
187
+ return self.filter_thought_process(
188
+ result_dict, self.config.include_thought_process
189
+ )
190
+ except Exception as e:
191
+ logger.error(
192
+ f"Error during non-streaming evaluation for agent '{agent.name}': {e}",
193
+ exc_info=True,
194
+ )
195
+ raise RuntimeError(f"Evaluation failed: {e}") from e
196
+
197
+ def filter_thought_process(
198
+ self, result_dict: dict[str, Any], include_thought_process: bool
199
+ ) -> dict[str, Any]:
200
+ """Filter out thought process from the result dictionary (from original implementation)."""
201
+ if include_thought_process:
202
+ return result_dict
203
+ else:
204
+ return {
205
+ k: v
206
+ for k, v in result_dict.items()
207
+ if not (k.startswith("reasoning") or k.startswith("trajectory"))
208
+ }
209
+
210
+ def filter_reasoning(
211
+ self, result_dict: dict[str, Any], include_reasoning: bool
212
+ ) -> dict[str, Any]:
213
+ """Filter out reasoning from the result dictionary."""
214
+ if include_reasoning:
215
+ return result_dict
216
+ else:
217
+ return {
218
+ k: v
219
+ for k, v in result_dict.items()
220
+ if not (k.startswith("reasoning"))
221
+ }
222
+
@@ -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,28 @@
1
- # src/flock/routers/conditional/conditional_router.py
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 import RoutingComponent
9
12
  from flock.core.context.context import FlockContext
10
- from flock.core.flock_agent import FlockAgent
11
- from flock.core.flock_registry import flock_component, get_registry
12
- from flock.core.flock_router import (
13
- FlockRouter,
14
- FlockRouterConfig,
15
- HandOffRequest,
16
- )
13
+
14
+ # HandOffRequest removed - using agent.next_agent directly
17
15
  from flock.core.logging.logging import get_logger
16
+ from flock.core.registry import flock_component, get_registry
17
+
18
+ if TYPE_CHECKING:
19
+ from flock.core.flock_agent import FlockAgent
18
20
 
19
- logger = get_logger("router.conditional")
21
+ logger = get_logger("components.routing.conditional")
20
22
 
21
23
 
22
- class ConditionalRouterConfig(FlockRouterConfig):
23
- """Configuration for the ConditionalRouter."""
24
+ class ConditionalRoutingConfig(AgentComponentConfig):
25
+ """Configuration for the ConditionalRoutingComponent."""
24
26
 
25
27
  condition_context_key: str = Field(
26
28
  default="flock.condition",
@@ -115,10 +117,6 @@ class ConditionalRouterConfig(FlockRouterConfig):
115
117
  default="flock.assertion_feedback", # Useful if paired with AssertionCheckerModule
116
118
  description="Optional context key containing feedback message to potentially include when retrying.",
117
119
  )
118
- feedback_on_failure: str | None = Field(
119
- default=None,
120
- description="Default feedback message to use when condition evaluation fails.",
121
- )
122
120
  retry_count_context_key_prefix: str = Field(
123
121
  default="flock.conditional_retry_count_",
124
122
  description="Internal prefix for context key storing retry attempts per agent.",
@@ -126,7 +124,7 @@ class ConditionalRouterConfig(FlockRouterConfig):
126
124
 
127
125
  # --- Validator to ensure only one condition type is set ---
128
126
  @model_validator(mode="after")
129
- def check_exclusive_condition(self) -> "ConditionalRouterConfig":
127
+ def check_exclusive_condition(self) -> "ConditionalRoutingConfig":
130
128
  conditions_set = [
131
129
  self.condition_callable is not None,
132
130
  self.expected_string is not None
@@ -141,27 +139,37 @@ class ConditionalRouterConfig(FlockRouterConfig):
141
139
  ]
142
140
  if sum(conditions_set) > 1:
143
141
  raise ValueError(
144
- "Only one type of condition (callable, string/length, number, list size, type, boolean, exists) can be configured per ConditionalRouter."
142
+ "Only one type of condition (callable, string/length, number, list size, type, boolean, exists) can be configured per ConditionalRoutingComponent."
145
143
  )
146
144
  if sum(conditions_set) == 0:
147
145
  raise ValueError(
148
- "At least one condition type must be configured for ConditionalRouter."
146
+ "At least one condition type must be configured for ConditionalRoutingComponent."
149
147
  )
150
148
  return self
151
149
 
152
150
 
153
- @flock_component(config_class=ConditionalRouterConfig)
154
- class ConditionalRouter(FlockRouter):
151
+ @flock_component(config_class=ConditionalRoutingConfig)
152
+ class ConditionalRoutingComponent(RoutingComponent):
155
153
  """Routes workflow based on evaluating a condition against a value in the FlockContext.
154
+
156
155
  Supports various built-in checks (string, number, list, type, bool, existence)
157
156
  or a custom callable. Can optionally retry the current agent on failure.
158
157
  """
159
158
 
160
- name: str = "conditional_router"
161
- config: ConditionalRouterConfig = Field(
162
- default_factory=ConditionalRouterConfig
159
+ config: ConditionalRoutingConfig = Field(
160
+ default_factory=ConditionalRoutingConfig
163
161
  )
164
162
 
163
+ def __init__(
164
+ self,
165
+ name: str = "conditional_router",
166
+ config: ConditionalRoutingConfig | None = None,
167
+ **data,
168
+ ):
169
+ if config is None:
170
+ config = ConditionalRoutingConfig()
171
+ super().__init__(name=name, config=config, **data)
172
+
165
173
  def _evaluate_condition(self, value: Any) -> tuple[bool, str | None]:
166
174
  """Evaluates the condition based on the router's configuration.
167
175
 
@@ -172,7 +180,7 @@ class ConditionalRouter(FlockRouter):
172
180
  """
173
181
  cfg = self.config
174
182
  condition_passed = False
175
- feedback = cfg.feedback_on_failure # Default feedback
183
+ feedback = None # Default feedback
176
184
  condition_type = "unknown"
177
185
 
178
186
  try:
@@ -377,12 +385,17 @@ class ConditionalRouter(FlockRouter):
377
385
  feedback,
378
386
  ) # Treat evaluation errors as condition failure
379
387
 
380
- async def route(
388
+ async def determine_next_step(
381
389
  self,
382
- current_agent: FlockAgent,
390
+ agent: "FlockAgent",
383
391
  result: dict[str, Any],
384
- context: FlockContext,
385
- ) -> HandOffRequest:
392
+ context: FlockContext | None = None,
393
+ ) -> None:
394
+ """Determine next step based on evaluating a condition against context value."""
395
+ if not context:
396
+ logger.warning("No context provided for conditional routing")
397
+ return
398
+
386
399
  cfg = self.config
387
400
  condition_value = context.get_variable(cfg.condition_context_key, None)
388
401
  feedback_value = context.get_variable(cfg.feedback_context_key, None)
@@ -399,17 +412,17 @@ class ConditionalRouter(FlockRouter):
399
412
  if condition_passed:
400
413
  # --- Success Path ---
401
414
  logger.info(
402
- f"Condition PASSED for agent '{current_agent.name}'. Routing to success path."
415
+ f"Condition PASSED for agent '{agent.name}'. Routing to success path."
403
416
  )
404
417
  # Reset retry count if applicable
405
418
  if cfg.retry_on_failure:
406
419
  retry_key = (
407
- f"{cfg.retry_count_context_key_prefix}{current_agent.name}"
420
+ f"{cfg.retry_count_context_key_prefix}{agent.name}"
408
421
  )
409
422
  if retry_key in context.state:
410
423
  del context.state[retry_key]
411
424
  logger.debug(
412
- f"Reset retry count for agent '{current_agent.name}'."
425
+ f"Reset retry count for agent '{agent.name}'."
413
426
  )
414
427
 
415
428
  # Clear feedback from context on success
@@ -422,20 +435,21 @@ class ConditionalRouter(FlockRouter):
422
435
  f"Cleared feedback key '{cfg.feedback_context_key}' on success."
423
436
  )
424
437
 
425
- next_agent = cfg.success_agent or "" # Stop chain if None
438
+ next_agent = cfg.success_agent or None # Stop chain if None
426
439
  logger.debug(f"Success route target: '{next_agent}'")
427
- return HandOffRequest(next_agent=next_agent)
440
+
441
+ agent.next_agent = next_agent # Set directly on agent
428
442
 
429
443
  else:
430
444
  # --- Failure Path ---
431
445
  logger.warning(
432
- f"Condition FAILED for agent '{current_agent.name}'. Reason: {feedback_msg}"
446
+ f"Condition FAILED for agent '{agent.name}'. Reason: {feedback_msg}"
433
447
  )
434
448
 
435
449
  if cfg.retry_on_failure:
436
450
  # --- Retry Logic ---
437
451
  retry_key = (
438
- f"{cfg.retry_count_context_key_prefix}{current_agent.name}"
452
+ f"{cfg.retry_count_context_key_prefix}{agent.name}"
439
453
  )
440
454
  retry_count = context.get_variable(retry_key, 0)
441
455
 
@@ -443,44 +457,38 @@ class ConditionalRouter(FlockRouter):
443
457
  next_retry_count = retry_count + 1
444
458
  context.set_variable(retry_key, next_retry_count)
445
459
  logger.info(
446
- f"Routing back to agent '{current_agent.name}' for retry #{next_retry_count}/{cfg.max_retries}."
460
+ f"Routing back to agent '{agent.name}' for retry #{next_retry_count}/{cfg.max_retries}."
447
461
  )
448
462
 
449
463
  # Add specific feedback to context if retry is enabled
450
464
  if cfg.feedback_context_key:
451
465
  context.set_variable(
452
466
  cfg.feedback_context_key,
453
- feedback_msg or cfg.feedback_on_failure,
467
+ feedback_msg or "Condition failed",
454
468
  )
455
469
  logger.debug(
456
- f"Set feedback key '{cfg.feedback_context_key}': {feedback_msg or cfg.feedback_on_failure}"
470
+ f"Set feedback key '{cfg.feedback_context_key}': {feedback_msg or 'Condition failed'}"
457
471
  )
458
472
 
459
- return HandOffRequest(
460
- next_agent=current_agent.name, # Route back to self
461
- output_to_input_merge_strategy="add", # Make feedback available
462
- )
473
+ agent.next_agent = agent.name # Route back to self
463
474
  else:
464
475
  # --- Max Retries Exceeded ---
465
476
  logger.error(
466
- f"Max retries ({cfg.max_retries}) exceeded for agent '{current_agent.name}'."
477
+ f"Max retries ({cfg.max_retries}) exceeded for agent '{agent.name}'."
467
478
  )
468
479
  if retry_key in context.state:
469
480
  del context.state[retry_key] # Reset count
470
- # Clear feedback before final failure route? Optional.
471
- # if cfg.feedback_context_key in context.state: del context.state[cfg.feedback_context_key]
472
- next_agent = cfg.failure_agent or ""
481
+ next_agent = cfg.failure_agent or None
473
482
  logger.debug(
474
483
  f"Failure route target (after retries): '{next_agent}'"
475
484
  )
476
- return HandOffRequest(next_agent=next_agent)
485
+
486
+ agent.next_agent = next_agent
477
487
  else:
478
488
  # --- No Retry Logic ---
479
489
  next_agent = (
480
- cfg.failure_agent or ""
490
+ cfg.failure_agent or None
481
491
  ) # Use failure agent or stop
482
492
  logger.debug(f"Failure route target (no retry): '{next_agent}'")
483
- # Optionally add feedback even if not retrying?
484
- # if cfg.feedback_context_key:
485
- # context.set_variable(cfg.feedback_context_key, feedback_msg or cfg.feedback_on_failure)
486
- return HandOffRequest(next_agent=next_agent)
493
+
494
+ 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 import RoutingComponent
11
+ from flock.core.context.context import FlockContext
12
+ from flock.core.logging.logging import get_logger
13
+ from flock.core.registry import flock_component
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(RoutingComponent):
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