flock-core 0.3.23__py3-none-any.whl → 0.3.31__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 (38) hide show
  1. flock/__init__.py +23 -11
  2. flock/cli/constants.py +2 -4
  3. flock/cli/create_flock.py +220 -1
  4. flock/cli/execute_flock.py +200 -0
  5. flock/cli/load_flock.py +27 -7
  6. flock/cli/loaded_flock_cli.py +202 -0
  7. flock/cli/manage_agents.py +443 -0
  8. flock/cli/view_results.py +29 -0
  9. flock/cli/yaml_editor.py +283 -0
  10. flock/core/__init__.py +2 -2
  11. flock/core/api/__init__.py +11 -0
  12. flock/core/api/endpoints.py +222 -0
  13. flock/core/api/main.py +237 -0
  14. flock/core/api/models.py +34 -0
  15. flock/core/api/run_store.py +72 -0
  16. flock/core/api/ui/__init__.py +0 -0
  17. flock/core/api/ui/routes.py +271 -0
  18. flock/core/api/ui/utils.py +119 -0
  19. flock/core/flock.py +509 -388
  20. flock/core/flock_agent.py +384 -121
  21. flock/core/flock_registry.py +532 -0
  22. flock/core/logging/logging.py +97 -23
  23. flock/core/mixin/dspy_integration.py +363 -158
  24. flock/core/serialization/__init__.py +7 -1
  25. flock/core/serialization/callable_registry.py +52 -0
  26. flock/core/serialization/serializable.py +259 -37
  27. flock/core/serialization/serialization_utils.py +199 -0
  28. flock/evaluators/declarative/declarative_evaluator.py +2 -0
  29. flock/modules/memory/memory_module.py +17 -4
  30. flock/modules/output/output_module.py +9 -3
  31. flock/workflow/activities.py +2 -2
  32. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/METADATA +6 -3
  33. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/RECORD +36 -22
  34. flock/core/flock_api.py +0 -214
  35. flock/core/registry/agent_registry.py +0 -120
  36. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/WHEEL +0 -0
  37. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/entry_points.txt +0 -0
  38. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/licenses/LICENSE +0 -0
flock/core/flock.py CHANGED
@@ -1,476 +1,597 @@
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
+ name: str = Field(
77
+ default_factory=lambda: f"flock_{uuid.uuid4().hex[:8]}",
78
+ description="A unique identifier for this Flock instance.",
79
+ )
80
+ model: str | None = Field(
81
+ default="openai/gpt-4o",
82
+ description="Default model identifier to be used for agents if not specified otherwise.",
83
+ )
84
+ description: str | None = Field(
85
+ default=None,
86
+ description="A brief description of the purpose of this Flock configuration.",
87
+ )
88
+ enable_temporal: bool = Field(
89
+ default=False,
90
+ description="If True, execute workflows via Temporal; otherwise, run locally.",
91
+ )
92
+ # --- Runtime Attributes (Excluded from Serialization) ---
93
+ # Store agents internally but don't make it part of the Pydantic model definition
94
+ # Use a regular attribute, initialized in __init__
95
+ # Pydantic V2 handles __init__ and attributes not in Field correctly
96
+ _agents: dict[str, FlockAgent]
97
+ _start_agent_name: str | None
98
+ _start_input: dict
99
+
100
+ # Pydantic v2 model config
101
+ model_config = {
102
+ "arbitrary_types_allowed": True,
103
+ "ignored_types": (
104
+ type(FlockRegistry),
105
+ ), # Prevent validation issues with registry
106
+ # No need to exclude fields here, handled in to_dict
107
+ }
108
+
74
109
  def __init__(
75
110
  self,
76
- model: str = "openai/gpt-4o",
111
+ model: str | None = "openai/gpt-4o",
112
+ description: str | None = None,
77
113
  enable_temporal: bool = False,
78
- enable_logging: bool | list[str] = False,
114
+ enable_logging: bool
115
+ | list[str] = False, # Keep logging control at init
116
+ agents: list[FlockAgent] | None = None, # Allow passing agents at init
117
+ **kwargs, # Allow extra fields during init if needed, Pydantic handles it
79
118
  ):
