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.
- flock/__init__.py +39 -29
- flock/cli/assets/release_notes.md +111 -0
- flock/cli/constants.py +1 -0
- flock/cli/load_release_notes.py +23 -0
- flock/core/__init__.py +12 -1
- flock/core/context/context.py +10 -5
- flock/core/flock.py +61 -21
- flock/core/flock_agent.py +112 -442
- flock/core/flock_evaluator.py +49 -0
- flock/core/flock_factory.py +73 -0
- flock/core/flock_module.py +77 -0
- flock/{interpreter → core/interpreter}/python_interpreter.py +9 -1
- flock/core/logging/formatters/themes.py +1 -1
- flock/core/logging/logging.py +119 -15
- flock/core/mixin/dspy_integration.py +11 -8
- flock/core/registry/agent_registry.py +4 -2
- flock/core/tools/basic_tools.py +1 -1
- flock/core/util/cli_helper.py +59 -2
- flock/evaluators/declarative/declarative_evaluator.py +52 -0
- flock/evaluators/natural_language/natural_language_evaluator.py +66 -0
- flock/modules/callback/callback_module.py +86 -0
- flock/modules/memory/memory_module.py +235 -0
- flock/modules/memory/memory_parser.py +125 -0
- flock/modules/memory/memory_storage.py +736 -0
- flock/modules/output/output_module.py +194 -0
- flock/modules/performance/metrics_module.py +477 -0
- flock/themes/aardvark-blue.toml +1 -1
- {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/METADATA +112 -4
- {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/RECORD +32 -19
- {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/WHEEL +0 -0
- {flock_core-0.2.17.dist-info → flock_core-0.3.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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.
|
|
16
|
-
|
|
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
|
-
|
|
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"] =
|
|
82
|
-
default="",
|
|
29
|
+
next_agent: Union[str, "FlockAgent"] = Field(
|
|
30
|
+
default="", description="Next agent to invoke"
|
|
83
31
|
)
|
|
84
|
-
input: dict[str, Any] =
|
|
32
|
+
input: dict[str, Any] = Field(
|
|
85
33
|
default_factory=dict,
|
|
86
|
-
|
|
34
|
+
description="Input data for the next agent",
|
|
87
35
|
)
|
|
88
|
-
context: FlockContext =
|
|
89
|
-
default=None,
|
|
36
|
+
context: FlockContext = Field(
|
|
37
|
+
default=None, descrio="Override context parameters"
|
|
90
38
|
)
|
|
91
39
|
|
|
92
40
|
|
|
93
|
-
class FlockAgent(BaseModel, ABC
|
|
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[...,
|
|
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
|
-
|
|
83
|
+
evaluator: FlockEvaluator = Field(
|
|
206
84
|
None,
|
|
207
|
-
description="
|
|
85
|
+
description="Evaluator to use for agent evaluation",
|
|
208
86
|
)
|
|
209
87
|
|
|
210
|
-
|
|
211
|
-
default_factory=
|
|
212
|
-
description="
|
|
88
|
+
modules: dict[str, FlockModule] = Field(
|
|
89
|
+
default_factory=dict,
|
|
90
|
+
description="FlockModules attached to this agent",
|
|
213
91
|
)
|
|
214
92
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
""
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
""
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
342
|
-
|
|
172
|
+
|
|
173
|
+
for module in self.get_enabled_modules():
|
|
174
|
+
inputs = await module.pre_evaluate(self, inputs)
|
|
175
|
+
|
|
343
176
|
try:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
self
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|