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
flock/core/flock_agent.py CHANGED
@@ -1,70 +1,54 @@
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 import (
16
+ EvaluationComponent,
17
+ )
18
+ from flock.core.component.routing_component import RoutingComponent
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 FlockMCPServer
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
- SignatureType = (
50
- str
51
- | Callable[..., str]
52
- | type[BaseModel]
53
- | Callable[..., type[BaseModel]]
54
- | None
55
- )
56
33
 
34
+ DynamicStr = str | Callable[[FlockContext], str]
57
35
 
58
- # Make FlockAgent inherit from Serializable
59
36
  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.
37
+ """Unified FlockAgent using the new component architecture.
38
+
39
+ This is the next-generation FlockAgent that uses a single components list
40
+ instead of separate evaluator, router, and modules. All agent functionality
41
+ is now provided through AgentComponent instances.
42
+
43
+ Key changes:
44
+ - components: list[AgentComponent] - unified component list
45
+ - next_agent: str | None - explicit workflow state
46
+ - evaluator/router properties - convenience access to primary components
63
47
  """
64
48
 
65
49
  agent_id: str = Field(
66
50
  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.",
51
+ description="Internal, Unique UUID4 for this agent instance.",
68
52
  )
69
53
 
70
54
  name: str = Field(..., description="Unique identifier for the agent.")
@@ -73,73 +57,61 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
73
57
  None,
74
58
  description="The model identifier to use (e.g., 'openai/gpt-4o'). If None, uses Flock's default.",
75
59
  )
76
- description: str | Callable[..., str] | None = Field(
77
- "",
60
+ description_spec: DynamicStr | None = Field(
61
+ default="",
62
+ alias="description",
63
+ validation_alias="description",
78
64
  description="A human-readable description or a callable returning one.",
79
65
  )
80
- input: SignatureType = Field(
81
- 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
- ),
66
+ input_spec: DynamicStr | None = Field(
67
+ default="",
68
+ alias="input",
69
+ validation_alias="input",
70
+ description="Signature for input keys. Supports type hints (:) and descriptions (|).",
86
71
  )
87
- output: SignatureType = Field(
88
- 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
- ),
72
+ output_spec: DynamicStr | None = Field(
73
+ default="",
74
+ alias="output",
75
+ validation_alias="output",
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
- servers: list[str | FlockMCPServerBase] | None = Field(
82
+ servers: list[str | FlockMCPServer] | 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_spec: DynamicStr | 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
+ alias="next_agent",
97
+ validation_alias="next_agent",
98
+ description="Next agent in workflow - set by user or routing components.",
126
99
  )
127
100
 
128
101
  config: FlockAgentConfig = Field(
129
102
  default_factory=lambda: FlockAgentConfig(),
130
- description="Configuration for this agent, holding various settings and parameters.",
103
+ description="Configuration for this agent.",
131
104
  )
132
105
 
133
- # --- Temporal Configuration (Optional) ---
134
106
  temporal_activity_config: TemporalActivityConfig | None = Field(
135
107
  default=None,
136
- description="Optional Temporal settings specific to this agent's activity execution.",
108
+ description="Optional Temporal settings specific to this agent.",
137
109
  )
138
110
 
139
111
  # --- Runtime State (Excluded from Serialization) ---
140
112
  context: FlockContext | None = Field(
141
113
  default=None,
142
- exclude=True, # Exclude context from model_dump and serialization
114
+ exclude=True,
143
115
  description="Runtime context associated with the flock execution.",
144
116
  )
145
117
 
@@ -147,349 +119,143 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
147
119
  self,
148
120
  name: str,
149
121
  model: str | None = None,
150
- description: str | Callable[..., str] | None = "",
151
- input: SignatureType = None,
152
- output: SignatureType = None,
122
+ description: DynamicStr | None = None,
123
+ input: DynamicStr | None = None,
124
+ output: DynamicStr | None = None,
153
125
  tools: list[Callable[..., Any]] | None = None,
154
- 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
+ servers: list[str | FlockMCPServer] | None = None,
127
+ components: list[AgentComponent] | None = None,
128
+ config: FlockAgentConfig | None = None,
129
+ next_agent: DynamicStr | None = None,
161
130
  temporal_activity_config: TemporalActivityConfig | None = None,
162
- **kwargs,
163
131
  ):
132
+ """Initialize the unified FlockAgent with components and configuration."""
133
+ if config is None:
134
+ config = FlockAgentConfig()
164
135
  super().__init__(
165
136
  name=name,
166
137
  model=model,
167
138
  description=description,
168
- input=input, # Store the raw input spec
169
- output=output, # Store the raw output spec
139
+ input=input,
140
+ output=output,
170
141
  tools=tools,
171
142
  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
143
+ components=components if components is not None else [],
144
+ config=config,
179
145
  temporal_activity_config=temporal_activity_config,
180
- **kwargs,
146
+ next_agent=next_agent,
181
147
  )
182
148
 
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
149
+ # Initialize helper systems (reuse existing logic)
150
+ self._execution = FlockAgentExecution(self)
151
+ self._integration = FlockAgentIntegration(self)
152
+ self._serialization = FlockAgentSerialization(self)
153
+ # Lifecycle will be lazy-loaded when needed
187
154
 
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}'")
199
-
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]
155
+ # --- CONVENIENCE PROPERTIES ---
156
+ # These provide familiar access patterns while using the unified model
219
157
 
