flock-core 0.5.0b2__py3-none-any.whl → 0.5.0b3__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 (45) hide show
  1. flock/cli/manage_agents.py +3 -3
  2. flock/components/evaluation/declarative_evaluation_component.py +10 -10
  3. flock/components/routing/conditional_routing_component.py +7 -6
  4. flock/components/routing/default_routing_component.py +3 -3
  5. flock/components/routing/llm_routing_component.py +24 -26
  6. flock/components/utility/memory_utility_component.py +3 -3
  7. flock/components/utility/metrics_utility_component.py +11 -11
  8. flock/components/utility/output_utility_component.py +11 -9
  9. flock/core/__init__.py +24 -10
  10. flock/core/agent/flock_agent_components.py +16 -16
  11. flock/core/agent/flock_agent_integration.py +88 -29
  12. flock/core/agent/flock_agent_serialization.py +23 -20
  13. flock/core/api/endpoints.py +1 -1
  14. flock/core/component/__init__.py +7 -7
  15. flock/core/component/{evaluation_component_base.py → evaluation_component.py} +2 -2
  16. flock/core/component/{routing_component_base.py → routing_component.py} +3 -4
  17. flock/core/component/{utility_component_base.py → utility_component.py} +3 -3
  18. flock/core/flock.py +7 -7
  19. flock/core/flock_agent.py +68 -38
  20. flock/core/flock_factory.py +21 -18
  21. flock/core/flock_server_manager.py +8 -8
  22. flock/core/mcp/flock_mcp_server.py +11 -11
  23. flock/core/mcp/{flock_mcp_tool_base.py → flock_mcp_tool.py} +2 -2
  24. flock/core/mcp/mcp_client.py +9 -9
  25. flock/core/mcp/mcp_client_manager.py +9 -9
  26. flock/core/mcp/mcp_config.py +24 -24
  27. flock/core/orchestration/flock_execution.py +3 -3
  28. flock/core/orchestration/flock_initialization.py +6 -6
  29. flock/core/orchestration/flock_server_manager.py +8 -6
  30. flock/core/registry/__init__.py +16 -10
  31. flock/core/registry/registry_hub.py +7 -4
  32. flock/core/registry/server_registry.py +6 -6
  33. flock/core/serialization/flock_serializer.py +3 -2
  34. flock/mcp/servers/sse/flock_sse_server.py +10 -10
  35. flock/mcp/servers/stdio/flock_stdio_server.py +10 -10
  36. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +10 -10
  37. flock/mcp/servers/websockets/flock_websocket_server.py +10 -10
  38. flock/workflow/activities.py +10 -10
  39. {flock_core-0.5.0b2.dist-info → flock_core-0.5.0b3.dist-info}/METADATA +1 -1
  40. {flock_core-0.5.0b2.dist-info → flock_core-0.5.0b3.dist-info}/RECORD +43 -45
  41. flock/core/flock_registry.py.backup +0 -688
  42. flock/workflow/activities_unified.py +0 -230
  43. {flock_core-0.5.0b2.dist-info → flock_core-0.5.0b3.dist-info}/WHEEL +0 -0
  44. {flock_core-0.5.0b2.dist-info → flock_core-0.5.0b3.dist-info}/entry_points.txt +0 -0
  45. {flock_core-0.5.0b2.dist-info → flock_core-0.5.0b3.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,80 @@
1
1
  # src/flock/core/agent/flock_agent_integration.py
2
2
  """Tool and server integration functionality for FlockAgent."""
3
3
 
4
- from typing import TYPE_CHECKING, Any
4
+ from collections.abc import Callable
5
+ from functools import wraps
6
+ from inspect import Parameter, signature
7
+ from typing import TYPE_CHECKING, Any, TypeVar, cast
5
8
 
6
9
  from flock.core.context.context import FlockContext
7
10
  from flock.core.logging.logging import get_logger
8
- from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
11
+ from flock.core.mcp.flock_mcp_server import FlockMCPServer
9
12
 
10
13
  if TYPE_CHECKING:
11
14
  from flock.core.flock_agent import FlockAgent
12
15
 
13
16
  logger = get_logger("agent.integration")
14
17
 
18
+ R = TypeVar("R", bound=str)
19
+
20
+
21
+ def adapt(prop_name: str, fn: Callable[..., R]) -> Callable[[FlockContext], R]:
22
+ """Coerce *fn* into the canonical ``(ctx: FlockContext) -> str`` form.
23
+
24
+ Acceptable signatures
25
+ ---------------------
26
+ 1. ``() -> str`` (no parameters)
27
+ 2. ``(ctx: FlockContext) -> str`` (exactly one positional parameter)
28
+
29
+ Anything else raises ``TypeError``.
30
+
31
+ The wrapper also enforces at runtime that the result is ``str``.
32
+ """
33
+ if not callable(fn):
34
+ raise TypeError(f"{prop_name} must be a callable, got {type(fn).__name__}")
35
+
36
+ sig = signature(fn)
37
+ params = list(sig.parameters.values())
38
+
39
+ def _validate_result(res: object) -> R:
40
+ if not isinstance(res, str):
41
+ raise TypeError(
42
+ f"{prop_name} callable must return str, got {type(res).__name__}"
43
+ )
44
+ return cast(R, res)
45
+
46
+ # ── Case 1: () -> str ────────────────────────────────────────────────────
47
+ if len(params) == 0:
48
+
49
+ @wraps(fn)
50
+ def _wrapped(ctx: FlockContext) -> R:
51
+ return _validate_result(fn())
52
+
53
+ return _wrapped
54
+
55
+ # ── Case 2: (ctx) -> str ────────────────────────────────────────────────
56
+ if len(params) == 1:
57
+ p: Parameter = params[0]
58
+ valid_kind = p.kind in (
59
+ Parameter.POSITIONAL_ONLY,
60
+ Parameter.POSITIONAL_OR_KEYWORD,
61
+ )
62
+ valid_annotation = p.annotation in (Parameter.empty, FlockContext)
63
+ has_no_default = p.default is Parameter.empty
64
+
65
+ if valid_kind and valid_annotation and has_no_default:
66
+
67
+ @wraps(fn)
68
+ def _wrapped(ctx: FlockContext) -> R:
69
+ return _validate_result(fn(ctx)) # type: ignore[arg-type]
70
+
71
+ return _wrapped
72
+
73
+ # ── Anything else: reject ───────────────────────────────────────────────
74
+ raise TypeError(
75
+ f"{prop_name} callable must be () -> str or (ctx: FlockContext) -> str; "
76
+ f"got signature {sig}"
77
+ )
15
78
 
16
79
  class FlockAgentIntegration:
17
80
  """Handles tool and server integration for FlockAgent including MCP servers and callable tools."""
@@ -19,30 +82,26 @@ class FlockAgentIntegration:
19
82
  def __init__(self, agent: "FlockAgent"):
20
83
  self.agent = agent
21
84
 
22
- def resolve_callables(self, context: FlockContext | None = None) -> None:
23
- """Resolves callable fields (description, input, output) using context."""
24
- if callable(self.agent.description):
25
- self.agent.description = self.agent.description(
26
- context
27
- ) # Pass context if needed by callable
28
- if callable(self.agent.input):
29
- self.agent.input = self.agent.input(context)
30
- if callable(self.agent.output):
31
- self.agent.output = self.agent.output(context)
32
-
33
- def resolve_description(self, context: FlockContext | None = None) -> str:
34
- if callable(self.agent.description):
35
- try:
36
- # Attempt to call without context first.
37
- return self.agent.description(context)
38
- except Exception as e:
39
- logger.error(
40
- f"Error resolving callable description for agent '{self.agent.name}': {e}"
41
- )
42
- return None
43
- elif isinstance(self.agent.description, str):
44
- return self.agent.description
45
- return None
85
+ def _resolve(self, raw: str | Callable[..., str], name: str, ctx: FlockContext | None) -> str | None:
86
+ if callable(raw):
87
+ raw = adapt(name, raw)(ctx or FlockContext())
88
+ return raw
89
+
90
+ def resolve_description(self, context: FlockContext | None = None) -> str | None:
91
+ """Resolve the agent's description, handling callable descriptions."""
92
+ return self._resolve(self.agent.description_spec, "description", context)
93
+
94
+ def resolve_input(self, context: FlockContext | None = None) -> str | None:
95
+ """Resolve the agent's input, handling callable inputs."""
96
+ return self._resolve(self.agent.input_spec, "input", context)
97
+
98
+ def resolve_output(self, context: FlockContext | None = None) -> str | None:
99
+ """Resolve the agent's output, handling callable outputs."""
100
+ return self._resolve(self.agent.output_spec, "output", context)
101
+
102
+ def resolve_next_agent(self, context: FlockContext | None = None) -> str | None:
103
+ """Resolve the next agent, handling callable next agents."""
104
+ return self._resolve(self.agent.next_agent_spec, "next_agent", context)
46
105
 
47
106
  async def get_mcp_tools(self) -> list[Any]:
48
107
  """Get tools from registered MCP servers."""
@@ -52,9 +111,9 @@ class FlockAgentIntegration:
52
111
 
53
112
  registry = get_registry() # Get the registry
54
113
  for server in self.agent.servers:
55
- registered_server: FlockMCPServerBase | None = None
114
+ registered_server: FlockMCPServer | None = None
56
115
  server_tools = []
57
- if isinstance(server, FlockMCPServerBase):
116
+ if isinstance(server, FlockMCPServer):
58
117
  # check if registered
59
118
  server_name = server.config.name
60
119
  registered_server = registry.get_server(
@@ -72,7 +131,7 @@ class FlockAgentIntegration:
72
131
  )
73
132
  else:
74
133
  logger.warning(
75
- f"No Server with name '{server.config.name if isinstance(server, FlockMCPServerBase) else server}' registered! Skipping."
134
+ f"No Server with name '{server.config.name if isinstance(server, FlockMCPServer) else server}' registered! Skipping."
76
135
  )
77
136
  mcp_tools = mcp_tools + server_tools
78
137
  return mcp_tools
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, TypeVar
8
8
 
9
9
  # Legacy component imports removed
10
10
  from flock.core.logging.logging import get_logger
11
- from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
11
+ from flock.core.mcp.flock_mcp_server import FlockMCPServer
12
12
  from flock.core.serialization.json_encoder import FlockJSONEncoder
13
13
  from flock.core.serialization.serialization_utils import (
14
14
  deserialize_component,
@@ -68,20 +68,20 @@ class FlockAgentSerialization:
68
68
  is_output_callable = False
69
69
  is_next_agent_callable = False
70
70
  # if self.agent.description is a callable, exclude it
71
- if callable(self.agent.description):
71
+ if callable(self.agent.description_spec):
72
72
  is_description_callable = True
73
- exclude.append("description")
73
+ exclude.append("description_spec")
74
74
  # if self.agent.input is a callable, exclude it
75
- if callable(self.agent.input):
75
+ if callable(self.agent.input_spec):
76
76
  is_input_callable = True
77
- exclude.append("input")
77
+ exclude.append("input_spec")
78
78
  # if self.agent.output is a callable, exclude it
79
- if callable(self.agent.output):
79
+ if callable(self.agent.output_spec):
80
80
  is_output_callable = True
81
- exclude.append("output")
82
- if callable(self.agent.next_agent):
81
+ exclude.append("output_spec")
82
+ if callable(self.agent.next_agent_spec):
83
83
  is_next_agent_callable = True
84
- exclude.append("next_agent")
84
+ exclude.append("next_agent_spec")
85
85
 
86
86
  logger.debug(f"Serializing agent '{self.agent.name}' to dict.")
87
87
  # Use Pydantic's dump, exclude manually handled fields and runtime context
@@ -122,7 +122,7 @@ class FlockAgentSerialization:
122
122
  )
123
123
  serialized_servers = []
124
124
  for server in self.agent.servers:
125
- if isinstance(server, FlockMCPServerBase):
125
+ if isinstance(server, FlockMCPServer):
126
126
  serialized_servers.append(server.config.name)
127
127
  else:
128
128
  # Write it down as a list of server names.
@@ -167,7 +167,7 @@ class FlockAgentSerialization:
167
167
  )