80
- """Initialize the Flock orchestrator.
119
+ """Initialize the Flock orchestrator."""
120
+ # Initialize Pydantic fields
121
+ super().__init__(
122
+ model=model,
123
+ description=description,
124
+ enable_temporal=enable_temporal,
125
+ **kwargs, # Pass extra kwargs to Pydantic BaseModel
126
+ )
81
127
 
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)
128
+ # Initialize runtime attributes AFTER super().__init__()
129
+ self._agents = {}
130
+ self._start_agent_name = None
131
+ self._start_input = {}
92
132
 
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:
116
- 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")
133
+ # Set up logging
134
+ self._configure_logging(enable_logging)
121
135
 
122
- def add_agent(self, agent: T) -> T:
123
- """Add a new agent to the Flock system.
136
+ # Register passed agents
137
+ if agents:
138
+ # Ensure FlockAgent type is available for isinstance check
139
+ # This import might need to be deferred or handled carefully if it causes issues
140
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
124
141
 
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.
142
+ for agent in agents:
143
+ if isinstance(agent, ConcreteFlockAgent):
144
+ self.add_agent(agent)
145
+ else:
146
+ logger.warning(
147
+ f"Item provided in 'agents' list is not a FlockAgent: {type(agent)}"
148
+ )
128
149
 
129
- Args:
130
- agent (FlockAgent): The agent instance to add.
150
+ # Initialize console if needed
151
+ init_console()
131
152
 
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
- logger.debug(
140
- f"Using default model for agent {agent.name}",
141
- model=self.model,
142
- )
153
+ # Set Temporal debug environment variable
154
+ self._set_temporal_debug_flag()
143
155
 