220
158
  @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...")
159
+ def evaluator(self) -> EvaluationComponent | None:
160
+ """Get the primary evaluation component for this agent."""
161
+ return self._components.get_primary_evaluator()
302
162
 
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)
163
+ @property
164
+ def router(self) -> RoutingComponent | None:
165
+ """Get the primary routing component for this agent."""
166
+ return self._components.get_primary_router()
310
167
 
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)
168
+ @property
169
+ def modules(self) -> list[AgentComponent]:
170
+ """Get all components (for backward compatibility with module-style access)."""
171
+ return self.components.copy()
327
172
 
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,
173
+ @property
174
+ def _components(self):
175
+ """Get the component management helper."""
176
+ if not hasattr(self, '_components_helper'):
177
+ from flock.core.agent.flock_agent_components import (
178
+ FlockAgentComponents,
340
179
  )
180
+ self._components_helper = FlockAgentComponents(self)
181
+ return self._components_helper
341
182
 
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
- )
183
+ # Component management delegated to _components
184
+ def add_component(self, component: AgentComponent) -> None:
185
+ """Add a component to this agent."""
186
+ self._components.add_component(component)
350
187
 
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
188
+ def remove_component(self, component_name: str) -> None:
189
+ """Remove a component from this agent."""
190
+ self._components.remove_component(component_name)
359
191
 
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
192
+ def get_component(self, component_name: str) -> AgentComponent | None:
193
+ """Get a component by name."""
194
+ return self._components.get_component(component_name)
364
195
 
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
196
 
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")
197
+ def get_enabled_components(self) -> list[AgentComponent]:
198
+ """Get enabled components (backward compatibility)."""
199
+ return self._components.get_enabled_components()
397
200
 
398
- # If a MiddlewarePipeline is registered in DI, wrap the evaluator
399
- result: dict[str, Any] | None = None
201
+ # --- LIFECYCLE DELEGATION ---
202
+ # Delegate lifecycle methods to the composition objects
400
203
 
401
- if container is not None:
402
- try:
403
- from wd.di.middleware import (
404
- MiddlewarePipeline,
405
- )
204
+ @property
205
+ def _lifecycle(self):
206
+ """Get the lifecycle management helper (lazy-loaded)."""
207
+ if not hasattr(self, '_lifecycle_helper'):
208
+ from flock.core.agent.flock_agent_lifecycle import (
209
+ FlockAgentLifecycle,
210
+ )
211
+ self._lifecycle_helper = FlockAgentLifecycle(self)
212
+ return self._lifecycle_helper
406
213
 
407
- pipeline: MiddlewarePipeline | None = None
408
- try:
409
- pipeline = container.get_service(MiddlewarePipeline)
410
- except Exception:
411
- pipeline = None
214
+ async def initialize(self, inputs: dict[str, Any]) -> None:
215
+ """Initialize agent and run component initializers."""
216
+ return await self._lifecycle.initialize(inputs)
412
217
 
413
- if pipeline is not None:
414
- # Build execution chain where the evaluator is the terminal handler
218
+ async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
219
+ """Core evaluation logic using unified component system."""
220
+ return await self._lifecycle.evaluate(inputs)
415
221
 