168
168
 
169
169
  if is_description_callable:
170
- path_str = registry.get_callable_path_string(self.agent.description)
170
+ path_str = registry.get_callable_path_string(self.agent.description_spec)
171
171
  if path_str:
172
172
  func_name = path_str.split(".")[-1]
173
173
  data["description_callable"] = func_name
@@ -176,11 +176,11 @@ class FlockAgentSerialization:
176
176
  )
177
177
  else:
178
178
  logger.warning(
179
- f"Could not get path string for description {self.agent.description} in agent '{self.agent.name}'. Skipping."
179
+ f"Could not get path string for description {self.agent.description_spec} in agent '{self.agent.name}'. Skipping."
180
180
  )
181
181
 
182
182
  if is_input_callable:
183
- path_str = registry.get_callable_path_string(self.agent.input)
183
+ path_str = registry.get_callable_path_string(self.agent.input_spec)
184
184
  if path_str:
185
185
  func_name = path_str.split(".")[-1]
186
186
  data["input_callable"] = func_name
@@ -189,11 +189,11 @@ class FlockAgentSerialization:
189
189
  )
190
190
  else:
191
191
  logger.warning(
192
- f"Could not get path string for input {self.agent.input} in agent '{self.agent.name}'. Skipping."
192
+ f"Could not get path string for input {self.agent.input_spec} in agent '{self.agent.name}'. Skipping."
193
193
  )
