flock-core 0.4.520__py3-none-any.whl → 0.5.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/cli/manage_agents.py +3 -3
- flock/components/__init__.py +28 -0
- flock/components/evaluation/__init__.py +9 -0
- flock/components/evaluation/declarative_evaluation_component.py +198 -0
- flock/components/routing/__init__.py +15 -0
- flock/{routers/conditional/conditional_router.py → components/routing/conditional_routing_component.py} +60 -49
- flock/components/routing/default_routing_component.py +103 -0
- flock/components/routing/llm_routing_component.py +208 -0
- flock/components/utility/__init__.py +15 -0
- flock/{modules/enterprise_memory/enterprise_memory_module.py → components/utility/memory_utility_component.py} +195 -173
- flock/{modules/performance/metrics_module.py → components/utility/metrics_utility_component.py} +101 -86
- flock/{modules/output/output_module.py → components/utility/output_utility_component.py} +49 -49
- flock/core/__init__.py +2 -8
- flock/core/agent/__init__.py +16 -0
- flock/core/agent/flock_agent_components.py +104 -0
- flock/core/agent/flock_agent_execution.py +101 -0
- flock/core/agent/flock_agent_integration.py +147 -0
- flock/core/agent/flock_agent_lifecycle.py +177 -0
- flock/core/agent/flock_agent_serialization.py +378 -0
- flock/core/component/__init__.py +15 -0
- flock/core/{flock_module.py → component/agent_component_base.py} +136 -35
- flock/core/component/evaluation_component_base.py +56 -0
- flock/core/component/routing_component_base.py +75 -0
- flock/core/component/utility_component_base.py +69 -0
- flock/core/config/flock_agent_config.py +49 -2
- flock/core/evaluation/utils.py +1 -1
- flock/core/execution/evaluation_executor.py +1 -1
- flock/core/flock.py +137 -483
- flock/core/flock_agent.py +151 -1018
- flock/core/flock_factory.py +94 -73
- flock/core/{flock_registry.py → flock_registry.py.backup} +3 -17
- flock/core/logging/logging.py +1 -0
- flock/core/mcp/flock_mcp_server.py +42 -37
- flock/core/mixin/dspy_integration.py +5 -5
- flock/core/orchestration/__init__.py +18 -0
- flock/core/orchestration/flock_batch_processor.py +94 -0
- flock/core/orchestration/flock_evaluator.py +113 -0
- flock/core/orchestration/flock_execution.py +288 -0
- flock/core/orchestration/flock_initialization.py +125 -0
- flock/core/orchestration/flock_server_manager.py +65 -0
- flock/core/orchestration/flock_web_server.py +117 -0
- flock/core/registry/__init__.py +39 -0
- flock/core/registry/agent_registry.py +69 -0
- flock/core/registry/callable_registry.py +139 -0
- flock/core/registry/component_discovery.py +142 -0
- flock/core/registry/component_registry.py +64 -0
- flock/core/registry/config_mapping.py +64 -0
- flock/core/registry/decorators.py +137 -0
- flock/core/registry/registry_hub.py +202 -0
- flock/core/registry/server_registry.py +57 -0
- flock/core/registry/type_registry.py +86 -0
- flock/core/serialization/flock_serializer.py +33 -30
- flock/core/serialization/serialization_utils.py +28 -25
- flock/core/util/input_resolver.py +29 -2
- flock/platform/docker_tools.py +3 -3
- flock/tools/markdown_tools.py +1 -2
- flock/tools/text_tools.py +1 -2
- flock/webapp/app/main.py +9 -5
- flock/workflow/activities.py +59 -84
- flock/workflow/activities_unified.py +230 -0
- flock/workflow/agent_execution_activity.py +6 -6
- flock/workflow/flock_workflow.py +1 -1
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/METADATA +2 -2
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/RECORD +67 -68
- flock/core/flock_evaluator.py +0 -60
- flock/core/flock_router.py +0 -83
- flock/evaluators/__init__.py +0 -1
- flock/evaluators/declarative/__init__.py +0 -1
- flock/evaluators/declarative/declarative_evaluator.py +0 -194
- flock/evaluators/memory/memory_evaluator.py +0 -90
- flock/evaluators/test/test_case_evaluator.py +0 -38
- flock/evaluators/zep/zep_evaluator.py +0 -59
- flock/modules/__init__.py +0 -1
- flock/modules/assertion/__init__.py +0 -1
- flock/modules/assertion/assertion_module.py +0 -286
- flock/modules/callback/__init__.py +0 -1
- flock/modules/callback/callback_module.py +0 -91
- flock/modules/enterprise_memory/README.md +0 -99
- flock/modules/mem0/__init__.py +0 -1
- flock/modules/mem0/mem0_module.py +0 -126
- flock/modules/mem0_async/__init__.py +0 -1
- flock/modules/mem0_async/async_mem0_module.py +0 -126
- flock/modules/memory/__init__.py +0 -1
- flock/modules/memory/memory_module.py +0 -429
- flock/modules/memory/memory_parser.py +0 -125
- flock/modules/memory/memory_storage.py +0 -736
- flock/modules/output/__init__.py +0 -1
- flock/modules/performance/__init__.py +0 -1
- flock/modules/zep/__init__.py +0 -1
- flock/modules/zep/zep_module.py +0 -192
- flock/routers/__init__.py +0 -1
- flock/routers/agent/__init__.py +0 -1
- flock/routers/agent/agent_router.py +0 -236
- flock/routers/agent/handoff_agent.py +0 -58
- flock/routers/default/__init__.py +0 -1
- flock/routers/default/default_router.py +0 -80
- flock/routers/feedback/feedback_router.py +0 -114
- flock/routers/list_generator/list_generator_router.py +0 -166
- flock/routers/llm/__init__.py +0 -1
- flock/routers/llm/llm_router.py +0 -365
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/WHEEL +0 -0
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py
CHANGED
|
@@ -1,50 +1,34 @@
|
|
|
1
1
|
# src/flock/core/flock_agent.py
|
|
2
|
-
"""FlockAgent
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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
|
-
|
|
96
|
-
|
|
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.
|
|
84
|
+
description="List of MCP Servers the agent can use to enhance its capabilities.",
|
|
103
85
|
)
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
# ---
|
|
115
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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,
|
|
169
|
-
output=output,
|
|
138
|
+
input=input,
|
|
139
|
+
output=output,
|
|
170
140
|
tools=tools,
|
|
171
141
|
servers=servers,
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
145
|
+
next_agent=next_agent,
|
|
181
146
|
)
|
|
182
147
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
|
222
|
-
"""
|
|
223
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
200
|
+
# --- LIFECYCLE DELEGATION ---
|
|
201
|
+
# Delegate lifecycle methods to the composition objects
|
|
400
202
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
229
|
+
# --- EXECUTION METHODS ---
|
|
230
|
+
# Delegate to the execution system
|
|
425
231
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
465
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|