flock-core 0.3.22__py3-none-any.whl → 0.3.30__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/core/flock.py CHANGED
@@ -1,476 +1,554 @@
1
+ # src/flock/core/flock.py
1
2
  """High-level orchestrator for creating and executing agents."""
2
3
 
4
+ from __future__ import annotations # Ensure forward references work
5
+
3
6
  import asyncio
4
- import json
5
7
  import os
6
8
  import uuid
7
- from typing import Any, TypeVar
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, TypeVar
8
11
 
9
- import cloudpickle
10
12
  from opentelemetry import trace
11
13
  from opentelemetry.baggage import get_baggage, set_baggage
12
14
 
15
+ # Pydantic and OpenTelemetry
16
+ from pydantic import BaseModel, Field # Using Pydantic directly now
17
+
18
+ # Flock core components & utilities
13
19
  from flock.config import TELEMETRY
14
20
  from flock.core.context.context import FlockContext
15
21
  from flock.core.context.context_manager import initialize_context
16
22
  from flock.core.execution.local_executor import run_local_workflow
17
23
  from flock.core.execution.temporal_executor import run_temporal_workflow
18
- from flock.core.flock_agent import FlockAgent
19
24
  from flock.core.logging.logging import LOGGERS, get_logger, get_module_loggers
20
- from flock.core.registry.agent_registry import Registry
25
+
26
+ # Import FlockAgent using TYPE_CHECKING to avoid circular import at runtime
27
+ if TYPE_CHECKING:
28
+ from flock.core.flock_agent import FlockAgent
29
+ else:
30
+ # Provide a forward reference string or Any for runtime if FlockAgent is used in hints here
31
+ FlockAgent = "FlockAgent" # Forward reference string for Pydantic/runtime
32
+
33
+ # Registry and Serialization
34
+ from flock.core.flock_registry import (
35
+ get_registry, # Use the unified registry
36
+ )
37
+ from flock.core.serialization.serializable import (
38
+ Serializable, # Import Serializable base
39
+ )
40
+
41
+ # NOTE: Flock.to_dict/from_dict primarily orchestrates agent serialization.
42
+ # It doesn't usually need serialize_item/deserialize_item directly,
43
+ # relying on FlockAgent's implementation instead.
44
+ # from flock.core.serialization.serialization_utils import serialize_item, deserialize_item
45
+ # CLI Helper (if still used directly, otherwise can be removed)
21
46
  from flock.core.util.cli_helper import init_console
22
- from flock.core.util.input_resolver import top_level_to_keys
23
47
 
24
- T = TypeVar("T", bound=FlockAgent)
25
- logger = get_logger("flock")
26
- TELEMETRY.setup_tracing()
27
- tracer = trace.get_tracer(__name__)
48
+ # Cloudpickle for fallback/direct serialization if needed
49
+ try:
50
+ import cloudpickle
28
51
 
52
+ PICKLE_AVAILABLE = True
53
+ except ImportError:
54
+ PICKLE_AVAILABLE = False
29
55
 
30
- def init_loggers(enable_logging: bool | list[str] = False):
31
- """Initialize the loggers for the Flock system.
32
56
 
33
- Args:
34
- enable_logging (bool): If True, enable verbose logging. Defaults to False.
35
- """
36
- if isinstance(enable_logging, list):
37
- for logger in LOGGERS:
38
- if logger in enable_logging:
39
- other_loggers = get_logger(logger)
40
- other_loggers.enable_logging = True
41
- else:
42
- other_loggers = get_logger(logger)
43
- other_loggers.enable_logging = False
44
- else:
45
- logger = get_logger("flock")
46
- logger.enable_logging = enable_logging
47
- other_loggers = get_logger("interpreter")
48
- other_loggers.enable_logging = enable_logging
49
- other_loggers = get_logger("memory")
50
- other_loggers.enable_logging = enable_logging
51
- other_loggers = get_logger("activities")
52
- other_loggers.enable_logging = enable_logging
53
- other_loggers = get_logger("context")
54
- other_loggers.enable_logging = enable_logging
55
- other_loggers = get_logger("registry")
56
- other_loggers.enable_logging = enable_logging
57
- other_loggers = get_logger("tools")
58
- other_loggers.enable_logging = enable_logging
59
- other_loggers = get_logger("agent")
60
- other_loggers.enable_logging = enable_logging
57
+ logger = get_logger("flock")
58
+ TELEMETRY.setup_tracing() # Setup OpenTelemetry
59
+ tracer = trace.get_tracer(__name__)
60
+ FlockRegistry = get_registry() # Get the registry instance
61
61
 
62
- module_loggers = get_module_loggers()
63
- for module_logger in module_loggers:
64
- module_logger.enable_logging = enable_logging
62
+ # Define TypeVar for generic methods like from_dict
63
+ T = TypeVar("T", bound="Flock")
65
64
 