194
194
 
195
195
  if is_output_callable:
196
- path_str = registry.get_callable_path_string(self.agent.output)
196
+ path_str = registry.get_callable_path_string(self.agent.output_spec)
197
197
  if path_str:
198
198
  func_name = path_str.split(".")[-1]
199
199
  data["output_callable"] = func_name
@@ -202,20 +202,20 @@ class FlockAgentSerialization:
202
202
  )
203
203
  else:
204
204
  logger.warning(
205
- f"Could not get path string for output {self.agent.output} in agent '{self.agent.name}'. Skipping."
205
+ f"Could not get path string for output {self.agent.output_spec} in agent '{self.agent.name}'. Skipping."
206
206
  )
207
207
 
208
208
  if is_next_agent_callable:
209
- path_str = registry.get_callable_path_string(self.agent.next_agent)
209
+ path_str = registry.get_callable_path_string(self.agent.next_agent_spec)
210
210
  if path_str:
211
211
  func_name = path_str.split(".")[-1]
212
- data["next_agent"] = func_name
212
+ data["next_agent_callable"] = func_name
213
213
  logger.debug(
214
214
  f"Added next_agent '{func_name}' (from path '{path_str}') to agent '{self.agent.name}'"
215
215
  )
216
216
  else:
217
217
  logger.warning(
218
- f"Could not get path string for next_agent {self.agent.next_agent} in agent '{self.agent.name}'. Skipping."
218
+ f"Could not get path string for next_agent {self.agent.next_agent_spec} in agent '{self.agent.name}'. Skipping."
219
219
  )
