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
flock/core/flock_agent.py CHANGED
@@ -1,50 +1,34 @@
1
1
  # src/flock/core/flock_agent.py
2
- """FlockAgent is the core, declarative base class for all agents in the Flock framework."""
2
+ """FlockAgent with unified component architecture."""
3
3
 
4
- import asyncio
5
- import json
6
- import os
7
4
  import uuid
8
5
  from abc import ABC
9
6
  from collections.abc import Callable
10
- from datetime import datetime
11
- from typing import TYPE_CHECKING, Any, TypeVar
7
+ from typing import Any, TypeVar
12
8
 
13
- from flock.core.config.flock_agent_config import FlockAgentConfig
14
- from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
15
- from flock.core.serialization.json_encoder import FlockJSONEncoder
16
- from flock.workflow.temporal_config import TemporalActivityConfig
17
-
18
- if TYPE_CHECKING:
19
- from flock.core.context.context import FlockContext
20
- from flock.core.flock_evaluator import FlockEvaluator
21
- from flock.core.flock_module import FlockModule
22
- from flock.core.flock_router import FlockRouter
23
-
24
- from opentelemetry import trace
25
9
  from pydantic import BaseModel, Field
26
10
 
27
- # Core Flock components (ensure these are importable)
11
+ from flock.core.agent.flock_agent_execution import FlockAgentExecution
12
+ from flock.core.agent.flock_agent_integration import FlockAgentIntegration
13
+ from flock.core.agent.flock_agent_serialization import FlockAgentSerialization
14
+ from flock.core.component.agent_component_base import AgentComponent
15
+ from flock.core.component.evaluation_component_base import (
16
+ EvaluationComponentBase,
17
+ )
18
+ from flock.core.component.routing_component_base import RoutingComponentBase
19
+ from flock.core.config.flock_agent_config import FlockAgentConfig
28
20
  from flock.core.context.context import FlockContext
29
- from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
30
- from flock.core.flock_module import FlockModule, FlockModuleConfig
31
- from flock.core.flock_router import FlockRouter, FlockRouterConfig
32
21
  from flock.core.logging.logging import get_logger
22
+ from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
33
23
 
34
24
  # Mixins and Serialization components
35
25
  from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
36
- from flock.core.serialization.serializable import (
37
- Serializable, # Import Serializable base
38
- )
39
- from flock.core.serialization.serialization_utils import (
40
- deserialize_component,
41
- serialize_item,
42
- )
26
+ from flock.core.serialization.serializable import Serializable
27
+ from flock.workflow.temporal_config import TemporalActivityConfig
43
28
 
44
- logger = get_logger("agent")
45
- tracer = trace.get_tracer(__name__)
46
- T = TypeVar("T", bound="FlockAgent")
29
+ logger = get_logger("agent.unified")
47
30
 
31
+ T = TypeVar("T", bound="FlockAgent")
48
32
 
