flock-core 0.4.520__py3-none-any.whl → 0.5.0b1__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 (103) hide show
  1. flock/cli/manage_agents.py +3 -3
  2. flock/components/__init__.py +28 -0
  3. flock/components/evaluation/__init__.py +9 -0
  4. flock/components/evaluation/declarative_evaluation_component.py +198 -0
  5. flock/components/routing/__init__.py +15 -0
  6. flock/{routers/conditional/conditional_router.py → components/routing/conditional_routing_component.py} +60 -49
  7. flock/components/routing/default_routing_component.py +103 -0
  8. flock/components/routing/llm_routing_component.py +208 -0
  9. flock/components/utility/__init__.py +15 -0
  10. flock/{modules/enterprise_memory/enterprise_memory_module.py → components/utility/memory_utility_component.py} +195 -173
  11. flock/{modules/performance/metrics_module.py → components/utility/metrics_utility_component.py} +101 -86
  12. flock/{modules/output/output_module.py → components/utility/output_utility_component.py} +49 -49
  13. flock/core/__init__.py +2 -8
  14. flock/core/agent/__init__.py +16 -0
  15. flock/core/agent/flock_agent_components.py +104 -0
  16. flock/core/agent/flock_agent_execution.py +101 -0
  17. flock/core/agent/flock_agent_integration.py +147 -0
  18. flock/core/agent/flock_agent_lifecycle.py +177 -0
  19. flock/core/agent/flock_agent_serialization.py +378 -0
  20. flock/core/component/__init__.py +15 -0
  21. flock/core/{flock_module.py → component/agent_component_base.py} +136 -35
  22. flock/core/component/evaluation_component_base.py +56 -0
  23. flock/core/component/routing_component_base.py +75 -0
  24. flock/core/component/utility_component_base.py +69 -0
  25. flock/core/config/flock_agent_config.py +49 -2
  26. flock/core/evaluation/utils.py +1 -1
  27. flock/core/execution/evaluation_executor.py +1 -1
  28. flock/core/flock.py +137 -483
  29. flock/core/flock_agent.py +151 -1018
  30. flock/core/flock_factory.py +94 -73
  31. flock/core/{flock_registry.py → flock_registry.py.backup} +3 -17
  32. flock/core/logging/logging.py +1 -0
  33. flock/core/mcp/flock_mcp_server.py +42 -37
  34. flock/core/mixin/dspy_integration.py +5 -5
  35. flock/core/orchestration/__init__.py +18 -0
  36. flock/core/orchestration/flock_batch_processor.py +94 -0
  37. flock/core/orchestration/flock_evaluator.py +113 -0
  38. flock/core/orchestration/flock_execution.py +288 -0
  39. flock/core/orchestration/flock_initialization.py +125 -0
  40. flock/core/orchestration/flock_server_manager.py +65 -0
  41. flock/core/orchestration/flock_web_server.py +117 -0
  42. flock/core/registry/__init__.py +39 -0
  43. flock/core/registry/agent_registry.py +69 -0
  44. flock/core/registry/callable_registry.py +139 -0
  45. flock/core/registry/component_discovery.py +142 -0
  46. flock/core/registry/component_registry.py +64 -0
  47. flock/core/registry/config_mapping.py +64 -0
  48. flock/core/registry/decorators.py +137 -0
  49. flock/core/registry/registry_hub.py +202 -0
  50. flock/core/registry/server_registry.py +57 -0
  51. flock/core/registry/type_registry.py +86 -0
  52. flock/core/serialization/flock_serializer.py +33 -30
  53. flock/core/serialization/serialization_utils.py +28 -25
  54. flock/core/util/input_resolver.py +29 -2
  55. flock/platform/docker_tools.py +3 -3
  56. flock/tools/markdown_tools.py +1 -2
  57. flock/tools/text_tools.py +1 -2
  58. flock/webapp/app/main.py +9 -5
  59. flock/workflow/activities.py +59 -84
  60. flock/workflow/activities_unified.py +230 -0
  61. flock/workflow/agent_execution_activity.py +6 -6
  62. flock/workflow/flock_workflow.py +1 -1
  63. {flock_core-0.4.520.dist-info → flock_core-0.5.0b1.dist-info}/METADATA +2 -2
  64. {flock_core-0.4.520.dist-info → flock_core-0.5.0b1.dist-info}/RECORD +67 -68
  65. flock/core/flock_evaluator.py +0 -60
  66. flock/core/flock_router.py +0 -83
  67. flock/evaluators/__init__.py +0 -1
  68. flock/evaluators/declarative/__init__.py +0 -1
  69. flock/evaluators/declarative/declarative_evaluator.py +0 -194
  70. flock/evaluators/memory/memory_evaluator.py +0 -90
  71. flock/evaluators/test/test_case_evaluator.py +0 -38
  72. flock/evaluators/zep/zep_evaluator.py +0 -59
  73. flock/modules/__init__.py +0 -1
  74. flock/modules/assertion/__init__.py +0 -1
  75. flock/modules/assertion/assertion_module.py +0 -286
  76. flock/modules/callback/__init__.py +0 -1
  77. flock/modules/callback/callback_module.py +0 -91
  78. flock/modules/enterprise_memory/README.md +0 -99
  79. flock/modules/mem0/__init__.py +0 -1
  80. flock/modules/mem0/mem0_module.py +0 -126
  81. flock/modules/mem0_async/__init__.py +0 -1
  82. flock/modules/mem0_async/async_mem0_module.py +0 -126
  83. flock/modules/memory/__init__.py +0 -1
  84. flock/modules/memory/memory_module.py +0 -429
  85. flock/modules/memory/memory_parser.py +0 -125
  86. flock/modules/memory/memory_storage.py +0 -736
  87. flock/modules/output/__init__.py +0 -1
  88. flock/modules/performance/__init__.py +0 -1
  89. flock/modules/zep/__init__.py +0 -1
  90. flock/modules/zep/zep_module.py +0 -192
  91. flock/routers/__init__.py +0 -1
  92. flock/routers/agent/__init__.py +0 -1
  93. flock/routers/agent/agent_router.py +0 -236
  94. flock/routers/agent/handoff_agent.py +0 -58
  95. flock/routers/default/__init__.py +0 -1
  96. flock/routers/default/default_router.py +0 -80
  97. flock/routers/feedback/feedback_router.py +0 -114
  98. flock/routers/list_generator/list_generator_router.py +0 -166
  99. flock/routers/llm/__init__.py +0 -1
  100. flock/routers/llm/llm_router.py +0 -365
  101. {flock_core-0.4.520.dist-info → flock_core-0.5.0b1.dist-info}/WHEEL +0 -0
  102. {flock_core-0.4.520.dist-info → flock_core-0.5.0b1.dist-info}/entry_points.txt +0 -0
  103. {flock_core-0.4.520.dist-info → flock_core-0.5.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -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.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
@@ -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/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_base import RoutingComponentBase
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
+ 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
- logger = get_logger("router.conditional")
17
+ if TYPE_CHECKING:
18
+ from flock.core.flock_agent import FlockAgent
20
19
 
20
+ logger = get_logger("components.routing.conditional")
21
21
 
22
- class ConditionalRouterConfig(FlockRouterConfig):
23
- """Configuration for the ConditionalRouter."""
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) -> "ConditionalRouterConfig":
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 ConditionalRouter."
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 ConditionalRouter."
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=ConditionalRouterConfig)
150
- class ConditionalRouter(FlockRouter):
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
- name: str = "conditional_router"
157
- config: ConditionalRouterConfig = Field(
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 = cfg.feedback_on_failure # Default 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 route(
387
+ async def determine_next_step(
377
388
  self,
378
- current_agent: FlockAgent,
389
+ agent: "FlockAgent",
379
390
  result: dict[str, Any],
380
- context: FlockContext,
381
- ) -> HandOffRequest:
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 '{current_agent.name}'. Routing to success path."
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}{current_agent.name}"
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 '{current_agent.name}'."
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 "" # Stop chain if None
437
+ next_agent = cfg.success_agent or None # Stop chain if None
422
438
  logger.debug(f"Success route target: '{next_agent}'")
423
- return HandOffRequest(next_agent=next_agent)
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 '{current_agent.name}'. Reason: {feedback_msg}"
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}{current_agent.name}"
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 '{current_agent.name}' for retry #{next_retry_count}/{cfg.max_retries}."
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 cfg.feedback_on_failure,
466
+ feedback_msg or "Condition failed",
450
467
  )
451
468
  logger.debug(
452
- f"Set feedback key '{cfg.feedback_context_key}': {feedback_msg or cfg.feedback_on_failure}"
469
+ f"Set feedback key '{cfg.feedback_context_key}': {feedback_msg or 'Condition failed'}"
453
470
  )
454
471
 
455
- return HandOffRequest(
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 '{current_agent.name}'."
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
- # Clear feedback before final failure route? Optional.
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
- return HandOffRequest(next_agent=next_agent)
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
- # Optionally add feedback even if not retrying?
480
- # if cfg.feedback_context_key:
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