144
- if agent.name in self.agents:
145
- logger.warning(
146
- f"Agent {agent.name} already exists, returning existing instance"
156
+ # Ensure session ID exists in baggage
157
+ self._ensure_session_id()
158
+
159
+ logger.info(
160
+ "Flock instance initialized",
161
+ model=self.model,
162
+ enable_temporal=self.enable_temporal,
163
+ )
164
+
165
+ # --- Keep _configure_logging, _set_temporal_debug_flag, _ensure_session_id ---
166
+ # ... (implementation as before) ...
167
+ def _configure_logging(self, enable_logging: bool | list[str]):
168
+ """Configure logging levels based on the enable_logging flag."""
169
+ logger.debug(f"Configuring logging, enable_logging={enable_logging}")
170
+ is_enabled_globally = False
171
+ enabled_loggers = []
172
+
173
+ if isinstance(enable_logging, bool):
174
+ is_enabled_globally = enable_logging
175
+ elif isinstance(enable_logging, list):
176
+ is_enabled_globally = bool(
177
+ enable_logging
178
+ ) # Enable if list is not empty
179
+ enabled_loggers = enable_logging
180
+
181
+ # Configure core loggers
182
+ for log_name in LOGGERS:
183
+ log_instance = get_logger(log_name)
184
+ if is_enabled_globally or log_name in enabled_loggers:
185
+ log_instance.enable_logging = True
186
+ else:
187
+ log_instance.enable_logging = False
188
+
189
+ # Configure module loggers (existing ones)
190
+ module_loggers = get_module_loggers()
191
+ for mod_log in module_loggers:
192
+ if is_enabled_globally or mod_log.name in enabled_loggers:
193
+ mod_log.enable_logging = True
194
+ else:
195
+ mod_log.enable_logging = False
196
+
197
+ def _set_temporal_debug_flag(self):
198
+ """Set or remove LOCAL_DEBUG env var based on enable_temporal."""
199
+ if not self.enable_temporal:
200
+ if "LOCAL_DEBUG" not in os.environ:
201
+ os.environ["LOCAL_DEBUG"] = "1"
202
+ logger.debug(
203
+ "Set LOCAL_DEBUG environment variable for local execution."
147
204
  )
148
- return self.agents[agent.name]
149
- logger.info(f"Adding new agent '{agent.name}'")
205
+ elif "LOCAL_DEBUG" in os.environ:
206
+ del os.environ["LOCAL_DEBUG"]
207
+ logger.debug(
208
+ "Removed LOCAL_DEBUG environment variable for Temporal execution."
209
+ )
150
210
 
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()
211
+ def _ensure_session_id(self):
212
+ """Ensure a session_id exists in the OpenTelemetry baggage."""
213
+ session_id = get_baggage("session_id")
214
+ if not session_id:
215
+ session_id = str(uuid.uuid4())
216
+ set_baggage("session_id", session_id)
217
+ logger.debug(f"Generated new session_id: {session_id}")
218
+
219
+ # --- Keep add_agent, agents property, run, run_async ---
220
+ # ... (implementation as before, ensuring FlockAgent type hint is handled) ...
221
+ def add_agent(self, agent: FlockAgent) -> FlockAgent:
222
+ """Adds an agent instance to this Flock configuration."""
223
+ # Ensure FlockAgent type is available for isinstance check
224
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
225
+
226
+ if not isinstance(agent, ConcreteFlockAgent):
227
+ raise TypeError("Provided object is not a FlockAgent instance.")
228
+ if not agent.name:
229
+ raise ValueError("Agent must have a name.")
230
+
231
+ if agent.name in self._agents:
232
+ logger.warning(
233
+ f"Agent '{agent.name}' already exists in this Flock instance. Overwriting."
155
234
  )
235
+ self._agents[agent.name] = agent
236
+ FlockRegistry.register_agent(agent) # Also register globally
156
237
 
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
238
+ # Set default model if agent doesn't have one
239
+ if agent.model is None:
240
+ # agent.set_model(self.model) # Use Flock's default model
241
+ if self.model: # Ensure Flock has a model defined
242
+ agent.set_model(self.model)
243
+ logger.debug(
244
+ f"Agent '{agent.name}' using Flock default model: {self.model}"
245
+ )
246
+ else:
247
+ logger.warning(
248
+ f"Agent '{agent.name}' has no model and Flock default model is not set."
249
+ )
166
250
 
167
- def add_tool(self, tool_name: str, tool: callable):
168
- """Register a tool with the Flock system.
251
+ logger.info(f"Agent '{agent.name}' added to Flock.")
252
+ return agent
169
253
 
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")
254
+ @property
255
+ def agents(self) -> dict[str, FlockAgent]:
256
+ """Returns the dictionary of agents managed by this Flock instance."""
257
+ return self._agents
180
258
 
181
259
  def run(
182
260
  self,
183
261
  start_agent: FlockAgent | str | None = None,
184
262
  input: dict = {},
185
- context: FlockContext = None,
263
+ context: FlockContext
264
+ | None = None, # Allow passing initial context state
186
265
  run_id: str = "",
187
- box_result: bool = True,
188
- agents: list[FlockAgent] = [],
266
+ box_result: bool = False, # Changed default to False for raw dict
267
+ agents: list[FlockAgent] | None = None, # Allow adding agents via run
189
268
  ) -> dict:
190
269
  """Entry point for running an agent system synchronously."""
191
270
  return asyncio.run(
192
271
  self.run_async(
193
- start_agent, input, context, run_id, box_result, agents
272
+ start_agent=start_agent,
273
+ input=input,
274
+ context=context,
275
+ run_id=run_id,
276
+ box_result=box_result,
277
+ agents=agents,
194
278
  )
195
279
  )
196
280
 
197
- def save_to_file(
281
+ async def run_async(
198
282
  self,
199
- file_path: str,
200
- start_agent: str | None = None,
283
+ start_agent: FlockAgent | str | None = None,
201
284
  input: dict | None = None,
202
- ) -> None:
203
- """Save the Flock instance to a file.
285
+ context: FlockContext | None = None,
286
+ run_id: str = "",
287
+ box_result: bool = False, # Changed default
288
+ agents: list[FlockAgent] | None = None, # Allow adding agents via run
289
+ ) -> dict:
290
+ """Entry point for running an agent system asynchronously."""
291
+ # This import needs to be here or handled carefully due to potential cycles
292
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
293
+
294
+ with tracer.start_as_current_span("flock.run_async") as span:
295
+ # Add passed agents first
296
+ if agents:
297
+ for agent_obj in agents:
298
+ if isinstance(agent_obj, ConcreteFlockAgent):
299
+ self.add_agent(
300
+ agent_obj
301
+ ) # Adds to self._agents and registry
302
+ else:
303
+ logger.warning(
304
+ f"Item in 'agents' list is not a FlockAgent: {type(agent_obj)}"
305
+ )
204
306
 
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.
307
+ # Determine starting agent name
308
+ start_agent_name: str | None = None
309
+ if isinstance(start_agent, ConcreteFlockAgent):
310
+ start_agent_name = start_agent.name
311
+ if start_agent_name not in self._agents:
312
+ self.add_agent(
313
+ start_agent
314
+ ) # Add if instance was passed but not added
315
+ elif isinstance(start_agent, str):
316
+ start_agent_name = start_agent
317
+ else:
318
+ start_agent_name = (
319
+ self._start_agent_name
320
+ ) # Use pre-configured if any
321
+
322
+ # Default to first agent if only one exists and none specified
323
+ if not start_agent_name and len(self._agents) == 1:
324
+ start_agent_name = list(self._agents.keys())[0]
325
+ elif not start_agent_name:
326
+ raise ValueError(
327
+ "No start_agent specified and multiple agents exist or none are added."
328
+ )
207
329
 
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()
330
+ # Get starting input
331
+ run_input = input if input is not None else self._start_input
212
332
 