220
220
 
221
221
 
@@ -245,6 +245,7 @@ class FlockAgentSerialization:
245
245
  "description_callable",
246
246
  "input_callable",
247
247
  "output_callable",
248
+ "next_agent_callable",
248
249
  ]
249
250
 
250
251
  for key in callable_keys:
@@ -322,7 +323,7 @@ class FlockAgentSerialization:
322
323
  if isinstance(server_name, str):
323
324
  # Case 1 (default behavior): A server name is passed.
324
325
  agent.servers.append(server_name)
325
- elif isinstance(server_name, FlockMCPServerBase):
326
+ elif isinstance(server_name, FlockMCPServer):
326
327
  # Case 2 (highly unlikely): If someone somehow manages to pass
327
328
  # an instance of a server during the deserialization step (however that might be achieved)
328
329
  # check the registry, if the server is already registered, if not, register it
@@ -373,6 +374,8 @@ class FlockAgentSerialization:
373
374
  resolve_and_assign("description", "description_callable")
374
375
  resolve_and_assign("input", "input_callable")
375
376
  resolve_and_assign("output", "output_callable")
377
+ resolve_and_assign("next_agent", "next_agent_callable")
378
+ # --- Finalize ---
376
379
 
377
380
  logger.info(f"Successfully deserialized agent '{agent.name}'.")