416
- async def _final_handler():
417
- return await self.evaluator.evaluate(
418
- self, current_inputs, registered_tools
419
- )
222
+ async def terminate(self, inputs: dict[str, Any], result: dict[str, Any]) -> None:
223
+ """Terminate agent and run component terminators."""
224
+ return await self._lifecycle.terminate(inputs, result)
420
225
 
421
- idx = 0
226
+ async def on_error(self, error: Exception, inputs: dict[str, Any]) -> None:
227
+ """Handle errors and run component error handlers."""
228
+ return await self._lifecycle.on_error(error, inputs)
422
229
 
423
- async def _invoke_next():
424
- nonlocal idx
230
+ # --- EXECUTION METHODS ---
231
+ # Delegate to the execution system
425
232
 
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()
233
+ def run(self, inputs: dict[str, Any]) -> dict[str, Any]:
234
+ """Synchronous wrapper for run_async."""
235
+ return self._execution.run(inputs)
431
236
 
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
237
+ async def run_async(self, inputs: dict[str, Any]) -> dict[str, Any]:
238
+ """Asynchronous execution logic with unified lifecycle."""
239
+ return await self._execution.run_async(inputs)
463
240
 
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
241
+ # --- SERIALIZATION ---
242
+ # Delegate to the serialization system
476
243
 
477
- logger.debug(f"Evaluation completed for agent '{self.name}'")
478
- return current_result
244
+ def to_dict(self) -> dict[str, Any]:
245
+ """Convert to dictionary using unified component serialization."""
246
+ return self._serialization.to_dict()
479
247
 
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))
248
+ @classmethod
249
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
250
+ """Deserialize from dictionary using unified component deserialization."""
251
+ return FlockAgentSerialization.from_dict(cls, data)
490
252
 
491
253
  def set_model(self, model: str):
492
- """Set the model for the agent and its evaluator."""
254
+ """Set the model for the agent and its evaluator.
255
+
256
+ This method updates both the agent's model property and propagates
257
+ the model to the evaluator component if it has a config with a model field.
258
+ """
493
259
  self.model = model
494
260
  if self.evaluator and hasattr(self.evaluator, "config"):
495
261
  self.evaluator.config.model = model
@@ -505,654 +271,51 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
505
271
  f"Agent '{self.name}' has no evaluator to set model for."
506
272
  )
507
273
 
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)
274
+ @property
275
+ def description(self) -> str | None:
276
+ """Returns the resolved agent description."""
277
+ return self._integration.resolve_description(self.context)
645
278
 
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
- )
279
+ @property
280
+ def input(self) -> str | None:
281
+ """Returns the resolved agent input."""
282
+ return self._integration.resolve_input(self.context)
650
283
 
651
- # --- 3. Instantiate the Component (Same as before) ---
652
- try:
653
- init_args = {"config": config_instance, "name": instance_name}
284
+ @property
285
+ def output(self) -> str | None:
286
+ """Returns the resolved agent output."""
287
+ return self._integration.resolve_output(self.context)
654
288
 
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
289
+ @property
290
+ def next_agent(self) -> str | None:
291
+ """Returns the resolved agent next agent."""
292
+ return self._integration.resolve_next_agent(self.context)
662
293
 
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
- )
294
+ @description.setter
295
+ def description(self, value: DynamicStr) -> None:
296
+ self.description_spec = value
676
297
 
677
- return self
298
+ @input.setter
299
+ def input(self, value: DynamicStr) -> None:
300
+ self.input_spec = value
678
301
 
679
- # resolve_callables remains useful for dynamic definitions
680
- def resolve_callables(self, context: FlockContext | None = None) -> None:
681
- """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)
302
+ @output.setter
303
+ def output(self, value: DynamicStr) -> None:
304
+ self.output_spec = value
690
305
 
691
- # --- Serialization Implementation ---
306
+ @next_agent.setter
307
+ def next_agent(self, value: DynamicStr) -> None:
308
+ self.next_agent_spec = value
692
309
 
693
310
  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
311
+ """Save output to file if configured (delegated to serialization)."""
312
+ return self._serialization._save_output(agent_name, result)
1147
313
 
1148
314
  # --- 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
- # }
315
+ model_config = {
316
+ "arbitrary_types_allowed": True,
317
+ "populate_by_name": True,
318
+ # "json_encoders": {
319
+ # Callable: lambda f: f"{f.__module__}.{f.__qualname__}",
320
+ # },
321
+ }