213
- result = {
214
- "start_agent": start_agent,
215
- "input": input,
216
- "flock": hex_str,
217
- }
333
+ # Log and trace start info
334
+ span.set_attribute("start_agent", start_agent_name)
335
+ span.set_attribute("input", str(run_input))
336
+ span.set_attribute("run_id", run_id)
337
+ span.set_attribute("enable_temporal", self.enable_temporal)
338
+ logger.info(
339
+ f"Initiating Flock run. Start Agent: '{start_agent_name}'. Temporal: {self.enable_temporal}."
340
+ )
218
341
 
219
- path = os.path.dirname(file_path)
220
- if path:
221
- os.makedirs(os.path.dirname(file_path), exist_ok=True)
342
+ try:
343
+ # Resolve start agent instance from internal dict
344
+ resolved_start_agent = self._agents.get(start_agent_name)
345
+ if not resolved_start_agent:
346
+ # Maybe it's only in the global registry? (Less common)
347
+ resolved_start_agent = FlockRegistry.get_agent(
348
+ start_agent_name
349
+ )
350
+ if not resolved_start_agent:
351
+ raise ValueError(
352
+ f"Start agent '{start_agent_name}' not found in Flock instance or registry."
353
+ )
354
+ else:
355
+ # If found globally, add it to this instance for consistency during run
356
+ self.add_agent(resolved_start_agent)
222
357
 
223
- with open(file_path, "w") as file:
224
- file.write(json.dumps(result))
358
+ # Create or use provided context
359
+ run_context = context if context else FlockContext()
360
+ if not run_id:
361
+ run_id = f"flockrun_{uuid.uuid4().hex[:8]}"
362
+ set_baggage("run_id", run_id) # Ensure run_id is in baggage
225
363
 
226
- @staticmethod
227
- def load_from_file(file_path: str) -> "Flock":
228
- """Load a Flock instance from a file.
364
+ # Initialize context
365
+ initialize_context(
366
+ run_context,
367
+ start_agent_name,
368
+ run_input,
369
+ run_id,
370
+ not self.enable_temporal,
371
+ self.model
372
+ or resolved_start_agent.model
373
+ or "default-model-missing", # Pass effective model
374
+ )
229
375
 
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.
376
+ # Execute workflow
377
+ logger.info(
378
+ "Starting agent execution",
379
+ agent=start_agent_name,
380
+ enable_temporal=self.enable_temporal,
381
+ )
232
382
 
233
- Args:
234
- file_path (str): The path to the file containing the serialized Flock instance.
383
+ if not self.enable_temporal:
384
+ result = await run_local_workflow(
385
+ run_context, box_result=False
386
+ ) # Get raw dict
387
+ else:
388
+ result = await run_temporal_workflow(
389
+ run_context, box_result=False
390
+ ) # Get raw dict
391
+
392
+ span.set_attribute("result.type", str(type(result)))
393
+ # Avoid overly large results in trace attributes
394
+ result_str = str(result)
395
+ if len(result_str) > 1000:
396
+ result_str = result_str[:1000] + "... (truncated)"
397
+ span.set_attribute("result.preview", result_str)
398
+
399
+ # Optionally box result before returning
400
+ if box_result:
401
+ try:
402
+ from box import Box
403
+
404
+ logger.debug("Boxing final result.")
405
+ return Box(result)
406
+ except ImportError:
407
+ logger.warning(
408
+ "Box library not installed, returning raw dict. Install with 'pip install python-box'"
409
+ )
410
+ return result
411
+ else:
412
+ return result
235
413
 
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
414
+ except Exception as e:
415
+ logger.error(f"Flock run failed: {e}", exc_info=True)
416
+ span.record_exception(e)
417
+ span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
418
+ # Depending on desired behavior, either raise or return an error dict
419
+ # raise # Option 1: Let the exception propagate
420
+ return {
421
+ "error": str(e),
422
+ "details": "Flock run failed.",
423
+ } # Option 2: Return error dict
424
+
425
+ # --- ADDED Serialization Methods ---
249
426
 
250
427
  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
- """
428
+ """Convert Flock instance to dictionary representation."""
429
+ logger.debug("Serializing Flock instance to dict.")
430
+ # Use Pydantic's dump for base fields
431
+ data = self.model_dump(mode="json", exclude_none=True)
432
+
433
+ # Manually add serialized agents
434
+ data["agents"] = {}
435
+ for name, agent_instance in self._agents.items():
436
+ try:
437
+ # Agents handle their own serialization via their to_dict
438
+ data["agents"][name] = agent_instance.to_dict()
439
+ except Exception as e:
440
+ logger.error(
441
+ f"Failed to serialize agent '{name}' within Flock: {e}"
442
+ )
443
+ # Optionally skip problematic agents or raise error
444
+ # data["agents"][name] = {"error": f"Serialization failed: {e}"}
285
445
 
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
446
+ # Exclude runtime fields that shouldn't be serialized
447
+ # These are not Pydantic fields, so they aren't dumped by model_dump
448
+ # No need to explicitly remove _start_agent_name, _start_input unless added manually
294
449
 