49
33
  SignatureType = (
50
34
  str
@@ -55,16 +39,22 @@ SignatureType = (
55
39
  )
56
40
 
57
41
 
58
- # Make FlockAgent inherit from Serializable
59
42
  class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
60
- """Core, declarative base class for Flock agents, enabling serialization,
61
- modularity, and integration with evaluation and routing components.
62
- Inherits from Pydantic BaseModel, ABC, DSPyIntegrationMixin, and Serializable.
43
+ """Unified FlockAgent using the new component architecture.
44
+
45
+ This is the next-generation FlockAgent that uses a single components list
46
+ instead of separate evaluator, router, and modules. All agent functionality
47
+ is now provided through AgentComponent instances.
48
+
49
+ Key changes:
50
+ - components: list[AgentComponent] - unified component list
51
+ - next_agent: str | None - explicit workflow state
52
+ - evaluator/router properties - convenience access to primary components
63
53
  """
64
54
 
65
55
  agent_id: str = Field(
66
56
  default_factory=lambda: str(uuid.uuid4()),
67
- description="Internal, Unique UUID4 for this agent instance. No need to set it manually. Used for MCP features.",
57
+ description="Internal, Unique UUID4 for this agent instance.",
68
58
  )
69
59
 
70
60
  name: str = Field(..., description="Unique identifier for the agent.")
@@ -79,67 +69,48 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
79
69
  )
80
70
  input: SignatureType = Field(
81
71
  None,
82
- description=(
83
- "Signature for input keys. Supports type hints (:) and descriptions (|). "
84
- "E.g., 'query: str | Search query, context: dict | Conversation context'. Can be a callable."
85
- ),
72
+ description="Signature for input keys. Supports type hints (:) and descriptions (|).",
86
73
  )
87
74
  output: SignatureType = Field(
88
75
  None,
89
- description=(
90
- "Signature for output keys. Supports type hints (:) and descriptions (|). "
91
- "E.g., 'result: str | Generated result, summary: str | Brief summary'. Can be a callable."
92
- ),
76
+ description="Signature for output keys. Supports type hints (:) and descriptions (|).",
93
77
  )
94
- tools: list[Callable[..., Any]] | None = (
95
- Field( # Assume tools are always callable for serialization simplicity
96
- default=None,
97
- description="List of callable tools the agent can use. These must be registered.",
98
- )
78
+ tools: list[Callable[..., Any]] | None = Field(
79
+ default=None,
80
+ description="List of callable tools the agent can use. These must be registered.",
99
81
  )
100
82
  servers: list[str | FlockMCPServerBase] | None = Field(
101
83
  default=None,
102
- description="List of MCP Servers the agent can use to enhance its capabilities. These must be registered.",
84
+ description="List of MCP Servers the agent can use to enhance its capabilities.",
103
85
  )
104
86
 
105
- write_to_file: bool = Field(
106
- default=False,
107
- description="Write the agent's output to a file.",
108
- )
109
- wait_for_input: bool = Field(
110
- default=False,
111
- description="Wait for user input after the agent's output is displayed.",
87
+ # --- UNIFIED COMPONENT SYSTEM ---
88
+ components: list[AgentComponent] = Field(
89
+ default_factory=list,
90
+ description="List of all agent components (evaluators, routers, modules).",
112
91
  )
113
92
 
114
- # --- Components ---
115
- evaluator: FlockEvaluator | None = Field( # Make optional, allow None
116
- default=None,
117
- description="The evaluator instance defining the agent's core logic.",
118
- )
119
- handoff_router: FlockRouter | None = Field( # Make optional, allow None
93
+ # --- EXPLICIT WORKFLOW STATE ---
94
+ next_agent: str | Callable[..., str] | None = Field(
120
95
  default=None,
121
- description="Router determining the next agent in the workflow.",
122
- )
123
- modules: dict[str, FlockModule] = Field( # Keep as dict
124
- default_factory=dict,
125
- description="Dictionary of FlockModules attached to this agent.",
96
+ # exclude=True, # Runtime state, don't serialize
97
+ description="Next agent in workflow - set by user or routing components.",
126
98
  )
127
99
 
128
100
  config: FlockAgentConfig = Field(
129
101
  default_factory=lambda: FlockAgentConfig(),
130
- description="Configuration for this agent, holding various settings and parameters.",
102
+ description="Configuration for this agent.",
131
103
  )
132
104
 
133
- # --- Temporal Configuration (Optional) ---
134
105
  temporal_activity_config: TemporalActivityConfig | None = Field(
135
106
  default=None,
136
- description="Optional Temporal settings specific to this agent's activity execution.",
107
+ description="Optional Temporal settings specific to this agent.",
137
108
  )
138
109
 
139
110
  # --- Runtime State (Excluded from Serialization) ---
140
111
  context: FlockContext | None = Field(
141
112
  default=None,
142
- exclude=True, # Exclude context from model_dump and serialization
113
+ exclude=True,
143
114
  description="Runtime context associated with the flock execution.",
144
115
  )
145
116
 
@@ -152,344 +123,138 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
152
123
  output: SignatureType = None,
153
124
  tools: list[Callable[..., Any]] | None = None,
154
125
  servers: list[str | FlockMCPServerBase] | None = None,
155
- evaluator: "FlockEvaluator | None" = None,
156
- handoff_router: "FlockRouter | None" = None,
157
- # Use dict for modules
158
- modules: dict[str, "FlockModule"] | None = None,
159
- write_to_file: bool = False,
160
- wait_for_input: bool = False,
126
+ components: list[AgentComponent] | None = None,
127
+ config: FlockAgentConfig | None = None,
128
+ next_agent: str | Callable[..., str] | None = None,
161
129
  temporal_activity_config: TemporalActivityConfig | None = None,
162
- **kwargs,
163
130
  ):
131
+ """Initialize the unified FlockAgent with components and configuration."""
132
+ if config is None:
133
+ config = FlockAgentConfig()
164
134
  super().__init__(
165
135
  name=name,
166
136
  model=model,
167
137
  description=description,
168
- input=input, # Store the raw input spec
169
- output=output, # Store the raw output spec
138
+ input=input,
139
+ output=output,
170
140
  tools=tools,
171
141
  servers=servers,
172
- write_to_file=write_to_file,
173
- wait_for_input=wait_for_input,
174
- evaluator=evaluator,
175
- handoff_router=handoff_router,
176
- modules=modules
177
- if modules is not None
178
- else {}, # Ensure modules is a dict
142
+ components=components if components is not None else [],
143
+ config=config,
179
144
  temporal_activity_config=temporal_activity_config,
180
- **kwargs,
145
+ next_agent=next_agent,
181
146
  )
182
147
 
183
- if isinstance(self.input, type) and issubclass(self.input, BaseModel):
184
- self._input_model = self.input
185
- if isinstance(self.output, type) and issubclass(self.output, BaseModel):
186
- self._output_model = self.output
187
-
188
- # --- Existing Methods (add_module, remove_module, etc.) ---
189
- # (Keep these methods as they were, adding type hints where useful)
190
- def add_module(self, module: FlockModule) -> None:
191
- """Add a module to this agent."""
192
- if not module.name:
193
- logger.error("Module must have a name to be added.")
194
- return
195
- if module.name in self.modules:
196
- logger.warning(f"Overwriting existing module: {module.name}")
197
- self.modules[module.name] = module
198
- logger.debug(f"Added module '{module.name}' to agent '{self.name}'")
148
+ # Initialize helper systems (reuse existing logic)
149
+ self._execution = FlockAgentExecution(self)
150
+ self._integration = FlockAgentIntegration(self)
151
+ self._serialization = FlockAgentSerialization(self)
152
+ # Lifecycle will be lazy-loaded when needed
199
153
 
200
- def remove_module(self, module_name: str) -> None:
201
- """Remove a module from this agent."""
202
- if module_name in self.modules:
203
- del self.modules[module_name]
204
- logger.debug(
205
- f"Removed module '{module_name}' from agent '{self.name}'"
206
- )
207
- else:
208
- logger.warning(
209
- f"Module '{module_name}' not found on agent '{self.name}'."
210
- )
211
-
212
- def get_module(self, module_name: str) -> FlockModule | None:
213
- """Get a module by name."""
214
- return self.modules.get(module_name)
215
-
216
- def get_enabled_modules(self) -> list[FlockModule]:
217
- """Get a list of currently enabled modules attached to this agent."""
218
- return [m for m in self.modules.values() if m.config.enabled]
154
+ # --- CONVENIENCE PROPERTIES ---
155
+ # These provide familiar access patterns while using the unified model
219
156
 
220
157
  @property
221
- def resolved_description(self) -> str | None:
222
- """Returns the resolved agent description.
223
- If the description is a callable, it attempts to call it.
224
- Returns None if the description is None or a callable that fails.
225
- """
226
- if callable(self.description):
227
- try:
228
- # Attempt to call without context first.
229
- # If callables consistently need context, this might need adjustment
230
- # or the template-facing property might need to be simpler,
231
- # relying on prior resolution via resolve_callables.
232
- return self.description()
233
- except TypeError:
234
- # Log a warning that context might be needed?
235
- # For now, treat as unresolvable in this simple property.
236
- logger.warning(
237
- f"Callable description for agent '{self.name}' could not be resolved "
238
- f"without context via the simple 'resolved_description' property. "
239
- f"Consider calling 'agent.resolve_callables(context)' beforehand if context is required."
240
- )
241
- return None # Or a placeholder like "[Callable Description]"
242
- except Exception as e:
243
- logger.error(
244
- f"Error resolving callable description for agent '{self.name}': {e}"
245
- )
246
- return None
247
- elif isinstance(self.description, str):
248
- return self.description
249
- return None
250
-
251
- # --- Lifecycle Hooks (Keep as they were) ---
252
- async def initialize(self, inputs: dict[str, Any]) -> None:
253
- """Initialize agent and run module initializers."""
254
- logger.debug(f"Initializing agent '{self.name}'")
255
- with tracer.start_as_current_span("agent.initialize") as span:
256
- span.set_attribute("agent.name", self.name)
257
- span.set_attribute("inputs", str(inputs))
258
- logger.info(
259
- f"agent.initialize",
260
- agent=self.name,
261
- )
262
- try:
263
- for module in self.get_enabled_modules():
264
- await module.on_initialize(self, inputs, self.context)
265
- except Exception as module_error:
266
- logger.error(
267
- "Error during initialize",
268
- agent=self.name,
269
- error=str(module_error),
270
- )
271
- span.record_exception(module_error)
272
-
273
- async def terminate(
274
- self, inputs: dict[str, Any], result: dict[str, Any]
275
- ) -> None:
276
- """Terminate agent and run module terminators."""
277
- logger.debug(f"Terminating agent '{self.name}'")
278
- with tracer.start_as_current_span("agent.terminate") as span:
279
- span.set_attribute("agent.name", self.name)
280
- span.set_attribute("inputs", str(inputs))
281
- span.set_attribute("result", str(result))
282
- logger.info(
283
- f"agent.terminate",
284
- agent=self.name,
285
- )
286
- try:
287
- current_result = result
288
- for module in self.get_enabled_modules():
289
- tmp_result = await module.on_terminate(
290
- self, inputs, self.context, current_result
291
- )
292
- # If the module returns a result, use it
293
- if tmp_result:
294
- current_result = tmp_result
295
-
296
- if self.write_to_file:
297
- self._save_output(self.name, current_result)
298
-
299
- if self.wait_for_input:
300
- # simple input prompt
301
- input("Press Enter to continue...")
158
+ def evaluator(self) -> EvaluationComponentBase | None:
159
+ """Get the primary evaluation component for this agent."""
160
+ return self._components.get_primary_evaluator()
302
161
 
303
- except Exception as module_error:
304
- logger.error(
305
- "Error during terminate",
306
- agent=self.name,
307
- error=str(module_error),
308
- )
309
- span.record_exception(module_error)
162
+ @property
163
+ def router(self) -> RoutingComponentBase | None:
164
+ """Get the primary routing component for this agent."""
165
+ return self._components.get_primary_router()
310
166
 
311
- async def on_error(self, error: Exception, inputs: dict[str, Any]) -> None:
312
- """Handle errors and run module error handlers."""
313
- logger.error(f"Error occurred in agent '{self.name}': {error}")
314
- with tracer.start_as_current_span("agent.on_error") as span:
315
- span.set_attribute("agent.name", self.name)
316
- span.set_attribute("inputs", str(inputs))
317
- try:
318
- for module in self.get_enabled_modules():
319
- await module.on_error(self, inputs, self.context, error)
320
- except Exception as module_error:
321
- logger.error(
322
- "Error during on_error",
323
- agent=self.name,
324
- error=str(module_error),
325
- )
326
- span.record_exception(module_error)
167
+ @property
168
+ def modules(self) -> list[AgentComponent]:
169
+ """Get all components (for backward compatibility with module-style access)."""
170
+ return self.components.copy()
327
171
 
328
- async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
329
- """Core evaluation logic, calling the assigned evaluator and modules."""
330
- if not self.evaluator:
331
- raise RuntimeError(
332
- f"Agent '{self.name}' has no evaluator assigned."
333
- )
334
- with tracer.start_as_current_span("agent.evaluate") as span:
335
- span.set_attribute("agent.name", self.name)
336
- span.set_attribute("inputs", str(inputs))
337
- logger.info(
338
- f"agent.evaluate",
339
- agent=self.name,
172
+ @property
173
+ def _components(self):
174
+ """Get the component management helper."""
175
+ if not hasattr(self, '_components_helper'):
176
+ from flock.core.agent.flock_agent_components import (
177
+ FlockAgentComponents,
340
178
  )
179
+ self._components_helper = FlockAgentComponents(self)
180
+ return self._components_helper
341
181
 
342
- logger.debug(f"Evaluating agent '{self.name}'")
343
- current_inputs = inputs
344
-
345
- # Pre-evaluate hooks
346
- for module in self.get_enabled_modules():
347
- current_inputs = await module.on_pre_evaluate(
348
- self, current_inputs, self.context
349
- )
182
+ # Component management delegated to _components
183
+ def add_component(self, component: AgentComponent) -> None:
184
+ """Add a component to this agent."""
185
+ self._components.add_component(component)
350
186
 
351
- # Actual evaluation
352
- try:
353
- # Pass registered tools if the evaluator needs them
354
- registered_tools = []
355
- if self.tools:
356
- # Ensure tools are actually retrieved/validated if needed by evaluator type
357
- # For now, assume evaluator handles tool resolution if necessary
358
- registered_tools = self.tools
187
+ def remove_component(self, component_name: str) -> None:
188
+ """Remove a component from this agent."""
189
+ self._components.remove_component(component_name)
359
190
 
360
- # Retrieve available mcp_tools if the evaluator needs them
361
- mcp_tools = []
362
- if self.servers:
363
- from flock.core.flock_registry import get_registry
191
+ def get_component(self, component_name: str) -> AgentComponent | None:
192
+ """Get a component by name."""
193
+ return self._components.get_component(component_name)
364
194
 
365
- FlockRegistry = get_registry() # Get the registry
366
- for server in self.servers:
367
- registered_server: FlockMCPServerBase | None = None
368
- server_tools = []
369
- if isinstance(server, FlockMCPServerBase):
370
- # check if registered
371
- server_name = server.config.name
372
- registered_server = FlockRegistry.get_server(
373
- server_name
374
- )
375
- else:
376
- # servers must be registered.
377
- registered_server = FlockRegistry.get_server(
378
- name=server
379
- )
380
- if registered_server:
381
- server_tools = await registered_server.get_tools(
382
- agent_id=self.agent_id,
383
- run_id=self.context.run_id,
384
- )
385
- else:
386
- logger.warning(
387
- f"No Server with name '{server.config.name}' registered! Skipping."
388
- )
389
- mcp_tools = mcp_tools + server_tools
390
195
 
391
- # --------------------------------------------------
392
- # Optional DI middleware pipeline
393
- # --------------------------------------------------
394
- container = None
395
- if self.context is not None:
396
- container = self.context.get_variable("di.container")
196
+ def get_enabled_components(self) -> list[AgentComponent]:
197
+ """Get enabled components (backward compatibility)."""
198
+ return self._components.get_enabled_components()
397
199
 
398
- # If a MiddlewarePipeline is registered in DI, wrap the evaluator
399
- result: dict[str, Any] | None = None
200
+ # --- LIFECYCLE DELEGATION ---
201
+ # Delegate lifecycle methods to the composition objects
400
202
 
401
- if container is not None:
402
- try:
403
- from wd.di.middleware import (
404
- MiddlewarePipeline,
405
- )
203
+ @property
204
+ def _lifecycle(self):
205
+ """Get the lifecycle management helper (lazy-loaded)."""
206
+ if not hasattr(self, '_lifecycle_helper'):
207
+ from flock.core.agent.flock_agent_lifecycle import (
208
+ FlockAgentLifecycle,
209
+ )
210
+ self._lifecycle_helper = FlockAgentLifecycle(self)
211
+ return self._lifecycle_helper
406
212
 
407
- pipeline: MiddlewarePipeline | None = None
408
- try:
409
- pipeline = container.get_service(MiddlewarePipeline)
410
- except Exception:
411
- pipeline = None
213
+ async def initialize(self, inputs: dict[str, Any]) -> None:
214
+ """Initialize agent and run component initializers."""
215
+ return await self._lifecycle.initialize(inputs)
412
216
 
413
- if pipeline is not None:
414
- # Build execution chain where the evaluator is the terminal handler
217
+ async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
218
+ """Core evaluation logic using unified component system."""
219
+ return await self._lifecycle.evaluate(inputs)
415
220
 
416
- async def _final_handler():
417
- return await self.evaluator.evaluate(
418
- self, current_inputs, registered_tools
419
- )
221
+ async def terminate(self, inputs: dict[str, Any], result: dict[str, Any]) -> None:
222
+ """Terminate agent and run component terminators."""
223
+ return await self._lifecycle.terminate(inputs, result)
420
224
 
421
- idx = 0
225
+ async def on_error(self, error: Exception, inputs: dict[str, Any]) -> None:
226
+ """Handle errors and run component error handlers."""
227
+ return await self._lifecycle.on_error(error, inputs)
422
228
 
423
- async def _invoke_next():
424
- nonlocal idx
229
+ # --- EXECUTION METHODS ---
230
+ # Delegate to the execution system
425
231
 
426
- if idx < len(pipeline._middleware):
427
- mw = pipeline._middleware[idx]
428
- idx += 1
429
- return await mw(self.context, _invoke_next) # type: ignore[arg-type]
430
- return await _final_handler()
232
+ def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
233
+ """Synchronous wrapper for run_async."""
234
+ return self._execution.run(inputs)
431
235
 
432
- # Execute pipeline
433
- result = await _invoke_next()
434
- else:
435
- # No pipeline registered, direct evaluation
436
- result = await self.evaluator.evaluate(
437
- self, current_inputs, registered_tools
438
- )
439
- except ImportError:
440
- # wd.di not installed – fall back
441
- result = await self.evaluator.evaluate(
442
- self, current_inputs, registered_tools
443
- )
444
- else:
445
- # No DI container – standard execution
446
- result = await self.evaluator.evaluate(
447
- self,
448
- current_inputs,
449
- registered_tools,
450
- mcp_tools=mcp_tools,
451
- )
452
- except Exception as eval_error:
453
- logger.error(
454
- "Error during evaluate",
455
- agent=self.name,
456
- error=str(eval_error),
457
- )
458
- span.record_exception(eval_error)
459
- await self.on_error(
460
- eval_error, current_inputs
461
- ) # Call error hook
462
- raise # Re-raise the exception
236
+ async def run_async(self, inputs: dict[str, Any]) -> dict[str, Any]:
237
+ """Asynchronous execution logic with unified lifecycle."""
238
+ return await self._execution.run_async(inputs)
463
239
 
464
- # Post-evaluate hooks
465
- current_result = result
466
- for module in self.get_enabled_modules():
467
- tmp_result = await module.on_post_evaluate(
468
- self,
469
- current_inputs,
470
- self.context,
471
- current_result,
472
- )
473
- # If the module returns a result, use it
474
- if tmp_result:
475
- current_result = tmp_result
240
+ # --- SERIALIZATION ---
241
+ # Delegate to the serialization system
476
242
 
477
- logger.debug(f"Evaluation completed for agent '{self.name}'")
478
- return current_result
243
+ def to_dict(self) -> dict[str, Any]:
244
+ """Convert to dictionary using unified component serialization."""
245
+ return self._serialization.to_dict()
479
246
 
480
- def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
481
- """Synchronous wrapper for run_async."""
482
- try:
483
- loop = asyncio.get_running_loop()
484
- except (
485
- RuntimeError
486
- ): # 'RuntimeError: There is no current event loop...'
487
- loop = asyncio.new_event_loop()
488
- asyncio.set_event_loop(loop)
489
- return loop.run_until_complete(self.run_async(inputs))
247
+ @classmethod
248
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
249
+ """Deserialize from dictionary using unified component deserialization."""
250
+ return FlockAgentSerialization.from_dict(cls, data)
490
251
 
491
252
  def set_model(self, model: str):
492
- """Set the model for the agent and its evaluator."""
253
+ """Set the model for the agent and its evaluator.
254
+
255
+ This method updates both the agent's model property and propagates
256
+ the model to the evaluator component if it has a config with a model field.
257
+ """
493
258
  self.model = model
494
259
  if self.evaluator and hasattr(self.evaluator, "config"):
495
260
  self.evaluator.config.model = model
@@ -505,654 +270,22 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
505
270
  f"Agent '{self.name}' has no evaluator to set model for."
506
271
  )
507
272
 
508
- async def run_async(self, inputs: dict[str, Any]) -> dict[str, Any]:
509
- """Asynchronous execution logic with lifecycle hooks."""
510
- with tracer.start_as_current_span("agent.run") as span:
511
- span.set_attribute("agent.name", self.name)
512
- span.set_attribute("inputs", str(inputs))
513
- try:
514
- await self.initialize(inputs)
515
- result = await self.evaluate(inputs)
516
- await self.terminate(inputs, result)
517
- span.set_attribute("result", str(result))
518
- logger.info("Agent run completed", agent=self.name)
519
- return result
520
- except Exception as run_error:
521
- logger.error(
522
- "Error running agent", agent=self.name, error=str(run_error)
523
- )
524
- if "evaluate" not in str(
525
- run_error
526
- ): # Simple check, might need refinement
527
- await self.on_error(run_error, inputs)
528
- logger.error(
529
- f"Agent '{self.name}' run failed: {run_error}",
530
- exc_info=True,
531
- )
532
- span.record_exception(run_error)
533
- raise # Re-raise after handling
534
-
535
- async def run_temporal(self, inputs: dict[str, Any]) -> dict[str, Any]:
536
- with tracer.start_as_current_span("agent.run_temporal") as span:
537
- span.set_attribute("agent.name", self.name)
538
- span.set_attribute("inputs", str(inputs))
539
- try:
540
- from temporalio.client import Client
541
-
542
- from flock.workflow.agent_activities import (
543
- run_flock_agent_activity,
544
- )
545
- from flock.workflow.temporal_setup import run_activity
546
-
547
- client = await Client.connect(
548
- "localhost:7233", namespace="default"
549
- )
550
- agent_data = self.to_dict()
551
- inputs_data = inputs
552
-
553
- result = await run_activity(
554
- client,
555
- self.name,
556
- run_flock_agent_activity,
557
- {"agent_data": agent_data, "inputs": inputs_data},
558
- )
559
- span.set_attribute("result", str(result))
560
- logger.info("Temporal run successful", agent=self.name)
561
- return result
562
- except Exception as temporal_error:
563
- logger.error(
564
- "Error in Temporal workflow",
565
- agent=self.name,
566
- error=str(temporal_error),
567
- )
568
- span.record_exception(temporal_error)
569
- raise
570
-
571
- def add_component(
572
- self,
573
- config_instance: FlockModuleConfig
574
- | FlockRouterConfig
575
- | FlockEvaluatorConfig,
576
- component_name: str | None = None,
577
- ) -> "FlockAgent":
578
- """Adds or replaces a component (Evaluator, Router, Module) based on its configuration object.
579
-
580
- Args:
581
- config_instance: An instance of a config class inheriting from
582
- FlockModuleConfig, FlockRouterConfig, or FlockEvaluatorConfig.
583
- component_name: Explicit name for the component (required for Modules if not in config).
584
-
585
- Returns:
586
- self for potential chaining.
587
- """
588
- from flock.core.flock_registry import get_registry
589
-
590
- config_type = type(config_instance)
591
- registry = get_registry() # Get registry instance
592
- logger.debug(
593
- f"Attempting to add component via config: {config_type.__name__}"
594
- )
595
-
596
- # --- 1. Find Component Class using Registry Map ---
597
- ComponentClass = registry.get_component_class_for_config(config_type)
598
-
599
- if not ComponentClass:
600
- logger.error(
601
- f"No component class registered for config type {config_type.__name__}. Use @flock_component(config_class=...) on the component."
602
- )
603
- raise TypeError(
604
- f"Cannot find component class for config {config_type.__name__}"
605
- )
606
-
607
- component_class_name = ComponentClass.__name__
608
- logger.debug(
609
- f"Found component class '{component_class_name}' mapped to config '{config_type.__name__}'"
610
- )
611
-
612
- # --- 2. Determine Assignment Target and Name (Same as before) ---
613
- instance_name = component_name
614
- attribute_name: str = ""
615
-
616
- if issubclass(ComponentClass, FlockEvaluator):
617
- attribute_name = "evaluator"
618
- if not instance_name:
619
- instance_name = getattr(
620
- config_instance, "name", component_class_name.lower()
621
- )
622
-
623
- elif issubclass(ComponentClass, FlockRouter):
624
- attribute_name = "handoff_router"
625
- if not instance_name:
626
- instance_name = getattr(
627
- config_instance, "name", component_class_name.lower()
628
- )
629
-
630
- elif issubclass(ComponentClass, FlockModule):
631
- attribute_name = "modules"
632
- if not instance_name:
633
- instance_name = getattr(
634
- config_instance, "name", component_class_name.lower()
635
- )
636
- if not instance_name:
637
- raise ValueError(
638
- "Module name must be provided either in config or as component_name argument."
639
- )
640
- # Ensure config has name if module expects it
641
- if hasattr(config_instance, "name") and not getattr(
642
- config_instance, "name", None
643
- ):
644
- setattr(config_instance, "name", instance_name)
645
-
646
- else: # Should be caught by registry map logic ideally
647
- raise TypeError(
648
- f"Class '{component_class_name}' mapped from config is not a valid Flock component."
649
- )
650
-
651
- # --- 3. Instantiate the Component (Same as before) ---
652
- try:
653
- init_args = {"config": config_instance, "name": instance_name}
654
-
655
- component_instance = ComponentClass(**init_args)
656
- except Exception as e:
657
- logger.error(
658
- f"Failed to instantiate {ComponentClass.__name__} with config {config_type.__name__}: {e}",
659
- exc_info=True,
660
- )
661
- raise RuntimeError(f"Component instantiation failed: {e}") from e
662
-
663
- # --- 4. Assign to the Agent (Same as before) ---
664
- if attribute_name == "modules":
665
- if not isinstance(self.modules, dict):
666
- self.modules = {}
667
- self.modules[instance_name] = component_instance
668
- logger.info(
669
- f"Added/Updated module '{instance_name}' (type: {ComponentClass.__name__}) to agent '{self.name}'"
670
- )
671
- else:
672
- setattr(self, attribute_name, component_instance)
673
- logger.info(
674
- f"Set {attribute_name} to {ComponentClass.__name__} (instance name: '{instance_name}') for agent '{self.name}'"
675
- )
676
-
677
- return self
678
-
679
- # resolve_callables remains useful for dynamic definitions
680
273
  def resolve_callables(self, context: FlockContext | None = None) -> None:
681
274
  """Resolves callable fields (description, input, output) using context."""
682
- if callable(self.description):
683
- self.description = self.description(
684
- context
685
- ) # Pass context if needed by callable
686
- if callable(self.input):
687
- self.input = self.input(context)
688
- if callable(self.output):
689
- self.output = self.output(context)
275
+ self.context = context or self.context
276
+ return self._integration.resolve_callables(self.context)
690
277
 
691
- # --- Serialization Implementation ---
278
+ @property
279
+ def resolved_description(self) -> str | None:
280
+ """Returns the resolved agent description.
281
+ If the description is a callable, it attempts to call it.
282
+ Returns None if the description is None or a callable that fails.
283
+ """
284
+ return self._integration.resolve_description(self.context)
692
285
 
693
286
  def _save_output(self, agent_name: str, result: dict[str, Any]) -> None:
694
- """Save output to file if configured."""
695
- if not self.write_to_file:
696
- return
697
-
698
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
699
- filename = f"{agent_name}_output_{timestamp}.json"
700
- filepath = os.path.join(".flock/output/", filename)
701
- os.makedirs(".flock/output/", exist_ok=True)
702
-
703
- output_data = {
704
- "agent": agent_name,
705
- "timestamp": timestamp,
706
- "output": result,
707
- }
708
-
709
- try:
710
- with open(filepath, "w") as f:
711
- json.dump(output_data, f, indent=2, cls=FlockJSONEncoder)
712
- except Exception as e:
713
- logger.warning(f"Failed to save output to file: {e}")
714
-
715
- def to_dict(self) -> dict[str, Any]:
716
- """Convert instance to dictionary representation suitable for serialization."""
717
- from flock.core.flock_registry import get_registry
718
-
719
- FlockRegistry = get_registry()
720
-
721
- exclude = [
722
- "context",
723
- "evaluator",
724
- "modules",
725
- "handoff_router",
726
- "tools",
727
- "servers",
728
- ]
729
-
730
- is_descrition_callable = False
731
- is_input_callable = False
732
- is_output_callable = False
733
-
734
- # if self.description is a callable, exclude it
735
- if callable(self.description):
736
- is_descrition_callable = True
737
- exclude.append("description")
738
- # if self.input is a callable, exclude it
739
- if callable(self.input):
740
- is_input_callable = True
741
- exclude.append("input")
742
- # if self.output is a callable, exclude it
743
- if callable(self.output):
744
- is_output_callable = True
745
- exclude.append("output")
746
-
747
- logger.debug(f"Serializing agent '{self.name}' to dict.")
748
- # Use Pydantic's dump, exclude manually handled fields and runtime context
749
- data = self.model_dump(
750
- exclude=exclude,
751
- mode="json", # Use json mode for better handling of standard types by Pydantic
752
- exclude_none=True, # Exclude None values for cleaner output
753
- )
754
- logger.debug(f"Base agent data for '{self.name}': {list(data.keys())}")
755
- serialized_modules = {}
756
-
757
- def add_serialized_component(component: Any, field_name: str):
758
- if component:
759
- comp_type = type(component)
760
- type_name = FlockRegistry.get_component_type_name(
761
- comp_type
762
- ) # Get registered name
763
- if type_name:
764
- try:
765
- serialized_component_data = serialize_item(component)
766
-
767
- if not isinstance(serialized_component_data, dict):
768
- logger.error(
769
- f"Serialization of component {type_name} for field '{field_name}' did not result in a dictionary. Got: {type(serialized_component_data)}"
770
- )
771
- serialized_modules[field_name] = {
772
- "type": type_name,
773
- "name": getattr(component, "name", "unknown"),
774
- "error": "serialization_failed_non_dict",
775
- }
776
- else:
777
- serialized_component_data["type"] = type_name
778
- serialized_modules[field_name] = (
779
- serialized_component_data
780
- )
781
- logger.debug(
782
- f"Successfully serialized component for field '{field_name}' (type: {type_name})"
783
- )
784
-
785
- except Exception as e:
786
- logger.error(
787
- f"Failed to serialize component {type_name} for field '{field_name}': {e}",
788
- exc_info=True,
789
- )
790
- serialized_modules[field_name] = {
791
- "type": type_name,
792
- "name": getattr(component, "name", "unknown"),
793
- "error": "serialization_failed",
794
- }
795
- else:
796
- logger.warning(
797
- f"Cannot serialize unregistered component {comp_type.__name__} for field '{field_name}'"
798
- )
799
-
800
- add_serialized_component(self.evaluator, "evaluator")
801
- if serialized_modules:
802
- data["evaluator"] = serialized_modules["evaluator"]
803
- logger.debug(f"Added evaluator to agent '{self.name}'")
804
-
805
- serialized_modules = {}
806
- add_serialized_component(self.handoff_router, "handoff_router")
807
- if serialized_modules:
808
- data["handoff_router"] = serialized_modules["handoff_router"]
809
- logger.debug(f"Added handoff_router to agent '{self.name}'")
810
-
811
- serialized_modules = {}
812
- for module in self.modules.values():
813
- add_serialized_component(module, module.name)
814
-
815
- if serialized_modules:
816
- data["modules"] = serialized_modules
817
- logger.debug(
818
- f"Added {len(serialized_modules)} modules to agent '{self.name}'"
819
- )
820
-
821
- # --- Serialize Servers ---
822
- if self.servers:
823
- logger.debug(
824
- f"Serializing {len(self.servers)} servers for agent '{self.name}'"
825
- )
826
- serialized_servers = []
827
- for server in self.servers:
828
- if isinstance(server, FlockMCPServerBase):
829
- serialized_servers.append(server.config.name)
830
- else:
831
- # Write it down as a list of server names.
832
- serialized_servers.append(server)
833
-
834
- if serialized_servers:
835
- data["mcp_servers"] = serialized_servers
836
- logger.debug(
837
- f"Added {len(serialized_servers)} servers to agent '{self.name}'"
838
- )
839
-
840
- # --- Serialize Tools (Callables) ---
841
- if self.tools:
842
- logger.debug(
843
- f"Serializing {len(self.tools)} tools for agent '{self.name}'"
844
- )
845
- serialized_tools = []
846
- for tool in self.tools:
847
- if callable(tool) and not isinstance(tool, type):
848
- path_str = FlockRegistry.get_callable_path_string(tool)
849
- if path_str:
850
- # Get just the function name from the path string
851
- # If it's a namespaced path like module.submodule.function_name
852
- # Just use the function_name part
853
- func_name = path_str.split(".")[-1]
854
- serialized_tools.append(func_name)
855
- logger.debug(
856
- f"Added tool '{func_name}' (from path '{path_str}') to agent '{self.name}'"
857
- )
858
- else:
859
- logger.warning(
860
- f"Could not get path string for tool {tool} in agent '{self.name}'. Skipping."
861
- )
862
- else:
863
- logger.warning(
864
- f"Non-callable item found in tools list for agent '{self.name}': {tool}. Skipping."
865
- )
866
- if serialized_tools:
867
- data["tools"] = serialized_tools
868
- logger.debug(
869
- f"Added {len(serialized_tools)} tools to agent '{self.name}'"
870
- )
871
-
872
- if is_descrition_callable:
873
- path_str = FlockRegistry.get_callable_path_string(self.description)
874
- if path_str:
875
- func_name = path_str.split(".")[-1]
876
- data["description_callable"] = func_name
877
- logger.debug(
878
- f"Added description '{func_name}' (from path '{path_str}') to agent '{self.name}'"
879
- )
880
- else:
881
- logger.warning(
882
- f"Could not get path string for description {self.description} in agent '{self.name}'. Skipping."
883
- )
884
-
885
- if is_input_callable:
886
- path_str = FlockRegistry.get_callable_path_string(self.input)
887
- if path_str:
888
- func_name = path_str.split(".")[-1]
889
- data["input_callable"] = func_name
890
- logger.debug(
891
- f"Added input '{func_name}' (from path '{path_str}') to agent '{self.name}'"
892
- )
893
- else:
894
- logger.warning(
895
- f"Could not get path string for input {self.input} in agent '{self.name}'. Skipping."
896
- )
897
-
898
- if is_output_callable:
899
- path_str = FlockRegistry.get_callable_path_string(self.output)
900
- if path_str:
901
- func_name = path_str.split(".")[-1]
902
- data["output_callable"] = func_name
903
- logger.debug(
904
- f"Added output '{func_name}' (from path '{path_str}') to agent '{self.name}'"
905
- )
906
- else:
907
- logger.warning(
908
- f"Could not get path string for output {self.output} in agent '{self.name}'. Skipping."
909
- )
910
-
911
- # No need to call _filter_none_values here as model_dump(exclude_none=True) handles it
912
- logger.info(
913
- f"Serialization of agent '{self.name}' complete with {len(data)} fields"
914
- )
915
- return data
916
-
917
- @classmethod
918
- def from_dict(cls: type[T], data: dict[str, Any]) -> T:
919
- """Deserialize the agent from a dictionary, including components, tools, and callables."""
920
- from flock.core.flock_registry import (
921
- get_registry, # Import registry locally
922
- )
923
-
924
- registry = get_registry()
925
- logger.debug(
926
- f"Deserializing agent from dict. Keys: {list(data.keys())}"
927
- )
928
-
929
- # --- Separate Data ---
930
- component_configs = {}
931
- callable_configs = {}
932
- tool_config = []
933
- servers_config = []
934
- agent_data = {}
935
-
936
- component_keys = [
937
- "evaluator",
938
- "handoff_router",
939
- "modules",
940
- "temporal_activity_config",
941
- ]
942
- callable_keys = [
943
- "description_callable",
944
- "input_callable",
945
- "output_callable",
946
- ]
947
- tool_key = "tools"
948
-
949
- servers_key = "mcp_servers"
950
-
951
- for key, value in data.items():
952
- if key in component_keys and value is not None:
953
- component_configs[key] = value
954
- elif key in callable_keys and value is not None:
955
- callable_configs[key] = value
956
- elif key == tool_key and value is not None:
957
- tool_config = value # Expecting a list of names
958
- elif key == servers_key and value is not None:
959
- servers_config = value # Expecting a list of names
960
- elif key not in component_keys + callable_keys + [
961
- tool_key,
962
- servers_key,
963
- ]: # Avoid double adding
964
- agent_data[key] = value
965
- # else: ignore keys that are None or already handled
966
-
967
- # --- Deserialize Base Agent ---
968
- # Ensure required fields like 'name' are present if needed by __init__
969
- if "name" not in agent_data:
970
- raise ValueError(
971
- "Agent data must include a 'name' field for deserialization."
972
- )
973
- agent_name_log = agent_data["name"] # For logging
974
- logger.info(f"Deserializing base agent data for '{agent_name_log}'")
975
-
976
- # Pydantic should handle base fields based on type hints in __init__
977
- agent = cls(**agent_data)
978
- logger.debug(f"Base agent '{agent.name}' instantiated.")
979
-
980
- # --- Deserialize Components ---
981
- logger.debug(f"Deserializing components for '{agent.name}'")
982
- # Evaluator
983
- if "evaluator" in component_configs:
984
- try:
985
- agent.evaluator = deserialize_component(
986
- component_configs["evaluator"], FlockEvaluator
987
- )
988
- logger.debug(f"Deserialized evaluator for '{agent.name}'")
989
- except Exception as e:
990
- logger.error(
991
- f"Failed to deserialize evaluator for '{agent.name}': {e}",
992
- exc_info=True,
993
- )
994
-
995
- # Handoff Router
996
- if "handoff_router" in component_configs:
997
- try:
998
- agent.handoff_router = deserialize_component(
999
- component_configs["handoff_router"], FlockRouter
1000
- )
1001
- logger.debug(f"Deserialized handoff_router for '{agent.name}'")
1002
- except Exception as e:
1003
- logger.error(
1004
- f"Failed to deserialize handoff_router for '{agent.name}': {e}",
1005
- exc_info=True,
1006
- )
1007
-
1008
- # Modules
1009
- if "modules" in component_configs:
1010
- agent.modules = {} # Initialize
1011
- for module_name, module_data in component_configs[
1012
- "modules"
1013
- ].items():
1014
- try:
1015
- module_instance = deserialize_component(
1016
- module_data, FlockModule
1017
- )
1018
- if module_instance:
1019
- # Use add_module for potential logic within it
1020
- agent.add_module(module_instance)
1021
- logger.debug(
1022
- f"Deserialized and added module '{module_name}' for '{agent.name}'"
1023
- )
1024
- except Exception as e:
1025
- logger.error(
1026
- f"Failed to deserialize module '{module_name}' for '{agent.name}': {e}",
1027
- exc_info=True,
1028
- )
1029
-
1030
- # Temporal Activity Config
1031
- if "temporal_activity_config" in component_configs:
1032
- try:
1033
- agent.temporal_activity_config = TemporalActivityConfig(
1034
- **component_configs["temporal_activity_config"]
1035
- )
1036
- logger.debug(
1037
- f"Deserialized temporal_activity_config for '{agent.name}'"
1038
- )
1039
- except Exception as e:
1040
- logger.error(
1041
- f"Failed to deserialize temporal_activity_config for '{agent.name}': {e}",
1042
- exc_info=True,
1043
- )
1044
- agent.temporal_activity_config = None
1045
-
1046
- # --- Deserialize Tools ---
1047
- agent.tools = [] # Initialize tools list
1048
- if tool_config:
1049
- logger.debug(
1050
- f"Deserializing {len(tool_config)} tools for '{agent.name}'"
1051
- )
1052
- # Use get_callable to find each tool
1053
- for tool_name_or_path in tool_config:
1054
- try:
1055
- found_tool = registry.get_callable(tool_name_or_path)
1056
- if found_tool and callable(found_tool):
1057
- agent.tools.append(found_tool)
1058
- logger.debug(
1059
- f"Resolved and added tool '{tool_name_or_path}' for agent '{agent.name}'"
1060
- )
1061
- else:
1062
- # Should not happen if get_callable returns successfully but just in case
1063
- logger.warning(
1064
- f"Registry returned non-callable for tool '{tool_name_or_path}' for agent '{agent.name}'. Skipping."
1065
- )
1066
- except (
1067
- ValueError
1068
- ) as e: # get_callable raises ValueError if not found/ambiguous
1069
- logger.warning(
1070
- f"Could not resolve tool '{tool_name_or_path}' for agent '{agent.name}': {e}. Skipping."
1071
- )
1072
- except Exception as e:
1073
- logger.error(
1074
- f"Unexpected error resolving tool '{tool_name_or_path}' for agent '{agent.name}': {e}. Skipping.",
1075
- exc_info=True,
1076
- )
1077
-
1078
- # --- Deserialize Servers ---
1079
- agent.servers = [] # Initialize Servers list.
1080
- if servers_config:
1081
- logger.debug(
1082
- f"Deserializing {len(servers_config)} servers for '{agent.name}'"
1083
- )
1084
- # Agents keep track of server by getting a list of server names.
1085
- # The server instances will be retrieved during runtime from the registry. (default behavior)
1086
-
1087
- for server_name in servers_config:
1088
- if isinstance(server_name, str):
1089
- # Case 1 (default behavior): A server name is passe.
1090
- agent.servers.append(server_name)
1091
- elif isinstance(server_name, FlockMCPServerBase):
1092
- # Case 2 (highly unlikely): If someone somehow manages to pass
1093
- # an instance of a server during the deserialization step (however that might be achieved)
1094
- # check the registry, if the server is already registered, if not, register it
1095
- # and store the name in the servers list
1096
- FlockRegistry = get_registry()
1097
- server_exists = (
1098
- FlockRegistry.get_server(server_name.config.name)
1099
- is not None
1100
- )
1101
- if server_exists:
1102
- agent.servers.append(server_name.config.name)
1103
- else:
1104
- FlockRegistry.register_server(
1105
- server=server_name
1106
- ) # register it.
1107
- agent.servers.append(server_name.config.name)
1108
-
1109
- # --- Deserialize Callables ---
1110
- logger.debug(f"Deserializing callable fields for '{agent.name}'")
1111
- # available_callables = registry.get_all_callables() # Incorrect
1112
-
1113
- def resolve_and_assign(field_name: str, callable_key: str):
1114
- if callable_key in callable_configs:
1115
- callable_name = callable_configs[callable_key]
1116
- try:
1117
- # Use get_callable to find the signature function
1118
- found_callable = registry.get_callable(callable_name)
1119
- if found_callable and callable(found_callable):
1120
- setattr(agent, field_name, found_callable)
1121
- logger.debug(
1122
- f"Resolved callable '{callable_name}' for field '{field_name}' on agent '{agent.name}'"
1123
- )
1124
- else:
1125
- logger.warning(
1126
- f"Registry returned non-callable for name '{callable_name}' for field '{field_name}' on agent '{agent.name}'. Field remains default."
1127
- )
1128
- except (
1129
- ValueError
1130
- ) as e: # get_callable raises ValueError if not found/ambiguous
1131
- logger.warning(
1132
- f"Could not resolve callable '{callable_name}' in registry for field '{field_name}' on agent '{agent.name}': {e}. Field remains default."
1133
- )
1134
- except Exception as e:
1135
- logger.error(
1136
- f"Unexpected error resolving callable '{callable_name}' for field '{field_name}' on agent '{agent.name}': {e}. Field remains default.",
1137
- exc_info=True,
1138
- )
1139
- # Else: key not present, field retains its default value from __init__
1140
-
1141
- resolve_and_assign("description", "description_callable")
1142
- resolve_and_assign("input", "input_callable")
1143
- resolve_and_assign("output", "output_callable")
1144
-
1145
- logger.info(f"Successfully deserialized agent '{agent.name}'.")
1146
- return agent
287
+ """Save output to file if configured (delegated to serialization)."""
288
+ return self._serialization._save_output(agent_name, result)
1147
289
 
1148
290
  # --- Pydantic v2 Configuration ---
1149
- class Config:
1150
- arbitrary_types_allowed = (
1151
- True # Important for components like evaluator, router etc.
1152
- )
1153
- # Might need custom json_encoders if not using model_dump(mode='json') everywhere
1154
- # json_encoders = {
1155
- # FlockEvaluator: lambda v: v.to_dict() if v else None,
1156
- # FlockRouter: lambda v: v.to_dict() if v else None,
1157
- # FlockModule: lambda v: v.to_dict() if v else None,
1158
- # }
291
+ model_config = {"arbitrary_types_allowed": True}