378
381
  return agent
@@ -246,7 +246,7 @@ def create_api_router() -> APIRouter:
246
246
  """List all available agents in the currently loaded Flock."""
247
247
  logger.debug("API request: list agents")
248
248
  agents_list = [
249
- {"name": agent.name, "description": agent.resolved_description or agent.name}
249
+ agent.to_dict()
250
250
  for agent in flock_instance.agents.values()
251
251
  ]
252
252
  return {"agents": agents_list}
@@ -2,14 +2,14 @@
2
2
  """Unified component system for Flock agents."""
3
3
 
4
4
  from .agent_component_base import AgentComponent, AgentComponentConfig
5
- from .evaluation_component_base import EvaluationComponentBase
6
- from .routing_component_base import RoutingComponentBase
7
- from .utility_component_base import UtilityComponentBase
5
+ from .evaluation_component import EvaluationComponent
6
+ from .routing_component import RoutingComponent
7
+ from .utility_component import UtilityComponent
8
8
 
9
9
  __all__ = [
10
10
  "AgentComponent",
11
- "AgentComponentConfig",
12
- "EvaluationComponentBase",
13
- "RoutingComponentBase",
14
- "UtilityComponentBase",
11
+ "AgentComponentConfig",
12
+ "EvaluationComponent",
13
+ "RoutingComponent",
14
+ "UtilityComponent",
15
15
  ]
@@ -9,7 +9,7 @@ from flock.core.context.context import FlockContext
9
9
  from .agent_component_base import AgentComponent
10
10
 
11
11
 
12
- class EvaluationComponentBase(AgentComponent):
12
+ class EvaluationComponent(AgentComponent):
13
13
  """Base class for evaluation components.
14
14
 
15
15
  Evaluation components implement the core intelligence/logic of an agent.
@@ -23,7 +23,7 @@ class EvaluationComponentBase(AgentComponent):
23
23
  - ScriptEvaluationComponent (Python script-based)
24
24
  - LLMEvaluationComponent (direct LLM API)
25
25
  """
26
-
26
+
27
27
  @abstractmethod