295
- data = self.model_dump()
296
- return convert_callable(data)
450
+ # Filter final dict (optional, Pydantic's exclude_none helps)
451
+ # return self._filter_none_values(data)
452
+ return data
297
453
 
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.
454
+ @classmethod
455
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
456
+ """Create Flock instance from dictionary representation."""
457
+ logger.debug(
458
+ f"Deserializing Flock from dict. Provided keys: {list(data.keys())}"
459
+ )
300
460
 
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.
461
+ # Ensure FlockAgent is importable for type checking later
462
+ try:
463
+ from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
464
+ except ImportError:
465
+ logger.error(
466
+ "Cannot import FlockAgent, deserialization may fail for agents."
467
+ )
468
+ ConcreteFlockAgent = Any # Fallback
469
+
470
+ # Extract agent data before initializing Flock base model
471
+ agents_data = data.pop("agents", {})
472
+
473
+ # Create Flock instance using Pydantic constructor for basic fields
474
+ try:
475
+ # Pass only fields defined in Flock's Pydantic model
476
+ init_data = {k: v for k, v in data.items() if k in cls.model_fields}
477
+ flock_instance = cls(**init_data)
478
+ except Exception as e:
479
+ logger.error(
480
+ f"Pydantic validation/init failed for Flock: {e}", exc_info=True
481
+ )
482
+ raise ValueError(
483
+ f"Failed to initialize Flock from dict: {e}"
484
+ ) from e
303
485
 
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
486
+ # Deserialize and add agents AFTER Flock instance exists
487
+ for name, agent_data in agents_data.items():
488
+ try:
489
+ # Ensure agent_data has the name, or add it from the key
490
+ agent_data.setdefault("name", name)
491
+ # Use FlockAgent's from_dict method
492
+ agent_instance = ConcreteFlockAgent.from_dict(agent_data)
493
+ flock_instance.add_agent(
494
+ agent_instance
495
+ ) # Adds to _agents and registers
496
+ except Exception as e:
497
+ logger.error(
498
+ f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
499
+ exc_info=True,
500
+ )
501
+ # Decide: skip agent or raise error?
309
502
 