66
65
 
67
- class Flock:
68
- """High-level orchestrator for creating and executing agents.
66
+ # Inherit from Serializable for YAML/JSON/etc. methods
67
+ # Use BaseModel directly for Pydantic features
68
+ class Flock(BaseModel, Serializable):
69
+ """High-level orchestrator for creating and executing agent systems.
69
70
 
70
- Flock manages the registration of agents and tools, sets up the global context, and runs the agent workflows.
71
- It provides an easy-to-use API for both local (debug) and production (Temporal) execution.
71
+ Flock manages agent definitions, context, and execution flow, supporting
72
+ both local debugging and robust distributed execution via Temporal.
73
+ It is serializable to various formats like YAML and JSON.
72
74
  """
73
75
 
76
+ model: str | None = Field(
77
+ default="openai/gpt-4o",
78
+ description="Default model identifier to be used for agents if not specified otherwise.",
79
+ )
80
+ description: str | None = Field(
81
+ default=None,
82
+ description="A brief description of the purpose of this Flock configuration.",
83
+ )
84
+ enable_temporal: bool = Field(
85
+ default=False,
86
+ description="If True, execute workflows via Temporal; otherwise, run locally.",
87
+ )
88
+ # --- Runtime Attributes (Excluded from Serialization) ---
89
+ # Store agents internally but don't make it part of the Pydantic model definition
90
+ # Use a regular attribute, initialized in __init__
91
+ # Pydantic V2 handles __init__ and attributes not in Field correctly
92
+ _agents: dict[str, FlockAgent]
93
+ _start_agent_name: str | None
94
+ _start_input: dict
95
+
96
+ # Pydantic v2 model config
97
+ model_config = {
98
+ "arbitrary_types_allowed": True,
99
+ "ignored_types": (
100
+ type(FlockRegistry),
101
+ ), # Prevent validation issues with registry
102
+ # No need to exclude fields here, handled in to_dict
103
+ }
104
+
74
105
  def __init__(
75
106
  self,
76
- model: str = "openai/gpt-4o",
107
+ model: str | None = "openai/gpt-4o",
108
+ description: str | None = None,
77
109
  enable_temporal: bool = False,
78
- enable_logging: bool | list[str] = False,
110
+ enable_logging: bool
111
+ | list[str] = False, # Keep logging control at init
112
+ agents: list[FlockAgent] | None = None, # Allow passing agents at init
113
+ **kwargs, # Allow extra fields during init if needed, Pydantic handles it
79
114
  ):
80
- """Initialize the Flock orchestrator.
81
-
82
- Args:
83
- model (str): The default model identifier to be used for agents. Defaults to "openai/gpt-4o".
84
- local_debug (bool): If True, run the agent workflow locally for debugging purposes. Defaults to False.
85
- enable_logging (bool): If True, enable verbose logging. Defaults to False.
86
- output_formatter (FormatterOptions): Options for formatting output results.
87
- """
88
- with tracer.start_as_current_span("flock_init") as span:
89
- span.set_attribute("model", model)
90
- span.set_attribute("enable_temporal", enable_temporal)
91
- span.set_attribute("enable_logging", enable_logging)
92
-
93
- init_loggers(enable_logging)
94
- logger.info(
95
- "Initializing Flock",
96
- model=model,
97
- enable_temporal=enable_temporal,
98
- enable_logging=enable_logging,
99
- )
100
- session_id = get_baggage("session_id")
101
- if not session_id:
102
- session_id = str(uuid.uuid4())
103
- set_baggage("session_id", session_id)
104
-
105
- init_console()
106
-
107
- self.agents: dict[str, FlockAgent] = {}
108
- self.registry = Registry()
109
- self.context = FlockContext()
110
- self.model = model
111
- self.enable_temporal = enable_temporal
112
- self.start_agent: FlockAgent | str | None = None
113
- self.input: dict = {}
114
-
115
- if not enable_temporal:
115
+ """Initialize the Flock orchestrator."""
116
+ # Initialize Pydantic fields
117
+ super().__init__(
118
+ model=model,
119
+ description=description,
120
+ enable_temporal=enable_temporal,
121
+ **kwargs, # Pass extra kwargs to Pydantic BaseModel
122
+ )
123
+
124
+ # Initialize runtime attributes AFTER super().__init__()
125
+ self._agents = {}
126
+ self._start_agent_name = None
127
+ self._start_input = {}
128
+
129
+ # Set up logging
130
+ self._configure_logging(enable_logging)
131
+
132
+ # Register passed agents
133
+ if agents:
134
+ # Ensure FlockAgent type is available for isinstance check
135
+ # This import might need to be deferred or handled carefully if it causes issues
136
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
137
+
138
+ for agent in agents:
139
+ if isinstance(agent, ConcreteFlockAgent):
140
+ self.add_agent(agent)
141
+ else:
142
+ logger.warning(
143
+ f"Item provided in 'agents' list is not a FlockAgent: {type(agent)}"
144
+ )
145
+
146
+ # Initialize console if needed
147
+ init_console()
148
+
149
+ # Set Temporal debug environment variable
150
+ self._set_temporal_debug_flag()
151
+
152
+ # Ensure session ID exists in baggage
153
+ self._ensure_session_id()
154
+
155
+ logger.info(
156
+ "Flock instance initialized",
157
+ model=self.model,
158
+ enable_temporal=self.enable_temporal,
159
+ )
160
+
161
+ # --- Keep _configure_logging, _set_temporal_debug_flag, _ensure_session_id ---
162
+ # ... (implementation as before) ...
163
+ def _configure_logging(self, enable_logging: bool | list[str]):
164
+ """Configure logging levels based on the enable_logging flag."""
165
+ logger.debug(f"Configuring logging, enable_logging={enable_logging}")
166
+ is_enabled_globally = False
167
+ enabled_loggers = []
168
+
169
+ if isinstance(enable_logging, bool):
170
+ is_enabled_globally = enable_logging
171
+ elif isinstance(enable_logging, list):
172
+ is_enabled_globally = bool(
173
+ enable_logging
174
+ ) # Enable if list is not empty
175
+ enabled_loggers = enable_logging
176
+
177
+ # Configure core loggers
178
+ for log_name in LOGGERS:
179
+ log_instance = get_logger(log_name)
180
+ if is_enabled_globally or log_name in enabled_loggers:
181
+ log_instance.enable_logging = True
182
+ else:
183
+ log_instance.enable_logging = False
184
+
185
+ # Configure module loggers (existing ones)
186
+ module_loggers = get_module_loggers()
187
+ for mod_log in module_loggers:
188
+ if is_enabled_globally or mod_log.name in enabled_loggers:
189
+ mod_log.enable_logging = True
190
+ else:
191
+ mod_log.enable_logging = False
192
+
193
+ def _set_temporal_debug_flag(self):
194
+ """Set or remove LOCAL_DEBUG env var based on enable_temporal."""
195
+ if not self.enable_temporal:
196
+ if "LOCAL_DEBUG" not in os.environ:
116
197
  os.environ["LOCAL_DEBUG"] = "1"
117
- logger.debug("Set LOCAL_DEBUG environment variable")
118
- elif "LOCAL_DEBUG" in os.environ:
119
- del os.environ["LOCAL_DEBUG"]
120
- logger.debug("Removed LOCAL_DEBUG environment variable")
121
-
122
- def add_agent(self, agent: T) -> T:
123
- """Add a new agent to the Flock system.
124
-
125
- This method registers the agent, updates the internal registry and global context, and
126
- sets default values if needed. If an agent with the same name already exists, the existing
127
- agent is returned.
128
-
129
- Args:
130
- agent (FlockAgent): The agent instance to add.
131
-
132
- Returns:
133
- FlockAgent: The registered agent instance.
134
- """
135
- with tracer.start_as_current_span("add_agent") as span:
136
- span.set_attribute("agent_name", agent.name)
137
- if not agent.model:
138
- agent.set_model(self.model)
139
198
  logger.debug(
140
- f"Using default model for agent {agent.name}",
141
- model=self.model,
199
+ "Set LOCAL_DEBUG environment variable for local execution."
142
200
  )
201
+ elif "LOCAL_DEBUG" in os.environ:
202
+ del os.environ["LOCAL_DEBUG"]
203
+ logger.debug(
204
+ "Removed LOCAL_DEBUG environment variable for Temporal execution."
205
+ )
143
206
 
144
- if agent.name in self.agents:
207
+ def _ensure_session_id(self):
208
+ """Ensure a session_id exists in the OpenTelemetry baggage."""
209
+ session_id = get_baggage("session_id")
210
+ if not session_id:
211
+ session_id = str(uuid.uuid4())
212
+ set_baggage("session_id", session_id)
213
+ logger.debug(f"Generated new session_id: {session_id}")
214
+
215
+ # --- Keep add_agent, agents property, run, run_async ---
216
+ # ... (implementation as before, ensuring FlockAgent type hint is handled) ...
217
+ def add_agent(self, agent: FlockAgent) -> FlockAgent:
218
+ """Adds an agent instance to this Flock configuration."""
219
+ # Ensure FlockAgent type is available for isinstance check
220
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
221
+
222
+ if not isinstance(agent, ConcreteFlockAgent):
223
+ raise TypeError("Provided object is not a FlockAgent instance.")
224
+ if not agent.name:
225
+ raise ValueError("Agent must have a name.")
226
+
227
+ if agent.name in self._agents:
228
+ logger.warning(
229
+ f"Agent '{agent.name}' already exists in this Flock instance. Overwriting."
230
+ )
231
+ self._agents[agent.name] = agent
232
+ FlockRegistry.register_agent(agent) # Also register globally
233
+
234
+ # Set default model if agent doesn't have one
235
+ if agent.model is None:
236
+ # agent.set_model(self.model) # Use Flock's default model
237
+ if self.model: # Ensure Flock has a model defined
238
+ agent.set_model(self.model)
239
+ logger.debug(
240
+ f"Agent '{agent.name}' using Flock default model: {self.model}"
241
+ )
242
+ else:
145
243
  logger.warning(
146
- f"Agent {agent.name} already exists, returning existing instance"
244
+ f"Agent '{agent.name}' has no model and Flock default model is not set."
147
245
  )
148
- return self.agents[agent.name]
149
- logger.info(f"Adding new agent '{agent.name}'")
150
246
 
151
- self.agents[agent.name] = agent
152
- self.registry.register_agent(agent)
153
- self.context.add_agent_definition(
154
- type(agent), agent.name, agent.to_dict()
155
- )
247
+ logger.info(f"Agent '{agent.name}' added to Flock.")
248
+ return agent
156
249
 
157
- if hasattr(agent, "tools") and agent.tools:
158
- for tool in agent.tools:
159
- self.registry.register_tool(tool.__name__, tool)
160
- logger.debug(
161
- f"Registered tool '{tool.__name__}'",
162
- tool_name=tool.__name__,
163
- )
164
- logger.success(f"'{agent.name}' added successfully")
165
- return agent
166
-
167
- def add_tool(self, tool_name: str, tool: callable):
168
- """Register a tool with the Flock system.
169
-
170
- Args:
171
- tool_name (str): The name under which the tool will be registered.
172
- tool (callable): The tool function to register.
173
- """
174
- with tracer.start_as_current_span("add_tool") as span:
175
- span.set_attribute("tool_name", tool_name)
176
- span.set_attribute("tool", tool.__name__)
177
- logger.info("Registering tool", tool_name=tool_name)
178
- self.registry.register_tool(tool_name, tool)
179
- logger.debug("Tool registered successfully")
250
+ @property
251
+ def agents(self) -> dict[str, FlockAgent]:
252
+ """Returns the dictionary of agents managed by this Flock instance."""
253
+ return self._agents
180
254
 
181
255
  def run(
182
256
  self,
183
257
  start_agent: FlockAgent | str | None = None,
184
258
  input: dict = {},
185
- context: FlockContext = None,
259
+ context: FlockContext
260
+ | None = None, # Allow passing initial context state
186
261
  run_id: str = "",
187
- box_result: bool = True,
188
- agents: list[FlockAgent] = [],
262
+ box_result: bool = False, # Changed default to False for raw dict
263
+ agents: list[FlockAgent] | None = None, # Allow adding agents via run
189
264
  ) -> dict:
190
265
  """Entry point for running an agent system synchronously."""
191
266
  return asyncio.run(
192
267
  self.run_async(
193
- start_agent, input, context, run_id, box_result, agents
268
+ start_agent=start_agent,
269
+ input=input,
270
+ context=context,
271
+ run_id=run_id,
272
+ box_result=box_result,
273
+ agents=agents,
194
274
  )
195
275
  )
196
276
 
197
- def save_to_file(
198
- self,
199
- file_path: str,
200
- start_agent: str | None = None,
201
- input: dict | None = None,
202
- ) -> None:
203
- """Save the Flock instance to a file.
204
-
205
- This method serializes the Flock instance to a dictionary using the `to_dict()` method and saves it to a file.
206
- The saved file can be reloaded later using the `from_file()` method.
207
-
208
- Args:
209
- file_path (str): The path to the file where the Flock instance should be saved.
210
- """
211
- hex_str = cloudpickle.dumps(self).hex()
212
-
213
- result = {
214
- "start_agent": start_agent,
215
- "input": input,
216
- "flock": hex_str,
217
- }
218
-
219
- path = os.path.dirname(file_path)
220
- if path:
221
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
222
-
223
- with open(file_path, "w") as file:
224
- file.write(json.dumps(result))
225
-
226
- @staticmethod
227
- def load_from_file(file_path: str) -> "Flock":
228
- """Load a Flock instance from a file.
229
-
230
- This class method deserializes a Flock instance from a file that was previously saved using the `save_to_file()`
231
- method. It reads the file, converts the hexadecimal string back into a Flock instance, and returns it.
232
-
233
- Args:
234
- file_path (str): The path to the file containing the serialized Flock instance.
235
-
236
- Returns:
237
- Flock: A new Flock instance reconstructed from the saved file.
238
- """
239
- with open(file_path) as file:
240
- json_flock = json.load(file)
241
- hex_str = json_flock["flock"]
242
- flock = cloudpickle.loads(bytes.fromhex(hex_str))
243
- if json_flock["start_agent"]:
244
- agent = flock.registry.get_agent(json_flock["start_agent"])
245
- flock.start_agent = agent
246
- if json_flock["input"]:
247
- flock.input = json_flock["input"]
248
- return flock
249
-
250
- def to_dict(self) -> dict[str, Any]:
251
- """Serialize the FlockAgent instance to a dictionary.
252
-
253
- This method converts the entire agent instance—including its configuration, state, and lifecycle hooks—
254
- into a dictionary format. It uses cloudpickle to serialize any callable objects (such as functions or
255
- methods), converting them into hexadecimal string representations. This ensures that the agent can be
256
- easily persisted, transmitted, or logged as JSON.
257
-
258
- The serialization process is recursive:
259
- - If a field is a callable (and not a class), it is serialized using cloudpickle.
260
- - Lists and dictionaries are processed recursively to ensure that all nested callables are properly handled.
261
-
262
- **Returns:**
263
- dict[str, Any]: A dictionary representing the FlockAgent, which includes all of its configuration data.
264
- This dictionary is suitable for storage, debugging, or transmission over the network.
265
-
266
- **Example:**
267
- For an agent defined as:
268
- name = "idea_agent",
269
- model = "openai/gpt-4o",
270
- input = "query: str | The search query, context: dict | The full conversation context",
271
- output = "idea: str | The generated idea"
272
- Calling `agent.to_dict()` might produce:
273
- {
274
- "name": "idea_agent",
275
- "model": "openai/gpt-4o",
276
- "input": "query: str | The search query, context: dict | The full conversation context",
277
- "output": "idea: str | The generated idea",
278
- "tools": ["<serialized tool representation>"],
279
- "use_cache": False,
280
- "hand_off": None,
281
- "termination": None,
282
- ...
283
- }
284
- """
285
-
286
- def convert_callable(obj: Any) -> Any:
287
- if callable(obj) and not isinstance(obj, type):
288
- return cloudpickle.dumps(obj).hex()
289
- if isinstance(obj, list):
290
- return [convert_callable(item) for item in obj]
291
- if isinstance(obj, dict):
292
- return {k: convert_callable(v) for k, v in obj.items()}
293
- return obj
294
-
295
- data = self.model_dump()
296
- return convert_callable(data)
297
-
298
- def start_api(self, host: str = "0.0.0.0", port: int = 8344) -> None:
299
- """Start a REST API server for this Flock instance.
300
-
301
- This method creates a FlockAPI instance for the current Flock and starts the API server.
302
- It provides an easier alternative to manually creating and starting the API.
303
-
304
- Args:
305
- host (str): The host to bind the server to. Defaults to "0.0.0.0".
306
- port (int): The port to bind the server to. Defaults to 8344.
307
- """
308
- from flock.core.flock_api import FlockAPI
309
-
310
- api = FlockAPI(self)
311
- api.start(host=host, port=port)
312
-
313
- @classmethod
314
- def from_dict(cls: type[T], data: dict[str, Any]) -> T:
315
- """Deserialize a FlockAgent instance from a dictionary.
316
-
317
- This class method reconstructs a FlockAgent from its serialized dictionary representation, as produced
318
- by the `to_dict()` method. It recursively processes the dictionary to convert any serialized callables
319
- (stored as hexadecimal strings via cloudpickle) back into executable callable objects.
320
-
321
- **Arguments:**
322
- data (dict[str, Any]): A dictionary representation of a FlockAgent, typically produced by `to_dict()`.
323
- The dictionary should contain all configuration fields and state information necessary to fully
324
- reconstruct the agent.
325
-
326
- **Returns:**
327
- FlockAgent: An instance of FlockAgent reconstructed from the provided dictionary. The deserialized agent
328
- will have the same configuration, state, and behavior as the original instance.
329
-
330
- **Example:**
331
- Suppose you have the following dictionary:
332
- {
333
- "name": "idea_agent",
334
- "model": "openai/gpt-4o",
335
- "input": "query: str | The search query, context: dict | The full conversation context",
336
- "output": "idea: str | The generated idea",
337
- "tools": ["<serialized tool representation>"],
338
- "use_cache": False,
339
- "hand_off": None,
340
- "termination": None,
341
- ...
342
- }
343
- Then, calling:
344
- agent = FlockAgent.from_dict(data)
345
- will return a FlockAgent instance with the same properties and behavior as when it was originally serialized.
346
- """
347
-
348
- def convert_callable(obj: Any) -> Any:
349
- if isinstance(obj, str) and len(obj) > 2:
350
- try:
351
- return cloudpickle.loads(bytes.fromhex(obj))
352
- except Exception:
353
- return obj
354
- if isinstance(obj, list):
355
- return [convert_callable(item) for item in obj]
356
- if isinstance(obj, dict):
357
- return {k: convert_callable(v) for k, v in obj.items()}
358
- return obj
359
-
360
- converted = convert_callable(data)
361
- return cls(**converted)
362
-
363
277
  async def run_async(
364
278
  self,
365
279
  start_agent: FlockAgent | str | None = None,
366
- input: dict = {},
367
- context: FlockContext = None,
280
+ input: dict | None = None,
281
+ context: FlockContext | None = None,
368
282
  run_id: str = "",
369
- box_result: bool = True,
370
- agents: list[FlockAgent] = [],
283
+ box_result: bool = False, # Changed default
284
+ agents: list[FlockAgent] | None = None, # Allow adding agents via run
371
285
  ) -> dict:
372
- """Entry point for running an agent system asynchronously.
373
-
374
- This method performs the following steps:
375
- 1. If a string is provided for start_agent, it looks up the agent in the registry.
376
- 2. Optionally uses a provided global context.
377
- 3. Generates a unique run ID if one is not provided.
378
- 4. Initializes the context with standard variables (like agent name, input data, run ID, and debug flag).
379
- 5. Executes the agent workflow either locally (for debugging) or via Temporal (for production).
380
-
381
- Args:
382
- start_agent (FlockAgent | str): The agent instance or the name of the agent to start the workflow.
383
- input (dict): A dictionary of input values required by the agent.
384
- context (FlockContext, optional): A FlockContext instance to use. If not provided, a default context is used.
385
- run_id (str, optional): A unique identifier for this run. If empty, one is generated automatically.
386
- box_result (bool, optional): If True, wraps the output in a Box for nicer formatting. Defaults to True.
387
- agents (list, optional): additional way to add agents to flock instead of add_agent
388
-
389
- Returns:
390
- dict: A dictionary containing the result of the agent workflow execution.
391
-
392
- Raises:
393
- ValueError: If the specified agent is not found in the registry.
394
- Exception: For any other errors encountered during execution.
395
- """
396
- with tracer.start_as_current_span("run_async") as span:
397
- if isinstance(start_agent, str):
398
- start_agent = self.registry.get_agent(start_agent)
399
- span.set_attribute(
400
- "start_agent",
401
- start_agent.name
402
- if hasattr(start_agent, "name")
403
- else start_agent,
404
- )
405
- for agent in agents:
406
- self.add_agent(agent)
286
+ """Entry point for running an agent system asynchronously."""
287
+ # This import needs to be here or handled carefully due to potential cycles
288
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
289
+
290
+ with tracer.start_as_current_span("flock.run_async") as span:
291
+ # Add passed agents first
292
+ if agents:
293
+ for agent_obj in agents:
294
+ if isinstance(agent_obj, ConcreteFlockAgent):
295
+ self.add_agent(
296
+ agent_obj
297
+ ) # Adds to self._agents and registry
298
+ else:
299
+ logger.warning(
300
+ f"Item in 'agents' list is not a FlockAgent: {type(agent_obj)}"
301
+ )
407
302
 
408
- if start_agent:
409
- self.start_agent = start_agent
410
- if input:
411
- self.input = input
303
+ # Determine starting agent name
304
+ start_agent_name: str | None = None
305
+ if isinstance(start_agent, ConcreteFlockAgent):
306
+ start_agent_name = start_agent.name
307
+ if start_agent_name not in self._agents:
308
+ self.add_agent(
309
+ start_agent
310
+ ) # Add if instance was passed but not added
311
+ elif isinstance(start_agent, str):
312
+ start_agent_name = start_agent
313
+ else:
314
+ start_agent_name = (
315
+ self._start_agent_name
316
+ ) # Use pre-configured if any
317
+
318
+ # Default to first agent if only one exists and none specified
319
+ if not start_agent_name and len(self._agents) == 1:
320
+ start_agent_name = list(self._agents.keys())[0]
321
+ elif not start_agent_name:
322
+ raise ValueError(
323
+ "No start_agent specified and multiple agents exist or none are added."
324
+ )
412
325
 
413
- span.set_attribute("input", str(self.input))
414
- span.set_attribute("context", str(context))
326
+ # Get starting input
327
+ run_input = input if input is not None else self._start_input
328
+
329
+ # Log and trace start info
330
+ span.set_attribute("start_agent", start_agent_name)
331
+ span.set_attribute("input", str(run_input))
415
332
  span.set_attribute("run_id", run_id)
416
- span.set_attribute("box_result", box_result)
333
+ span.set_attribute("enable_temporal", self.enable_temporal)
334
+ logger.info(
335
+ f"Initiating Flock run. Start Agent: '{start_agent_name}'. Temporal: {self.enable_temporal}."
336
+ )
417
337
 
418
338
  try:
419
- if isinstance(self.start_agent, str):
420
- logger.debug(
421
- f"Looking up agent '{self.start_agent.name}' in registry",
422
- agent_name=self.start_agent,
339
+ # Resolve start agent instance from internal dict
340
+ resolved_start_agent = self._agents.get(start_agent_name)
341
+ if not resolved_start_agent:
342
+ # Maybe it's only in the global registry? (Less common)
343
+ resolved_start_agent = FlockRegistry.get_agent(
344
+ start_agent_name
423
345
  )
424
- self.start_agent = self.registry.get_agent(self.start_agent)
425
- if not self.start_agent:
426
- logger.error(
427
- "Agent not found", agent_name=self.start_agent
428
- )
346
+ if not resolved_start_agent:
429
347
  raise ValueError(
430
- f"Agent '{self.start_agent}' not found in registry"
348
+ f"Start agent '{start_agent_name}' not found in Flock instance or registry."
431
349
  )
432
- self.start_agent.resolve_callables(context=self.context)
433
- if context:
434
- logger.debug("Using provided context")
435
- self.context = context
436
- if not run_id:
437
- run_id = f"flock_{uuid.uuid4().hex[:4]}"
438
- logger.debug(f"Generated run ID '{run_id}'", run_id=run_id)
439
-
440
- set_baggage("run_id", run_id)
441
-
442
- # TODO - Add a check for required input keys
443
- input_keys = top_level_to_keys(self.start_agent.input)
444
- for key in input_keys:
445
- if key.startswith("flock."):
446
- key = key[6:] # Remove the "flock." prefix
447
- if key not in self.input:
448
- from rich.prompt import Prompt
350
+ else:
351
+ # If found globally, add it to this instance for consistency during run
352
+ self.add_agent(resolved_start_agent)
449
353
 
450
- self.input[key] = Prompt.ask(
451
- f"Please enter {key} for {self.start_agent.name}"
452
- )
354
+ # Create or use provided context
355
+ run_context = context if context else FlockContext()
356
+ if not run_id:
357
+ run_id = f"flockrun_{uuid.uuid4().hex[:8]}"
358
+ set_baggage("run_id", run_id) # Ensure run_id is in baggage
453
359
 
454
- # Initialize the context with standardized variables
360
+ # Initialize context
455
361
  initialize_context(
456
- self.context,
457
- self.start_agent.name,
458
- self.input,
362
+ run_context,
363
+ start_agent_name,
364
+ run_input,
459
365
  run_id,
460
366
  not self.enable_temporal,
461
- self.model,
367
+ self.model
368
+ or resolved_start_agent.model
369
+ or "default-model-missing", # Pass effective model
462
370
  )
463
371
 
372
+ # Execute workflow
464
373
  logger.info(
465
374
  "Starting agent execution",
466
- agent=self.start_agent.name,
375
+ agent=start_agent_name,
467
376
  enable_temporal=self.enable_temporal,
468
377
  )
469
378
 
470
379
  if not self.enable_temporal:
471
- return await run_local_workflow(self.context, box_result)
380
+ result = await run_local_workflow(
381
+ run_context, box_result=False
382
+ ) # Get raw dict
472
383
  else:
473
- return await run_temporal_workflow(self.context, box_result)
384
+ result = await run_temporal_workflow(
385
+ run_context, box_result=False
386
+ ) # Get raw dict
387
+
388
+ span.set_attribute("result.type", str(type(result)))
389
+ # Avoid overly large results in trace attributes
390
+ result_str = str(result)
391
+ if len(result_str) > 1000:
392
+ result_str = result_str[:1000] + "... (truncated)"
393
+ span.set_attribute("result.preview", result_str)
394
+
395
+ # Optionally box result before returning
396
+ if box_result:
397
+ try:
398
+ from box import Box
399
+
400
+ logger.debug("Boxing final result.")
401
+ return Box(result)
402
+ except ImportError:
403
+ logger.warning(
404
+ "Box library not installed, returning raw dict. Install with 'pip install python-box'"
405
+ )
406
+ return result
407
+ else:
408
+ return result
409
+
410
+ except Exception as e:
411
+ logger.error(f"Flock run failed: {e}", exc_info=True)
412
+ span.record_exception(e)
413
+ span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
414
+ # Depending on desired behavior, either raise or return an error dict
415
+ # raise # Option 1: Let the exception propagate
416
+ return {
417
+ "error": str(e),
418
+ "details": "Flock run failed.",
419
+ } # Option 2: Return error dict
420
+
421
+ # --- ADDED Serialization Methods ---
422
+
423
+ def to_dict(self) -> dict[str, Any]:
424
+ """Convert Flock instance to dictionary representation."""
425
+ logger.debug("Serializing Flock instance to dict.")
426
+ # Use Pydantic's dump for base fields
427
+ data = self.model_dump(mode="json", exclude_none=True)
428
+
429
+ # Manually add serialized agents
430
+ data["agents"] = {}
431
+ for name, agent_instance in self._agents.items():
432
+ try:
433
+ # Agents handle their own serialization via their to_dict
434
+ data["agents"][name] = agent_instance.to_dict()
474
435
  except Exception as e:
475
- logger.exception("Execution failed", error=str(e))
476
- raise
436
+ logger.error(
437
+ f"Failed to serialize agent '{name}' within Flock: {e}"
438
+ )
439
+ # Optionally skip problematic agents or raise error
440
+ # data["agents"][name] = {"error": f"Serialization failed: {e}"}
441
+
442
+ # Exclude runtime fields that shouldn't be serialized
443
+ # These are not Pydantic fields, so they aren't dumped by model_dump
444
+ # No need to explicitly remove _start_agent_name, _start_input unless added manually
445
+
446
+ # Filter final dict (optional, Pydantic's exclude_none helps)
447
+ # return self._filter_none_values(data)
448
+ return data
449
+
450
+ @classmethod
451
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
452
+ """Create Flock instance from dictionary representation."""
453
+ logger.debug(
454
+ f"Deserializing Flock from dict. Provided keys: {list(data.keys())}"
455
+ )
456
+
457
+ # Ensure FlockAgent is importable for type checking later
458
+ try:
459
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
460
+ except ImportError:
461
+ logger.error(
462
+ "Cannot import FlockAgent, deserialization may fail for agents."
463
+ )
464
+ ConcreteFlockAgent = Any # Fallback
465
+
466
+ # Extract agent data before initializing Flock base model
467
+ agents_data = data.pop("agents", {})
468
+
469
+ # Create Flock instance using Pydantic constructor for basic fields
470
+ try:
471
+ # Pass only fields defined in Flock's Pydantic model
472
+ init_data = {k: v for k, v in data.items() if k in cls.model_fields}
473
+ flock_instance = cls(**init_data)
474
+ except Exception as e:
475
+ logger.error(
476
+ f"Pydantic validation/init failed for Flock: {e}", exc_info=True
477
+ )
478
+ raise ValueError(
479
+ f"Failed to initialize Flock from dict: {e}"
480
+ ) from e
481
+
482
+ # Deserialize and add agents AFTER Flock instance exists
483
+ for name, agent_data in agents_data.items():
484
+ try:
485
+ # Ensure agent_data has the name, or add it from the key
486
+ agent_data.setdefault("name", name)
487
+ # Use FlockAgent's from_dict method
488
+ agent_instance = ConcreteFlockAgent.from_dict(agent_data)
489
+ flock_instance.add_agent(
490
+ agent_instance
491
+ ) # Adds to _agents and registers
492
+ except Exception as e:
493
+ logger.error(
494
+ f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
495
+ exc_info=True,
496
+ )
497
+ # Decide: skip agent or raise error?
498
+
499
+ logger.info("Successfully deserialized Flock instance.")
500
+ return flock_instance
501
+
502
+ # --- API Start Method ---
503
+ def start_api(
504
+ self,
505
+ host: str = "127.0.0.1",
506
+ port: int = 8344,
507
+ server_name: str = "Flock API",
508
+ create_ui: bool = False,
509
+ ) -> None:
510
+ """Start a REST API server for this Flock instance."""
511
+ # Import locally to avoid making API components a hard dependency
512
+ try:
513
+ from flock.core.api import FlockAPI
514
+ except ImportError:
515
+ logger.error(
516
+ "API components not found. Cannot start API. "
517
+ "Ensure 'fastapi' and 'uvicorn' are installed."
518
+ )
519
+ return
520
+
521
+ logger.info(
522
+ f"Preparing to start API server on {host}:{port} {'with UI' if create_ui else 'without UI'}"
523
+ )
524
+ api_instance = FlockAPI(self) # Pass the current Flock instance
525
+ # Use the start method of FlockAPI
526
+ api_instance.start(
527
+ host=host, port=port, server_name=server_name, create_ui=create_ui
528
+ )
529
+
530
+ # --- Static Method Loaders (Keep for convenience) ---
531
+ @staticmethod
532
+ def load_from_file(file_path: str) -> Flock:
533
+ """Load a Flock instance from various file formats (detects type)."""
534
+ p = Path(file_path)
535
+ if not p.exists():
536
+ raise FileNotFoundError(f"Flock file not found: {file_path}")
537
+
538
+ if p.suffix in [".yaml", ".yml"]:
539
+ return Flock.from_yaml_file(p)
540
+ elif p.suffix == ".json":
541
+ return Flock.from_json(p.read_text())
542
+ elif p.suffix == ".msgpack":
543
+ return Flock.from_msgpack_file(p)
544
+ elif p.suffix == ".pkl":
545
+ if PICKLE_AVAILABLE:
546
+ return Flock.from_pickle_file(p)
547
+ else:
548
+ raise RuntimeError(
549
+ "Cannot load Pickle file: cloudpickle not installed."
550
+ )
551
+ else:
552
+ raise ValueError(
553
+ f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
554
+ )