28
28
  async def evaluate_core(
29
29
  self,
@@ -6,11 +6,10 @@ from typing import Any
6
6
 
7
7
  from flock.core.context.context import FlockContext
8
8
 
9
-
10
9
  from .agent_component_base import AgentComponent
11
10
 
12
11
 
13
- class RoutingComponentBase(AgentComponent):
12
+ class RoutingComponent(AgentComponent):
14
13
  """Base class for routing components.
15
14
 
16
15
  Routing components determine the next step in a workflow based on the
@@ -26,7 +25,7 @@ class RoutingComponentBase(AgentComponent):
26
25
  - DefaultRoutingModule (simple next-agent routing)
27
26
  - ListGeneratorRoutingModule (dynamic agent creation)
28
27
  """
29
-
28
+
30
29
  @abstractmethod
31
30
  async def determine_next_step(
32
31
  self,
@@ -54,7 +53,7 @@ class RoutingComponentBase(AgentComponent):
54
53
  raise NotImplementedError(
55
54
  f"{self.__class__.__name__} must implement determine_next_step()"
56
55
  )
57
-
56
+
58
57
  async def evaluate_core(
59
58
  self,
60
59
  agent: Any,
@@ -4,12 +4,12 @@
4
4
  from typing import Any
5
5
 
6
6
  from flock.core.context.context import FlockContext
7
- # HandOffRequest removed - using agent.next_agent directly
8
7
 
8
+ # HandOffRequest removed - using agent.next_agent directly
9
9
  from .agent_component_base import AgentComponent
10
10
 
11
11
 
12
- class UtilityComponentBase(AgentComponent):
12
+ class UtilityComponent(AgentComponent):
13
13
  """Base class for utility/enhancement components.
14
14
 
15
15
  Utility components add cross-cutting concerns to agents without being
@@ -32,7 +32,7 @@ class UtilityComponentBase(AgentComponent):
32
32
  - MetricsUtilityModule (performance tracking)
33
33
  - AssertionUtilityModule (result validation)
34
34
  """
35
-
35
+
36
36
  async def evaluate_core(
37
37
  self,
38
38
  agent: Any,
flock/core/flock.py CHANGED
@@ -17,7 +17,7 @@ from typing import (
17
17
  from box import Box
18
18
  from temporalio import workflow
19
19
 
20
- from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
20
+ from flock.core.mcp.flock_mcp_server import FlockMCPServer
21
21
 
22
22
  with workflow.unsafe.imports_passed_through():
23
23
  from datasets import Dataset # type: ignore
@@ -125,7 +125,7 @@ class Flock(BaseModel, Serializable):
125
125
  _start_input: dict = {} # For potential pre-configuration
126
126
 
127
127
  # Internal server storage - not part of the Pydantic model for direct serialization
128
- _servers: dict[str, FlockMCPServerBase]
128
+ _servers: dict[str, FlockMCPServer]
129
129
 
130
130
  # Note: _mgr is now handled by the server manager helper
131
131
 
@@ -207,7 +207,7 @@ class Flock(BaseModel, Serializable):
207
207
  enable_temporal: bool = False,
208
208
  enable_opik: bool = False,
209
209
  agents: list[FlockAgent] | None = None,
210
- servers: list[FlockMCPServerBase] | None = None,
210
+ servers: list[FlockMCPServer] | None = None,
211
211
  temporal_config: TemporalWorkflowConfig | None = None,
212
212
  temporal_start_in_process_worker: bool = True,
213
213
  **kwargs,
@@ -296,7 +296,7 @@ class Flock(BaseModel, Serializable):
296
296
 
297
297
 
298
298
 
299
- def add_server(self, server: FlockMCPServerBase) -> FlockMCPServerBase:
299
+ def add_server(self, server: FlockMCPServer) -> FlockMCPServer:
300
300
  """Adds a server instance to this Flock configuration and registry."""
301
301
  return self._server_manager.add_server(server)
302
302
 
@@ -349,7 +349,7 @@ class Flock(BaseModel, Serializable):
349
349
  return self._agents
350
350
 
351
351
  @property
352
- def servers(self) -> dict[str, FlockMCPServerBase]:
352
+ def servers(self) -> dict[str, FlockMCPServer]:
353
353
  """Returns the dictionary of servers managed by this Flock instance."""
354
354
  return self._server_manager.servers
355
355
 
@@ -361,7 +361,7 @@ class Flock(BaseModel, Serializable):
361
361
  run_id: str = "",
362
362
  box_result: bool = True,
363
363
  agents: list[FlockAgent] | None = None,
364
- servers: list[FlockMCPServerBase] | None = None,
364
+ servers: list[FlockMCPServer] | None = None,
365
365
  memo: dict[str, Any] | None = None,
366
366
  ) -> Box | dict:
367
367
  """Synchronous execution wrapper."""
@@ -384,7 +384,7 @@ class Flock(BaseModel, Serializable):
384
384
  run_id: str = "",
385
385
  box_result: bool = True,
386
386
  agents: list[FlockAgent] | None = None,
387
- servers: list[FlockMCPServerBase] | None = None,
387
+ servers: list[FlockMCPServer] | None = None,
388
388
  memo: dict[str, Any] | None = None,
389
389
  ) -> Box | dict:
390
390
  """Entry point for running an agent system asynchronously."""
flock/core/flock_agent.py CHANGED
@@ -12,14 +12,14 @@ from flock.core.agent.flock_agent_execution import FlockAgentExecution
12
12
  from flock.core.agent.flock_agent_integration import FlockAgentIntegration
13
13
  from flock.core.agent.flock_agent_serialization import FlockAgentSerialization
14
14
  from flock.core.component.agent_component_base import AgentComponent
15
- from flock.core.component.evaluation_component_base import (
16
- EvaluationComponentBase,
15
+ from flock.core.component.evaluation_component import (
16
+ EvaluationComponent,
17
17
  )
18
- from flock.core.component.routing_component_base import RoutingComponentBase
18
+ from flock.core.component.routing_component import RoutingComponent
19
19
  from flock.core.config.flock_agent_config import FlockAgentConfig
20
20
  from flock.core.context.context import FlockContext
21
21
  from flock.core.logging.logging import get_logger
22
- from flock.core.mcp.flock_mcp_server import FlockMCPServerBase
22
+ from flock.core.mcp.flock_mcp_server import FlockMCPServer
23
23
 
24
24
  # Mixins and Serialization components
25
25
  from flock.core.mixin.dspy_integration import DSPyIntegrationMixin
@@ -30,14 +30,8 @@ logger = get_logger("agent.unified")
30
30
 
31
31
  T = TypeVar("T", bound="FlockAgent")
32
32
 
33
- SignatureType = (
34
- str
35
- | Callable[..., str]
36
- | type[BaseModel]
37
- | Callable[..., type[BaseModel]]
38
- | None
39
- )
40
33
 
34
+ DynamicStr = str | Callable[[FlockContext], str]
41
35
 
42
36
  class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
43
37
  """Unified FlockAgent using the new component architecture.
@@ -63,23 +57,29 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
63
57
  None,
64
58
  description="The model identifier to use (e.g., 'openai/gpt-4o'). If None, uses Flock's default.",
65
59
  )
66
- description: str | Callable[..., str] | None = Field(
67
- "",
60
+ description_spec: DynamicStr | None = Field(
61
+ default="",
62
+ alias="description",
63
+ validation_alias="description",
68
64
  description="A human-readable description or a callable returning one.",
69
65
  )
70
- input: SignatureType = Field(
71
- None,
66
+ input_spec: DynamicStr | None = Field(
67
+ default="",
68
+ alias="input",
69
+ validation_alias="input",
72
70
  description="Signature for input keys. Supports type hints (:) and descriptions (|).",
73
71
  )
74
- output: SignatureType = Field(
75
- None,
72
+ output_spec: DynamicStr | None = Field(
73
+ default="",
74
+ alias="output",
75
+ validation_alias="output",
76
76
  description="Signature for output keys. Supports type hints (:) and descriptions (|).",
77
77
  )
78
78
  tools: list[Callable[..., Any]] | None = Field(
79
79
  default=None,
80
80
  description="List of callable tools the agent can use. These must be registered.",
81
81
  )
82
- servers: list[str | FlockMCPServerBase] | None = Field(
82
+ servers: list[str | FlockMCPServer] | None = Field(
83
83
  default=None,
84
84
  description="List of MCP Servers the agent can use to enhance its capabilities.",
85
85
  )
@@ -91,9 +91,10 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
91
91
  )
92
92
 
93
93
  # --- EXPLICIT WORKFLOW STATE ---
94
- next_agent: str | Callable[..., str] | None = Field(
94
+ next_agent_spec: DynamicStr | None = Field(
95
95
  default=None,
96
- # exclude=True, # Runtime state, don't serialize
96
+ alias="next_agent",
97
+ validation_alias="next_agent",
97
98
  description="Next agent in workflow - set by user or routing components.",
98
99
  )
99
100
 
@@ -118,14 +119,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
118
119
  self,
119
120
  name: str,
120
121
  model: str | None = None,
121
- description: str | Callable[..., str] | None = "",
122
- input: SignatureType = None,
123
- output: SignatureType = None,
122
+ description: DynamicStr | None = None,
123
+ input: DynamicStr | None = None,
124
+ output: DynamicStr | None = None,
124
125
  tools: list[Callable[..., Any]] | None = None,
125
- servers: list[str | FlockMCPServerBase] | None = None,
126
+ servers: list[str | FlockMCPServer] | None = None,
126
127
  components: list[AgentComponent] | None = None,
127
128
  config: FlockAgentConfig | None = None,
128
- next_agent: str | Callable[..., str] | None = None,
129
+ next_agent: DynamicStr | None = None,
129
130
  temporal_activity_config: TemporalActivityConfig | None = None,
130
131
  ):
131
132
  """Initialize the unified FlockAgent with components and configuration."""
@@ -155,12 +156,12 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
155
156
  # These provide familiar access patterns while using the unified model
156
157
 
157
158
  @property
158
- def evaluator(self) -> EvaluationComponentBase | None:
159
+ def evaluator(self) -> EvaluationComponent | None:
159
160
  """Get the primary evaluation component for this agent."""
160
161
  return self._components.get_primary_evaluator()
161
162
 
162
163
  @property
163
- def router(self) -> RoutingComponentBase | None:
164
+ def router(self) -> RoutingComponent | None:
164
165
  """Get the primary routing component for this agent."""
165
166
  return self._components.get_primary_router()
166
167
 
@@ -270,22 +271,51 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
270
271
  f"Agent '{self.name}' has no evaluator to set model for."
271
272
  )
272
273
 
273
- def resolve_callables(self, context: FlockContext | None = None) -> None:
274
- """Resolves callable fields (description, input, output) using context."""
275
- self.context = context or self.context
276
- return self._integration.resolve_callables(self.context)
277
-
278
274
  @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
- """
275
+ def description(self) -> str | None:
276
+ """Returns the resolved agent description."""
284
277
  return self._integration.resolve_description(self.context)
285
278
 
279
+ @property
280
+ def input(self) -> str | None:
281
+ """Returns the resolved agent input."""
282
+ return self._integration.resolve_input(self.context)
283
+
284
+ @property
285
+ def output(self) -> str | None:
286
+ """Returns the resolved agent output."""
287
+ return self._integration.resolve_output(self.context)
288
+
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)
293
+
294
+ @description.setter
295
+ def description(self, value: DynamicStr) -> None:
296
+ self.description_spec = value
297
+
298
+ @input.setter
299
+ def input(self, value: DynamicStr) -> None:
300
+ self.input_spec = value
301
+
302
+ @output.setter
303
+ def output(self, value: DynamicStr) -> None:
304
+ self.output_spec = value
305
+
306
+ @next_agent.setter
307
+ def next_agent(self, value: DynamicStr) -> None:
308
+ self.next_agent_spec = value
309
+
286
310
  def _save_output(self, agent_name: str, result: dict[str, Any]) -> None:
287
311
  """Save output to file if configured (delegated to serialization)."""
288
312
  return self._serialization._save_output(agent_name, result)
289
313
 
290
314
  # --- Pydantic v2 Configuration ---
291
- model_config = {"arbitrary_types_allowed": True}
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
+ }