310
- api = FlockAPI(self)
311
- api.start(host=host, port=port)
503
+ logger.info("Successfully deserialized Flock instance.")
504
+ return flock_instance
312
505
 
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
- """
506
+ # --- API Start Method ---
507
+ def start_api(
508
+ self,
509
+ host: str = "127.0.0.1",
510
+ port: int = 8344,
511
+ server_name: str = "Flock API",
512
+ create_ui: bool = False,
513
+ ) -> None:
514
+ """Start a REST API server for this Flock instance."""
515
+ # Import locally to avoid making API components a hard dependency
516
+ try:
517
+ from flock.core.api import FlockAPI
518
+ except ImportError:
519
+ logger.error(
520
+ "API components not found. Cannot start API. "
521
+ "Ensure 'fastapi' and 'uvicorn' are installed."
522
+ )
523
+ return
347
524
 
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)
525
+ logger.info(
526
+ f"Preparing to start API server on {host}:{port} {'with UI' if create_ui else 'without UI'}"
527
+ )
528
+ api_instance = FlockAPI(self) # Pass the current Flock instance
529
+ # Use the start method of FlockAPI
530
+ api_instance.start(
531
+ host=host, port=port, server_name=server_name, create_ui=create_ui
532
+ )
362
533
 
363
- async def run_async(
534
+ # --- CLI Start Method ---
535
+ def start_cli(
364
536
  self,
365
- start_agent: FlockAgent | str | None = None,
366
- input: dict = {},
367
- context: FlockContext = None,
368
- run_id: str = "",
369
- box_result: bool = True,
370
- agents: list[FlockAgent] = [],
371
- ) -> dict:
372
- """Entry point for running an agent system asynchronously.
537
+ server_name: str = "Flock CLI",
538
+ show_results: bool = False,
539
+ edit_mode: bool = False,
540
+ ) -> None:
541
+ """Start a CLI interface for this Flock instance.
373
542
 
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).
543
+ This method loads the CLI with the current Flock instance already available,
544
+ allowing users to execute, edit, or manage agents from the existing configuration.
380
545
 
381
546
  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.
547
+ server_name: Optional name for the CLI interface
548
+ show_results: Whether to initially show results of previous runs
549
+ edit_mode: Whether to open directly in edit mode
395
550
  """
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,
551
+ # Import locally to avoid circular imports
552
+ try:
553
+ from flock.cli.loaded_flock_cli import start_loaded_flock_cli
554
+ except ImportError:
555
+ logger.error(
556
+ "CLI components not found. Cannot start CLI. "
557
+ "Ensure the CLI modules are properly installed."
404
558
  )
405
- for agent in agents:
406
- self.add_agent(agent)
407
-
408
- if start_agent:
409
- self.start_agent = start_agent
410
- if input:
411
- self.input = input
412
-
413
- span.set_attribute("input", str(self.input))
414
- span.set_attribute("context", str(context))
415
- span.set_attribute("run_id", run_id)
416
- span.set_attribute("box_result", box_result)
417
-
418
- 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,
423
- )
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
- )
429
- raise ValueError(
430
- f"Agent '{self.start_agent}' not found in registry"
431
- )
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
559
+ return
449
560
 
450
- self.input[key] = Prompt.ask(
451
- f"Please enter {key} for {self.start_agent.name}"
452
- )
561
+ logger.info(
562
+ f"Starting CLI interface with loaded Flock instance ({len(self._agents)} agents)"
563
+ )
453
564
 
454
- # Initialize the context with standardized variables
455
- initialize_context(
456
- self.context,
457
- self.start_agent.name,
458
- self.input,
459
- run_id,
460
- not self.enable_temporal,
461
- self.model,
462
- )
565
+ # Pass the current Flock instance to the CLI
566
+ start_loaded_flock_cli(
567
+ flock=self,
568
+ server_name=server_name,
569
+ show_results=show_results,
570
+ edit_mode=edit_mode,
571
+ )
463
572
 
464
- logger.info(
465
- "Starting agent execution",
466
- agent=self.start_agent.name,
467
- enable_temporal=self.enable_temporal,
573
+ # --- Static Method Loaders (Keep for convenience) ---
574
+ @staticmethod
575
+ def load_from_file(file_path: str) -> Flock:
576
+ """Load a Flock instance from various file formats (detects type)."""
577
+ p = Path(file_path)
578
+ if not p.exists():
579
+ raise FileNotFoundError(f"Flock file not found: {file_path}")
580
+
581
+ if p.suffix in [".yaml", ".yml"]:
582
+ return Flock.from_yaml_file(p)
583
+ elif p.suffix == ".json":
584
+ return Flock.from_json(p.read_text())
585
+ elif p.suffix == ".msgpack":
586
+ return Flock.from_msgpack_file(p)
587
+ elif p.suffix == ".pkl":
588
+ if PICKLE_AVAILABLE:
589
+ return Flock.from_pickle_file(p)
590
+ else:
591
+ raise RuntimeError(
592
+ "Cannot load Pickle file: cloudpickle not installed."
468
593
  )
469
-
470
- if not self.enable_temporal:
471
- return await run_local_workflow(self.context, box_result)
472
- else:
473
- return await run_temporal_workflow(self.context, box_result)
474
- except Exception as e:
475
- logger.exception("Execution failed", error=str(e))
476
- raise
594
+ else:
595
+ raise ValueError(
596
+ f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
597
+ )