flock-core 0.2.17__py3-none-any.whl → 0.3.1__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 (32) hide show
  1. flock/__init__.py +39 -29
  2. flock/cli/assets/release_notes.md +111 -0
  3. flock/cli/constants.py +1 -0
  4. flock/cli/load_release_notes.py +23 -0
  5. flock/core/__init__.py +12 -1
  6. flock/core/context/context.py +10 -5
  7. flock/core/flock.py +61 -21
  8. flock/core/flock_agent.py +112 -442
  9. flock/core/flock_evaluator.py +49 -0
  10. flock/core/flock_factory.py +73 -0
  11. flock/core/flock_module.py +77 -0
  12. flock/{interpreter → core/interpreter}/python_interpreter.py +9 -1
  13. flock/core/logging/formatters/themes.py +1 -1
  14. flock/core/logging/logging.py +119 -15
  15. flock/core/mixin/dspy_integration.py +11 -8
  16. flock/core/registry/agent_registry.py +4 -2
  17. flock/core/tools/basic_tools.py +1 -1
  18. flock/core/util/cli_helper.py +59 -2
  19. flock/evaluators/declarative/declarative_evaluator.py +52 -0
  20. flock/evaluators/natural_language/natural_language_evaluator.py +66 -0
  21. flock/modules/callback/callback_module.py +86 -0
  22. flock/modules/memory/memory_module.py +235 -0
  23. flock/modules/memory/memory_parser.py +125 -0
  24. flock/modules/memory/memory_storage.py +736 -0
  25. flock/modules/output/output_module.py +194 -0
  26. flock/modules/performance/metrics_module.py +477 -0
  27. flock/themes/aardvark-blue.toml +1 -1
  28. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/METADATA +112 -4
  29. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/RECORD +32 -19
  30. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/WHEEL +0 -0
  31. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/entry_points.txt +0 -0
  32. {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py CHANGED
@@ -4,168 +4,46 @@ import asyncio
4
4
  import json
5
5
  import os
6
6
  from abc import ABC
7
- from collections.abc import Awaitable, Callable
8
- from dataclasses import dataclass, field
7
+ from collections.abc import Callable
9
8
  from typing import Any, TypeVar, Union
10
9
 
11
10
  import cloudpickle
11
+ from opentelemetry import trace
12
12
  from pydantic import BaseModel, Field
13
13
 
14
14
  from flock.core.context.context import FlockContext
15
- from flock.core.logging.formatters.themed_formatter import (
16
- ThemedAgentResultFormatter,
17
- )
18
- from flock.core.logging.formatters.themes import OutputTheme
15
+ from flock.core.flock_evaluator import FlockEvaluator
16
+ from flock.core.flock_module import FlockModule
19
17
  from flock.core.logging.logging import get_logger
20
- from flock.core.mixin.dspy_integration import AgentType, DSPyIntegrationMixin
21
- from flock.core.mixin.prompt_parser import PromptParserMixin
22
-
23
- logger = get_logger("flock")
24
-
25
-
26
- from opentelemetry import trace
27
18
 
19
+ logger = get_logger("agent")
28
20
  tracer = trace.get_tracer(__name__)
29
21
 
30
22
 
31
23
  T = TypeVar("T", bound="FlockAgent")
32
24
 
33
25
 
34
- @dataclass
35
- class FlockAgentConfig:
36
- """Configuration options for a FlockAgent."""
37
-
38
- agent_type_override: AgentType = field(
39
- default=None,
40
- metadata={
41
- "description": "Overrides the agent type. TOOL USE ONLY WORKS WITH REACT"
42
- },
43
- )
44
- disable_output: bool = field(
45
- default=False, metadata={"description": "Disables the agent's output."}
46
- )
47
- temperature: float = field(
48
- default=0.0, metadata={"description": "Temperature for the LLM"}
49
- )
50
- max_tokens: int = field(
51
- default=2000, metadata={"description": "Max tokens for the LLM"}
52
- )
53
-
54
-
55
- @dataclass
56
- class FlockAgentOutputConfig:
57
- """Configuration options for a FlockAgent."""
58
-
59
- render_table: bool = field(
60
- default=False, metadata={"description": "Renders a table."}
61
- )
62
- theme: OutputTheme = field( # type: ignore
63
- default=OutputTheme.afterglow,
64
- metadata={"description": "Disables the agent's output."},
65
- )
66
- max_length: int = field(
67
- default=1000, metadata={"description": "Disables the agent's output."}
68
- )
69
- wait_for_input: bool = field(
70
- default=False, metadata={"description": "Wait for input."}
71
- )
72
- write_to_file: bool = field(
73
- default=False, metadata={"description": "Write to file."}
74
- )
75
-
76
-
77
- @dataclass
78
- class HandOff:
26
+ class HandOff(BaseModel):
79
27
  """Base class for handoff returns."""
80
28
 
81
- next_agent: Union[str, "FlockAgent"] = field(
82
- default="", metadata={"description": "Next agent to invoke"}
29
+ next_agent: Union[str, "FlockAgent"] = Field(
30
+ default="", description="Next agent to invoke"
83
31
  )
84
- input: dict[str, Any] = field(
32
+ input: dict[str, Any] = Field(
85
33
  default_factory=dict,
86
- metadata={"description": "Input data for the next agent"},
34
+ description="Input data for the next agent",
87
35
  )
88
- context: FlockContext = field(
89
- default=None, metadata={"description": "Override context parameters"}
36
+ context: FlockContext = Field(
37
+ default=None, descrio="Override context parameters"
90
38
  )
91
39
 
92
40
 
93
- class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
94
- """FlockAgent is the core, declarative base class for all agents in the Flock framework.
95
-
96
- Due to its declarative nature, FlockAgent does not rely on constructing classical prompts manually.
97
- Instead, each agent is defined by simply declaring:
98
- - Its expected inputs (what data it needs),
99
- - Its expected outputs (what data it produces), and
100
- - Any optional tools it can use during execution.
101
-
102
- In a declarative model, you describe *what* you expect rather than *how* to compute it. This means that
103
- instead of embedding prompt engineering logic directly in your code, you specify a concise signature.
104
- At runtime, the Flock framework automatically:
105
- - Resolves the inputs from a pre-computed context,
106
- - Constructs a precise prompt for the underlying language model (using metadata such as type hints
107
- and human-readable descriptions), and
108
- - Invokes the appropriate tools if needed.
109
-
110
- This approach minimizes hidden dependencies and boilerplate code. It allows developers to focus solely on
111
- what data the agent should work with and what result it should produce, without worrying about the intricacies
112
- of prompt formatting or context management.
113
-
114
- For details on how Flock resolves inputs, please refer to the documentation.
115
-
116
- Key benefits of the declarative approach include:
117
- - **Clarity:** The agent's interface (inputs and outputs) is explicitly defined.
118
- - **Reusability:** Agents can be easily serialized, shared, and reused since they are built as Pydantic models.
119
- - **Modularity:** By receiving a dictionary of pre-resolved inputs, agents operate independently of the global context,
120
- making them easier to test and debug.
121
- - **Extensibility:** Additional metadata (like detailed descriptions for each key) can be embedded and later used to
122
- refine the prompt for the LLM.
123
-
124
- Since FlockAgent is a Pydantic BaseModel, it can be serialized to and from JSON. This ensures that the agent's
125
- configuration and state are easily stored, transmitted, and reproduced.
126
-
127
- **Implementation Example:**
128
-
129
- Below is an example of how to define and instantiate a FlockAgent:
130
-
131
- from flock_agent import FlockAgent
132
- import basic_tools
133
-
134
- # Define an agent by declaring its inputs, outputs, and optional tools.
135
- idea_agent = FlockAgent(
136
- name="idea_agent",
137
- input="query: str | The search query, context: dict | The full conversation context",
138
- output="a_fun_software_project_idea: str | The generated software project idea",
139
- tools=[basic_tools.web_search_tavily],
140
- )
141
-
142
- # At runtime, Flock automatically resolves inputs and calls the agent:
143
- resolved_inputs = {
144
- "query": "A new social media app",
145
- "context": {"previous_idea": "a messaging platform"}
146
- }
147
- result = await idea_agent.run(resolved_inputs)
148
-
149
- In this example:
150
- - The agent declares that it needs a `query` (a string describing what to search for) and a `context` (a dictionary
151
- containing additional information).
152
- - It produces a `a_fun_software_project_idea` (a string with the generated idea).
153
- - The tool `basic_tools.web_search_tavily` is available for the agent to use if needed.
154
- - When the agent is run, the Flock framework resolves the inputs, constructs the appropriate prompt using the
155
- declared metadata, and returns the result.
156
-
157
- This declarative style streamlines agent creation and execution, making the framework highly modular, testable,
158
- and production-ready.
159
-
160
- In the future options will be provided to optimize the "hidden" prompt generation and execution so the agent will
161
- perform close to its theoretical maximum.
162
- """
163
-
41
+ class FlockAgent(BaseModel, ABC):
164
42
  name: str = Field(..., description="Unique identifier for the agent.")
165
43
  model: str | None = Field(
166
44
  None, description="The model to use (e.g., 'openai/gpt-4o')."
167
45
  )
168
- description: str | Callable[..., str] = Field(
46
+ description: str | Callable[..., str] | None = Field(
169
47
  "", description="A human-readable description of the agent."
170
48
  )
171
49
 
@@ -194,7 +72,7 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
194
72
  description="Set to True to enable caching of the agent's results.",
195
73
  )
196
74
 
197
- hand_off: str | Callable[..., Any] | None = Field(
75
+ hand_off: str | HandOff | Callable[..., HandOff] | None = Field(
198
76
  None,
199
77
  description=(
200
78
  "Specifies the next agent in the workflow or a callable that determines the handoff. "
@@ -202,160 +80,107 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
202
80
  ),
203
81
  )
204
82
 
205
- termination: str | Callable[..., str] | None = Field(
83
+ evaluator: FlockEvaluator = Field(
206
84
  None,
207
- description="An optional termination condition or phrase used to indicate when the agent should stop processing.",
85
+ description="Evaluator to use for agent evaluation",
208
86
  )
209
87
 
210
- config: FlockAgentConfig = Field(
211
- default_factory=FlockAgentConfig,
212
- description="Configuration options for the agent, such as serialization settings.",
88
+ modules: dict[str, FlockModule] = Field(
89
+ default_factory=dict,
90
+ description="FlockModules attached to this agent",
213
91
  )
214
92
 
215
- output_config: FlockAgentOutputConfig = Field(
216
- default_factory=FlockAgentOutputConfig,
217
- description="Configuration options for the agent's output.",
218
- )
93
+ def add_module(self, module: FlockModule) -> None:
94
+ """Add a module to this agent."""
95
+ self.modules[module.name] = module
219
96
 
220
- # Lifecycle callback fields: if provided, these callbacks are used instead of overriding the methods.
221
- initialize_callback: Callable[[dict[str, Any]], Awaitable[None]] | None = (
222
- Field(
223
- default=None,
224
- description="Optional callback function for initialization. If provided, this async function is called with the inputs.",
225
- )
226
- )
227
- evaluate_callback: (
228
- Callable[[dict[str, Any]], Awaitable[dict[str, Any]]] | None
229
- ) = Field(
230
- default=None,
231
- description="Optional callback function for evaluate. If provided, this async function is called with the inputs instead of the internal evaluate",
232
- )
233
- terminate_callback: (
234
- Callable[[dict[str, Any], dict[str, Any]], Awaitable[None]] | None
235
- ) = Field(
236
- default=None,
237
- description="Optional callback function for termination. If provided, this async function is called with the inputs and result.",
238
- )
239
- on_error_callback: (
240
- Callable[[Exception, dict[str, Any]], Awaitable[None]] | None
241
- ) = Field(
242
- default=None,
243
- description="Optional callback function for error handling. If provided, this async function is called with the error and inputs.",
244
- )
97
+ def remove_module(self, module_name: str) -> None:
98
+ """Remove a module from this agent."""
99
+ if module_name in self.modules:
100
+ del self.modules[module_name]
101
+
102
+ def get_module(self, module_name: str) -> FlockModule | None:
103
+ """Get a module by name."""
104
+ return self.modules.get(module_name)
105
+
106
+ def get_enabled_modules(self) -> list[FlockModule | None]:
107
+ """Get a module by name."""
108
+ return [m for m in self.modules.values() if m.config.enabled]
245
109
 
246
110
  # Lifecycle hooks
247
111
  async def initialize(self, inputs: dict[str, Any]) -> None:
248
- """Called at the very start of the agent's execution.
112
+ with tracer.start_as_current_span("agent.initialize") as span:
113
+ span.set_attribute("agent.name", self.name)
114
+ span.set_attribute("inputs", str(inputs))
249
115
 
250
- Override this method or provide an `initialize_callback` to perform setup tasks such as input validation or resource loading.
251
- """
252
- if self.initialize_callback is not None:
253
- await self.initialize_callback(self, inputs)
254
- else:
255
- pass
116
+ try:
117
+ for module in self.get_enabled_modules():
118
+ logger.info(
119
+ f"agent.initialize - module {module.name}",
120
+ agent=self.name,
121
+ )
122
+ await module.initialize(self, inputs)
123
+ except Exception as module_error:
124
+ logger.error(
125
+ "Error during initialize",
126
+ agent=self.name,
127
+ error=str(module_error),
128
+ )
129
+ span.record_exception(module_error)
256
130
 
257
131
  async def terminate(
258
132
  self, inputs: dict[str, Any], result: dict[str, Any]
259
133
  ) -> None:
260
- """Called at the very end of the agent's execution.
261
-
262
- Override this method or provide a `terminate_callback` to perform cleanup tasks such as releasing resources or logging results.
263
- """
264
- if self.terminate_callback is not None:
265
- await self.terminate_callback(self, inputs, result)
266
- else:
267
- pass
134
+ with tracer.start_as_current_span("agent.terminate") as span:
135
+ span.set_attribute("agent.name", self.name)
136
+ span.set_attribute("inputs", str(inputs))
137
+ span.set_attribute("result", str(result))
138
+ logger.info(
139
+ f"agent.terminate",
140
+ agent=self.name,
141
+ )
142
+ try:
143
+ for module in self.get_enabled_modules():
144
+ await module.terminate(self, inputs, inputs)
145
+ except Exception as module_error:
146
+ logger.error(
147
+ "Error during terminate",
148
+ agent=self.name,
149
+ error=str(module_error),
150
+ )
151
+ span.record_exception(module_error)
268
152
 
269
153
  async def on_error(self, error: Exception, inputs: dict[str, Any]) -> None:
270
- """Called if the agent encounters an error during execution.
271
-
272
- Override this method or provide an `on_error_callback` to implement custom error handling or recovery strategies.
273
- """
274
- if self.on_error_callback is not None:
275
- await self.on_error_callback(self, error, inputs)
276
- else:
277
- pass
154
+ with tracer.start_as_current_span("agent.on_error") as span:
155
+ span.set_attribute("agent.name", self.name)
156
+ span.set_attribute("inputs", str(inputs))
157
+ try:
158
+ for module in self.get_enabled_modules():
159
+ await module.on_error(self, error, inputs)
160
+ except Exception as module_error:
161
+ logger.error(
162
+ "Error during on_error",
163
+ agent=self.name,
164
+ error=str(module_error),
165
+ )
166
+ span.record_exception(module_error)
278
167
 
279
168
  async def evaluate(self, inputs: dict[str, Any]) -> dict[str, Any]:
280
- """Process the agent's task using the provided inputs and return the result.
281
-
282
- This asynchronous method is the core execution engine for a FlockAgent. It performs the following steps:
283
-
284
- 1. **Extract Descriptions:**
285
- Parses the agent's configured input and output strings to extract human-readable descriptions.
286
- These strings are expected to use the format "key: type_hint | description". The method removes the
287
- type hints and builds dictionaries that map each key to its corresponding description.
288
-
289
- 2. **Construct the Prompt:**
290
- Based on the extracted descriptions, the method builds a detailed prompt that clearly lists all input
291
- fields (with their descriptions) and output fields. This prompt is designed to guide the language model,
292
- ensuring that it understands what inputs are provided and what outputs are expected.
293
-
294
- 3. **Configure the Language Model:**
295
- The method initializes and configures a language model using the dspy library. The agent's model
296
- (e.g., "openai/gpt-4o") is used to set up the language model, ensuring that it is ready to process the prompt.
297
-
298
- 4. **Execute the Task:**
299
- Depending on whether the agent has been configured with additional tools:
300
- - **With Tools:** A ReAct task is instantiated. This task interleaves reasoning and tool usage,
301
- allowing the agent to leverage external functionalities during execution.
302
- - **Without Tools:** A Predict task is used for a straightforward generation based on the prompt.
303
-
304
- 5. **Process the Result:**
305
- After execution, the method attempts to convert the result to a dictionary. It also ensures that each
306
- expected input key is present in the output (by setting a default value from the inputs if necessary).
307
-
308
- 6. **Error Handling:**
309
- Any exceptions raised during the process are caught, logged (or printed), and then re-raised to allow
310
- higher-level error handling. This ensures that errors are not silently ignored and can be properly diagnosed.
311
-
312
- **Arguments:**
313
- inputs (dict[str, Any]): A dictionary containing all the resolved input values required by the agent.
314
- These inputs are typically obtained from the global context or from the output of a previous agent.
315
-
316
- **Returns:**
317
- dict[str, Any]: A dictionary containing the output generated by the agent. This output adheres to the
318
- agent's declared output fields and includes any fallback values for missing inputs.
319
-
320
- **Usage Example:**
321
-
322
- Suppose an agent is declared with the following configuration:
323
- input = "query: str | The search query, context: dict | Additional context"
324
- output = "idea: str | The generated software project idea"
325
-
326
- When invoked with:
327
- inputs = {"query": "build an app", "context": {"previous_idea": "messaging app"}}
328
-
329
- The method will:
330
- - Parse the descriptions to create:
331
- input_descriptions = {"query": "The search query", "context": "Additional context"}
332
- output_descriptions = {"idea": "The generated software project idea"}
333
- - Construct a prompt that lists these inputs and outputs clearly.
334
- - Configure the language model and execute the appropriate task (ReAct if tools are provided, otherwise Predict).
335
- - Return a dictionary similar to:
336
- {"idea": "A fun app idea based on ...", "query": "build an app", "context": {"previous_idea": "messaging app"}}
337
- """
338
169
  with tracer.start_as_current_span("agent.evaluate") as span:
339
170
  span.set_attribute("agent.name", self.name)
340
171
  span.set_attribute("inputs", str(inputs))
341
- if self.evaluate_callback is not None:
342
- return await self.evaluate_callback(self, inputs)
172
+
173
+ for module in self.get_enabled_modules():
174
+ inputs = await module.pre_evaluate(self, inputs)
175
+
343
176
  try:
344
- # Create and configure the signature and language model.
345
- self.__dspy_signature = self.create_dspy_signature_class(
346
- self.name,
347
- self.description,
348
- f"{self.input} -> {self.output}",
349
- )
350
- self._configure_language_model()
351
- agent_task = self._select_task(
352
- self.__dspy_signature,
353
- agent_type_override=self.config.agent_type_override,
354
- )
355
- # Execute the task.
356
- result = agent_task(**inputs)
357
- result = self._process_result(result, inputs)
177
+ result = await self.evaluator.evaluate(self, inputs, self.tools)
178
+
179
+ for module in self.get_enabled_modules():
180
+ result = await module.post_evaluate(self, inputs, result)
181
+
358
182
  span.set_attribute("result", str(result))
183
+
359
184
  logger.info("Evaluation successful", agent=self.name)
360
185
  return result
361
186
  except Exception as eval_error:
@@ -394,58 +219,14 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
394
219
  return asyncio.run(self.run_async(inputs))
395
220
 
396
221
  async def run_async(self, inputs: dict[str, Any]) -> dict[str, Any]:
397
- """Run the agent with the given inputs and return its generated output.
398
-
399
- This method represents the primary execution flow for a FlockAgent and performs the following
400
- lifecycle steps in sequence:
401
-
402
- 1. **Initialization:**
403
- Calls the `initialize(inputs)` hook to perform any necessary pre-run setup, such as input
404
- validation, resource allocation, or logging. This ensures that the agent is properly configured
405
- before processing begins.
406
-
407
- 2. **Evaluation:**
408
- Invokes the internal `_evaluate(inputs)` method, which constructs a detailed prompt (incorporating
409
- input and output descriptions), configures the underlying language model via dspy, and executes the
410
- main task (using a ReAct task if tools are provided, or a Predict task otherwise).
411
-
412
- 3. **Termination:**
413
- Calls the `terminate(inputs, result)` hook after evaluation, allowing the agent to clean up any
414
- resources or perform post-run actions (such as logging the output).
415
-
416
- 4. **Output Return:**
417
- Returns a dictionary containing the agent's output. This output conforms to the agent's declared
418
- output fields and may include any default or fallback values for missing inputs.
419
-
420
- If an error occurs during any of these steps, the `on_error(error, inputs)` hook is invoked to handle
421
- the exception (for instance, by logging detailed error information). The error is then re-raised, ensuring
422
- that higher-level error management can address the failure.
423
-
424
- **Arguments:**
425
- inputs (dict[str, Any]): A dictionary containing the resolved input values required by the agent.
426
- These inputs are typically derived from the agent's declared input signature and may include data
427
- provided by previous agents or from a global context.
428
-
429
- **Returns:**
430
- dict[str, Any]: A dictionary containing the output generated by the agent. The output structure
431
- adheres to the agent's declared output fields.
432
-
433
- **Example:**
434
- Suppose an agent is defined with:
435
- input = "query: str | The search query, context: dict | Additional context"
436
- output = "result: str | The generated idea"
437
- When executed with:
438
- inputs = {"query": "build a chatbot", "context": {"user": "Alice"}}
439
- The method might return:
440
- {"result": "A conversational chatbot that uses AI to...", "query": "build a chatbot", "context": {"user": "Alice"}}
441
- """
442
222
  with tracer.start_as_current_span("agent.run") as span:
443
223
  span.set_attribute("agent.name", self.name)
444
224
  span.set_attribute("inputs", str(inputs))
445
225
  try:
446
226
  await self.initialize(inputs)
227
+
447
228
  result = await self.evaluate(inputs)
448
- self.display_output(result)
229
+
449
230
  await self.terminate(inputs, result)
450
231
  span.set_attribute("result", str(result))
451
232
  logger.info("Agent run completed", agent=self.name)
@@ -458,60 +239,7 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
458
239
  span.record_exception(run_error)
459
240
  raise
460
241
 
461
- def display_info(self) -> None:
462
- pass
463
-
464
- def display_output(self, result: dict[str, Any]) -> None:
465
- """Display the agent's output using the configured output formatter."""
466
- ThemedAgentResultFormatter(
467
- self.output_config.theme,
468
- self.output_config.max_length,
469
- self.output_config.render_table,
470
- self.output_config.wait_for_input,
471
- self.output_config.write_to_file,
472
- ).display_result(result, self.name)
473
-
474
242
  async def run_temporal(self, inputs: dict[str, Any]) -> dict[str, Any]:
475
- """Execute this agent via a Temporal workflow for enhanced fault tolerance and asynchronous processing.
476
-
477
- This method enables remote execution of the agent within a Temporal environment, leveraging Temporal's
478
- capabilities for persistence, retries, and distributed error handling. The workflow encapsulates the agent's
479
- logic so that it can run on Temporal workers, providing robustness and scalability in production systems.
480
-
481
- The method performs these steps:
482
- 1. **Connect to Temporal:**
483
- Establishes a connection to the Temporal server using a Temporal client configured with the appropriate
484
- namespace (default is "default").
485
- 2. **Serialization:**
486
- Serializes the agent instance (via `to_dict()`) along with the provided inputs. This step converts any
487
- callable objects (e.g., lifecycle hooks or tools) into a storable format using cloudpickle.
488
- 3. **Activity Invocation:**
489
- Triggers a designated Temporal activity (e.g., `run_flock_agent_activity`) by passing the serialized agent
490
- data and inputs. The Temporal activity is responsible for executing the agent's logic in a fault-tolerant
491
- manner.
492
- 4. **Return Output:**
493
- Awaits and returns the resulting output from the Temporal workflow as a dictionary, consistent with the
494
- output structure of the local `run()` method.
495
-
496
- If any error occurs during these steps, the error is logged and re-raised to ensure that failure is properly
497
- handled by higher-level error management systems.
498
-
499
- **Arguments:**
500
- inputs (dict[str, Any]): A dictionary containing the resolved inputs required by the agent, similar to those
501
- provided to the local `run()` method.
502
-
503
- **Returns:**
504
- dict[str, Any]: A dictionary containing the output produced by the agent after remote execution via Temporal.
505
- The output format is consistent with that of the local `run()` method.
506
-
507
- **Example:**
508
- Given an agent defined with:
509
- input = "query: str | The search query, context: dict | Additional context"
510
- output = "result: str | The generated idea"
511
- Calling:
512
- result = await agent.run_temporal({"query": "analyze data", "context": {"source": "sales"}})
513
- will execute the agent on a Temporal worker and return the output in a structured dictionary format.
514
- """
515
243
  with tracer.start_as_current_span("agent.run_temporal") as span:
516
244
  span.set_attribute("agent.name", self.name)
517
245
  span.set_attribute("inputs", str(inputs))
@@ -548,12 +276,6 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
548
276
  raise
549
277
 
550
278
  def resolve_callables(self, context) -> None:
551
- """Resolve any callable fields in the agent instance using the provided context.
552
-
553
- This method resolves any callable fields in the agent instance using the provided context. It iterates over
554
- the agent's fields and replaces any callable objects (such as lifecycle hooks or tools) with their corresponding
555
- resolved values from the context. This ensures that the agent is fully configured and ready
556
- """
557
279
  if isinstance(self.input, Callable):
558
280
  self.input = self.input(context)
559
281
  if isinstance(self.output, Callable):
@@ -562,41 +284,6 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
562
284
  self.description = self.description(context)
563
285
 
564
286
  def to_dict(self) -> dict[str, Any]:
565
- """Serialize the FlockAgent instance to a dictionary.
566
-
567
- This method converts the entire agent instance—including its configuration, state, and lifecycle hooks—
568
- into a dictionary format. It uses cloudpickle to serialize any callable objects (such as functions or
569
- methods), converting them into hexadecimal string representations. This ensures that the agent can be
570
- easily persisted, transmitted, or logged as JSON.
571
-
572
- The serialization process is recursive:
573
- - If a field is a callable (and not a class), it is serialized using cloudpickle.
574
- - Lists and dictionaries are processed recursively to ensure that all nested callables are properly handled.
575
-
576
- **Returns:**
577
- dict[str, Any]: A dictionary representing the FlockAgent, which includes all of its configuration data.
578
- This dictionary is suitable for storage, debugging, or transmission over the network.
579
-
580
- **Example:**
581
- For an agent defined as:
582
- name = "idea_agent",
583
- model = "openai/gpt-4o",
584
- input = "query: str | The search query, context: dict | The full conversation context",
585
- output = "idea: str | The generated idea"
586
- Calling `agent.to_dict()` might produce:
587
- {
588
- "name": "idea_agent",
589
- "model": "openai/gpt-4o",
590
- "input": "query: str | The search query, context: dict | The full conversation context",
591
- "output": "idea: str | The generated idea",
592
- "tools": ["<serialized tool representation>"],
593
- "use_cache": False,
594
- "hand_off": None,
595
- "termination": None,
596
- ...
597
- }
598
- """
599
-
600
287
  def convert_callable(obj: Any) -> Any:
601
288
  if callable(obj) and not isinstance(obj, type):
602
289
  return cloudpickle.dumps(obj).hex()
@@ -607,43 +294,16 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
607
294
  return obj
608
295
 
609
296
  data = self.model_dump()
297
+ module_data = {}
298
+ for name, module in self.modules.items():
299
+ module_data[name] = module.dict()
300
+
301
+ data["modules"] = module_data
302
+
610
303
  return convert_callable(data)
611
304
 
612
305
  @classmethod
613
306
  def from_dict(cls: type[T], data: dict[str, Any]) -> T:
614
- """Deserialize a FlockAgent instance from a dictionary.
615
-
616
- This class method reconstructs a FlockAgent from its serialized dictionary representation, as produced
617
- by the `to_dict()` method. It recursively processes the dictionary to convert any serialized callables
618
- (stored as hexadecimal strings via cloudpickle) back into executable callable objects.
619
-
620
- **Arguments:**
621
- data (dict[str, Any]): A dictionary representation of a FlockAgent, typically produced by `to_dict()`.
622
- The dictionary should contain all configuration fields and state information necessary to fully
623
- reconstruct the agent.
624
-
625
- **Returns:**
626
- FlockAgent: An instance of FlockAgent reconstructed from the provided dictionary. The deserialized agent
627
- will have the same configuration, state, and behavior as the original instance.
628
-
629
- **Example:**
630
- Suppose you have the following dictionary:
631
- {
632
- "name": "idea_agent",
633
- "model": "openai/gpt-4o",
634
- "input": "query: str | The search query, context: dict | The full conversation context",
635
- "output": "idea: str | The generated idea",
636
- "tools": ["<serialized tool representation>"],
637
- "use_cache": False,
638
- "hand_off": None,
639
- "termination": None,
640
- ...
641
- }
642
- Then, calling:
643
- agent = FlockAgent.from_dict(data)
644
- will return a FlockAgent instance with the same properties and behavior as when it was originally serialized.
645
- """
646
-
647
307
  def convert_callable(obj: Any) -> Any:
648
308
  if isinstance(obj, str) and len(obj) > 2:
649
309
  try:
@@ -656,5 +316,15 @@ class FlockAgent(BaseModel, ABC, PromptParserMixin, DSPyIntegrationMixin):
656
316
  return {k: convert_callable(v) for k, v in obj.items()}
657
317
  return obj
658
318
 
319
+ module_data = data.pop("modules", {})
659
320
  converted = convert_callable(data)
660
- return cls(**converted)
321
+ agent = cls(**converted)
322
+
323
+ for name, module_dict in module_data.items():
324
+ module_type = module_dict.pop("type", None)
325
+ if module_type:
326
+ module_class = globals()[module_type]
327
+ module = module_class(**module_dict)
328
+ agent.add_module(module)
329
+
330
+ return agent