flock-core 0.4.0b4__py3-none-any.whl → 0.4.0b5__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,81 +1,71 @@
1
1
  # src/flock/core/flock.py
2
- """High-level orchestrator for creating and executing agents."""
2
+ """High-level orchestrator for managing and executing agents within the Flock framework."""
3
3
 
4
4
  from __future__ import annotations # Ensure forward references work
5
5
 
6
6
  import asyncio
7
7
  import os
8
8
  import uuid
9
+ from collections.abc import Callable
9
10
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Any, Literal, TypeVar
11
+ from typing import (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Literal,
15
+ TypeVar,
16
+ )
11
17
 
18
+ # Third-party imports
12
19
  from box import Box
20
+ from datasets import Dataset
13
21
  from opentelemetry import trace
14
22
  from opentelemetry.baggage import get_baggage, set_baggage
15
-
16
- # Pydantic and OpenTelemetry
17
- from pydantic import BaseModel, Field # Using Pydantic directly now
23
+ from pandas import DataFrame
24
+ from pydantic import BaseModel, Field
18
25
 
19
26
  # Flock core components & utilities
20
- from flock.config import TELEMETRY
27
+ from flock.config import DEFAULT_MODEL, TELEMETRY
21
28
  from flock.core.context.context import FlockContext
22
29
  from flock.core.context.context_manager import initialize_context
23
30
  from flock.core.execution.local_executor import run_local_workflow
24
31
  from flock.core.execution.temporal_executor import run_temporal_workflow
32
+ from flock.core.flock_evaluator import FlockEvaluator
25
33
  from flock.core.logging.logging import LOGGERS, get_logger, get_module_loggers
26
- from flock.core.serialization.serialization_utils import (
27
- extract_pydantic_models_from_type_string,
28
- )
29
- from flock.core.util.input_resolver import split_top_level
34
+ from flock.core.serialization.serializable import Serializable
35
+ from flock.core.util.cli_helper import init_console
30
36
 
31
37
  # Import FlockAgent using TYPE_CHECKING to avoid circular import at runtime
32
38
  if TYPE_CHECKING:
39
+ # These imports are only for type hints
33
40
  from flock.core.flock_agent import FlockAgent
34
- else:
35
- # Provide a forward reference string or Any for runtime if FlockAgent is used in hints here
36
- FlockAgent = "FlockAgent" # Forward reference string for Pydantic/runtime
37
41
 
38
- # Registry and Serialization
39
- from flock.core.flock_registry import (
40
- get_registry, # Use the unified registry
41
- )
42
- from flock.core.serialization.serializable import (
43
- Serializable, # Import Serializable base
44
- )
45
42
 
46
- # NOTE: Flock.to_dict/from_dict primarily orchestrates agent serialization.
47
- # It doesn't usually need serialize_item/deserialize_item directly,
48
- # relying on FlockAgent's implementation instead.
49
- # from flock.core.serialization.serialization_utils import serialize_item, deserialize_item
50
- # CLI Helper (if still used directly, otherwise can be removed)
51
- from flock.core.util.cli_helper import init_console
43
+ # Registry
44
+ from flock.core.flock_registry import get_registry
52
45
 
53
- # Cloudpickle for fallback/direct serialization if needed
54
46
  try:
55
- import cloudpickle
47
+ import pandas as pd
56
48
 
57
- PICKLE_AVAILABLE = True
49
+ PANDAS_AVAILABLE = True
58
50
  except ImportError:
59
- PICKLE_AVAILABLE = False
60
-
51
+ pd = None
52
+ PANDAS_AVAILABLE = False
61
53
 
62
54
  logger = get_logger("flock")
63
55
  TELEMETRY.setup_tracing() # Setup OpenTelemetry
64
56
  tracer = trace.get_tracer(__name__)
65
57
  FlockRegistry = get_registry() # Get the registry instance
66
58
 
67
- # Define TypeVar for generic methods like from_dict
59
+ # Define TypeVar for generic class methods like from_dict
68
60
  T = TypeVar("T", bound="Flock")
69
61
 
70
62
 
71
- # Inherit from Serializable for YAML/JSON/etc. methods
72
- # Use BaseModel directly for Pydantic features
73
63
  class Flock(BaseModel, Serializable):
74
- """High-level orchestrator for creating and executing agent systems.
64
+ """Orchestrator for managing and executing agent systems.
75
65
 
76
- Flock manages agent definitions, context, and execution flow, supporting
77
- both local debugging and robust distributed execution via Temporal.
78
- It is serializable to various formats like YAML and JSON.
66
+ Manages agent definitions, context, and execution flow (local or Temporal).
67
+ Relies on FlockSerializer for serialization/deserialization logic.
68
+ Inherits from Pydantic BaseModel and Serializable.
79
69
  """
80
70
 
81
71
  name: str | None = Field(
@@ -83,8 +73,8 @@ class Flock(BaseModel, Serializable):
83
73
  description="A unique identifier for this Flock instance.",
84
74
  )
85
75
  model: str | None = Field(
86
- default="openai/gpt-4o",
87
- description="Default model identifier to be used for agents if not specified otherwise.",
76
+ default=DEFAULT_MODEL,
77
+ description="Default model identifier for agents if not specified otherwise.",
88
78
  )
89
79
  description: str | None = Field(
90
80
  default=None,
@@ -100,47 +90,43 @@ class Flock(BaseModel, Serializable):
100
90
  )
101
91
  show_flock_banner: bool = Field(
102
92
  default=True,
103
- description="If True, show the Flock banner.",
93
+ description="If True, show the Flock banner on console interactions.",
104
94
  )
105
- # --- Runtime Attributes (Excluded from Serialization) ---
106
- # Store agents internally but don't make it part of the Pydantic model definition
107
- # Use a regular attribute, initialized in __init__
108
- # Pydantic V2 handles __init__ and attributes not in Field correctly
95
+ # Internal agent storage - not part of the Pydantic model for direct serialization
109
96
  _agents: dict[str, FlockAgent]
110
- _start_agent_name: str | None
111
- _start_input: dict
97
+ _start_agent_name: str | None = None # For potential pre-configuration
98
+ _start_input: dict = {} # For potential pre-configuration
112
99
 
113
100
  # Pydantic v2 model config
114
101
  model_config = {
115
102
  "arbitrary_types_allowed": True,
116
- "ignored_types": (
117
- type(FlockRegistry),
118
- ), # Prevent validation issues with registry
119
- # No need to exclude fields here, handled in to_dict
103
+ "ignored_types": (type(FlockRegistry),),
120
104
  }
121
105
 
122
106
  def __init__(
123
107
  self,
124
108
  name: str | None = None,
125
- model: str | None = "openai/gpt-4o",
109
+ model: str | None = DEFAULT_MODEL,
126
110
  description: str | None = None,
127
111
  show_flock_banner: bool = True,
128
112
  enable_temporal: bool = False,
129
- enable_logging: bool
130
- | list[str] = False, # Keep logging control at init
131
- agents: list[FlockAgent] | None = None, # Allow passing agents at init
132
- **kwargs, # Allow extra fields during init if needed, Pydantic handles it
113
+ enable_logging: bool | list[str] = False,
114
+ agents: list[FlockAgent] | None = None,
115
+ **kwargs,
133
116
  ):
134
117
  """Initialize the Flock orchestrator."""
118
+ # Use provided name or generate default BEFORE super init if needed elsewhere
119
+ effective_name = name or f"flock_{uuid.uuid4().hex[:8]}"
120
+
135
121
  # Initialize Pydantic fields
136
122
  super().__init__(
137
- name=name,
123
+ name=effective_name,
138
124
  model=model,
139
125
  description=description,
140
126
  enable_temporal=enable_temporal,
141
127
  enable_logging=enable_logging,
142
128
  show_flock_banner=show_flock_banner,
143
- **kwargs, # Pass extra kwargs to Pydantic BaseModel
129
+ **kwargs,
144
130
  )
145
131
 
146
132
  # Initialize runtime attributes AFTER super().__init__()
@@ -148,13 +134,11 @@ class Flock(BaseModel, Serializable):
148
134
  self._start_agent_name = None
149
135
  self._start_input = {}
150
136
 
151
- # Set up logging
152
- self._configure_logging(enable_logging)
137
+ # Set up logging based on the enable_logging flag
138
+ self._configure_logging(enable_logging) # Use instance attribute
153
139
 
154
140
  # Register passed agents
155
141
  if agents:
156
- # Ensure FlockAgent type is available for isinstance check
157
- # This import might need to be deferred or handled carefully if it causes issues
158
142
  from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
159
143
 
160
144
  for agent in agents:
@@ -165,8 +149,8 @@ class Flock(BaseModel, Serializable):
165
149
  f"Item provided in 'agents' list is not a FlockAgent: {type(agent)}"
166
150
  )
167
151
 
168
- # Initialize console if needed
169
- if show_flock_banner:
152
+ # Initialize console if needed for banner
153
+ if self.show_flock_banner: # Use instance attribute
170
154
  init_console()
171
155
 
172
156
  # Set Temporal debug environment variable
@@ -177,24 +161,20 @@ class Flock(BaseModel, Serializable):
177
161
 
178
162
  logger.info(
179
163
  "Flock instance initialized",
164
+ name=self.name,
180
165
  model=self.model,
181
166
  enable_temporal=self.enable_temporal,
182
167
  )
183
168
 
184
- # --- Keep _configure_logging, _set_temporal_debug_flag, _ensure_session_id ---
185
- # ... (implementation as before) ...
186
169
  def _configure_logging(self, enable_logging: bool | list[str]):
187
170
  """Configure logging levels based on the enable_logging flag."""
188
- # logger.debug(f"Configuring logging, enable_logging={enable_logging}")
189
171
  is_enabled_globally = False
190
172
  enabled_loggers = []
191
173
 
192
174
  if isinstance(enable_logging, bool):
193
175
  is_enabled_globally = enable_logging
194
176
  elif isinstance(enable_logging, list):
195
- is_enabled_globally = bool(
196
- enable_logging
197
- ) # Enable if list is not empty
177
+ is_enabled_globally = bool(enable_logging)
198
178
  enabled_loggers = enable_logging
199
179
 
200
180
  # Configure core loggers
@@ -235,11 +215,8 @@ class Flock(BaseModel, Serializable):
235
215
  set_baggage("session_id", session_id)
236
216
  logger.debug(f"Generated new session_id: {session_id}")
237
217
 
238
- # --- Keep add_agent, agents property, run, run_async ---
239
- # ... (implementation as before, ensuring FlockAgent type hint is handled) ...
240
218
  def add_agent(self, agent: FlockAgent) -> FlockAgent:
241
- """Adds an agent instance to this Flock configuration."""
242
- # Ensure FlockAgent type is available for isinstance check
219
+ """Adds an agent instance to this Flock configuration and registry."""
243
220
  from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
244
221
 
245
222
  if not isinstance(agent, ConcreteFlockAgent):
@@ -248,16 +225,13 @@ class Flock(BaseModel, Serializable):
248
225
  raise ValueError("Agent must have a name.")
249
226
 
250
227
  if agent.name in self._agents:
251
- logger.warning(
252
- f"Agent '{agent.name}' already exists in this Flock instance. Overwriting."
253
- )
228
+ logger.warning(f"Agent '{agent.name}' already exists. Overwriting.")
254
229
  self._agents[agent.name] = agent
255
- FlockRegistry.register_agent(agent) # Also register globally
230
+ FlockRegistry.register_agent(agent) # Register globally
256
231
 
257
232
  # Set default model if agent doesn't have one
258
233
  if agent.model is None:
259
- # agent.set_model(self.model) # Use Flock's default model
260
- if self.model: # Ensure Flock has a model defined
234
+ if self.model:
261
235
  agent.set_model(self.model)
262
236
  logger.debug(
263
237
  f"Agent '{agent.name}' using Flock default model: {self.model}"
@@ -267,7 +241,7 @@ class Flock(BaseModel, Serializable):
267
241
  f"Agent '{agent.name}' has no model and Flock default model is not set."
268
242
  )
269
243
 
270
- logger.info(f"Agent '{agent.name}' added to Flock.")
244
+ logger.info(f"Agent '{agent.name}' added to Flock '{self.name}'.")
271
245
  return agent
272
246
 
273
247
  @property
@@ -279,31 +253,46 @@ class Flock(BaseModel, Serializable):
279
253
  self,
280
254
  start_agent: FlockAgent | str | None = None,
281
255
  input: dict = {},
282
- context: FlockContext
283
- | None = None, # Allow passing initial context state
256
+ context: FlockContext | None = None,
284
257
  run_id: str = "",
285
- box_result: bool = True, # Changed default to False for raw dict
286
- agents: list[FlockAgent] | None = None, # Allow adding agents via run
287
- ) -> Box:
258
+ box_result: bool = True,
259
+ agents: list[FlockAgent] | None = None,
260
+ ) -> Box | dict:
288
261
  """Entry point for running an agent system synchronously."""
289
- # Check if an event loop is already running
290
262
  try:
291
263
  loop = asyncio.get_running_loop()
292
- except (
293
- RuntimeError
294
- ): # 'RuntimeError: There is no current event loop...'
264
+ # If loop exists, check if it's closed
265
+ if loop.is_closed():
266
+ raise RuntimeError("Event loop is closed")
267
+ except RuntimeError: # No running loop
295
268
  loop = asyncio.new_event_loop()
296
269
  asyncio.set_event_loop(loop)
297
- return loop.run_until_complete(
298
- self.run_async(
299
- start_agent=start_agent,
300
- input=input,
301
- context=context,
302
- run_id=run_id,
303
- box_result=box_result,
304
- agents=agents,
270
+
271
+ # Ensure the loop runs the task and handles closure if we created it
272
+ if asyncio.get_event_loop() is loop and not loop.is_running():
273
+ result = loop.run_until_complete(
274
+ self.run_async(
275
+ start_agent=start_agent,
276
+ input=input,
277
+ context=context,
278
+ run_id=run_id,
279
+ box_result=box_result,
280
+ agents=agents,
281
+ )
305
282
  )
306
- )
283
+ return result
284
+ else:
285
+ future = asyncio.ensure_future(
286
+ self.run_async(
287
+ start_agent=start_agent,
288
+ input=input,
289
+ context=context,
290
+ run_id=run_id,
291
+ box_result=box_result,
292
+ agents=agents,
293
+ )
294
+ )
295
+ return loop.run_until_complete(future)
307
296
 
308
297
  async def run_async(
309
298
  self,
@@ -311,11 +300,11 @@ class Flock(BaseModel, Serializable):
311
300
  input: dict | None = None,
312
301
  context: FlockContext | None = None,
313
302
  run_id: str = "",
314
- box_result: bool = True, # Changed default
315
- agents: list[FlockAgent] | None = None, # Allow adding agents via run
316
- ) -> Box:
303
+ box_result: bool = True,
304
+ agents: list[FlockAgent] | None = None,
305
+ ) -> Box | dict:
317
306
  """Entry point for running an agent system asynchronously."""
318
- # This import needs to be here or handled carefully due to potential cycles
307
+ # Import here to allow forward reference resolution
319
308
  from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
320
309
 
321
310
  with tracer.start_as_current_span("flock.run_async") as span:
@@ -323,9 +312,7 @@ class Flock(BaseModel, Serializable):
323
312
  if agents:
324
313
  for agent_obj in agents:
325
314
  if isinstance(agent_obj, ConcreteFlockAgent):
326
- self.add_agent(
327
- agent_obj
328
- ) # Adds to self._agents and registry
315
+ self.add_agent(agent_obj)
329
316
  else:
330
317
  logger.warning(
331
318
  f"Item in 'agents' list is not a FlockAgent: {type(agent_obj)}"
@@ -336,1060 +323,298 @@ class Flock(BaseModel, Serializable):
336
323
  if isinstance(start_agent, ConcreteFlockAgent):
337
324
  start_agent_name = start_agent.name
338
325
  if start_agent_name not in self._agents:
339
- self.add_agent(
340
- start_agent
341
- ) # Add if instance was passed but not added
326
+ self.add_agent(start_agent)
342
327
  elif isinstance(start_agent, str):
343
328
  start_agent_name = start_agent
344
329
  else:
345
- start_agent_name = (
346
- self._start_agent_name
347
- ) # Use pre-configured if any
330
+ start_agent_name = self._start_agent_name
348
331
 
349
332
  # Default to first agent if only one exists and none specified
350
333
  if not start_agent_name and len(self._agents) == 1:
351
334
  start_agent_name = list(self._agents.keys())[0]
352
335
  elif not start_agent_name:
353
336
  raise ValueError(
354
- "No start_agent specified and multiple agents exist or none are added."
337
+ "No start_agent specified and multiple/no agents exist."
355
338
  )
356
339
 
357
- # Get starting input
358
340
  run_input = input if input is not None else self._start_input
341
+ effective_run_id = run_id or f"flockrun_{uuid.uuid4().hex[:8]}"
359
342
 
360
- # Log and trace start info
361
343
  span.set_attribute("start_agent", start_agent_name)
362
344
  span.set_attribute("input", str(run_input))
363
- span.set_attribute("run_id", run_id)
345
+ span.set_attribute("run_id", effective_run_id)
364
346
  span.set_attribute("enable_temporal", self.enable_temporal)
365
347
  logger.info(
366
- f"Initiating Flock run. Start Agent: '{start_agent_name}'. Temporal: {self.enable_temporal}."
348
+ f"Initiating Flock run '{self.name}'. Start Agent: '{start_agent_name}'. Temporal: {self.enable_temporal}."
367
349
  )
368
350
 
369
351
  try:
370
- # Resolve start agent instance from internal dict
371
352
  resolved_start_agent = self._agents.get(start_agent_name)
372
353
  if not resolved_start_agent:
373
- # Maybe it's only in the global registry? (Less common)
374
354
  resolved_start_agent = FlockRegistry.get_agent(
375
355
  start_agent_name
376
356
  )
377
357
  if not resolved_start_agent:
378
358
  raise ValueError(
379
- f"Start agent '{start_agent_name}' not found in Flock instance or registry."
359
+ f"Start agent '{start_agent_name}' not found."
380
360
  )
381
- else:
382
- # If found globally, add it to this instance for consistency during run
383
- self.add_agent(resolved_start_agent)
361
+ self.add_agent(resolved_start_agent)
384
362
 
385
- # Create or use provided context
386
363
  run_context = context if context else FlockContext()
387
- if not run_id:
388
- run_id = f"flockrun_{uuid.uuid4().hex[:8]}"
389
- set_baggage("run_id", run_id) # Ensure run_id is in baggage
364
+ set_baggage("run_id", effective_run_id)
390
365
 
391
- # Initialize context
392
366
  initialize_context(
393
367
  run_context,
394
368
  start_agent_name,
395
369
  run_input,
396
- run_id,
370
+ effective_run_id,
397
371
  not self.enable_temporal,
398
- self.model
399
- or resolved_start_agent.model
400
- or "default-model-missing", # Pass effective model
372
+ self.model or resolved_start_agent.model or DEFAULT_MODEL,
401
373
  )
374
+ # Add agent definitions to context for routing/serialization within workflow
375
+ for agent_name, agent_instance in self.agents.items():
376
+ # Agents already handle their serialization
377
+ agent_dict_repr = agent_instance.to_dict()
378
+ run_context.add_agent_definition(
379
+ agent_type=type(agent_instance),
380
+ agent_name=agent_name,
381
+ agent_data=agent_dict_repr, # Pass the serialized dict
382
+ )
402
383
 
403
- # Execute workflow
404
384
  logger.info(
405
385
  "Starting agent execution",
406
386
  agent=start_agent_name,
407
387
  enable_temporal=self.enable_temporal,
408
388
  )
409
389
 
390
+ # Execute workflow
410
391
  if not self.enable_temporal:
411
392
  result = await run_local_workflow(
412
393
  run_context, box_result=False
413
- ) # Get raw dict
394
+ )
414
395
  else:
415
396
  result = await run_temporal_workflow(
416
397
  run_context, box_result=False
417
- ) # Get raw dict
398
+ )
418
399
 
419
400
  span.set_attribute("result.type", str(type(result)))
420
- # Avoid overly large results in trace attributes
421
401
  result_str = str(result)
422
- if len(result_str) > 1000:
423
- result_str = result_str[:1000] + "... (truncated)"
424
- span.set_attribute("result.preview", result_str)
402
+ span.set_attribute(
403
+ "result.preview",
404
+ result_str[:1000]
405
+ + ("..." if len(result_str) > 1000 else ""),
406
+ )
425
407
 
426
- # Optionally box result before returning
427
408
  if box_result:
428
409
  try:
429
- from box import Box
430
-
431
410
  logger.debug("Boxing final result.")
432
411
  return Box(result)
433
412
  except ImportError:
434
413
  logger.warning(
435
- "Box library not installed, returning raw dict. Install with 'pip install python-box'"
414
+ "Box library not installed, returning raw dict."
436
415
  )
437
416
  return result
438
417
  else:
439
418
  return result
440
419
 
441
420
  except Exception as e:
442
- logger.error(f"Flock run failed: {e}", exc_info=True)
421
+ logger.error(
422
+ f"Flock run '{self.name}' failed: {e}", exc_info=True
423
+ )
443
424
  span.record_exception(e)
444
425
  span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
445
- # Depending on desired behavior, either raise or return an error dict
446
- # raise # Option 1: Let the exception propagate
447
426
  return {
448
427
  "error": str(e),
449
- "details": "Flock run failed.",
450
- } # Option 2: Return error dict
451
-
452
- # --- ADDED Serialization Methods ---
453
-
454
- def to_dict(
455
- self, path_type: Literal["absolute", "relative"] = "absolute"
456
- ) -> dict[str, Any]:
457
- """Convert Flock instance to dictionary representation.
458
-
459
- Args:
460
- path_type: How file paths should be formatted ('absolute' or 'relative')
461
- """
462
- logger.debug("Serializing Flock instance to dict.")
463
- # Use Pydantic's dump for base fields
464
- data = self.model_dump(mode="json", exclude_none=True)
465
- logger.info(
466
- f"Serializing Flock '{self.name}' with {len(self._agents)} agents"
467
- )
468
-
469
- # Manually add serialized agents
470
- data["agents"] = {}
471
-
472
- # Track custom types used across all agents
473
- custom_types = {}
474
- # Track components used across all agents
475
- components = {}
476
-
477
- for name, agent_instance in self._agents.items():
478
- try:
479
- logger.debug(f"Serializing agent '{name}'")
480
- # Agents handle their own serialization via their to_dict
481
- agent_data = agent_instance.to_dict()
482
- data["agents"][name] = agent_data
483
-
484
- if agent_instance.input:
485
- logger.debug(
486
- f"Extracting type information from agent '{name}' input: {agent_instance.input}"
487
- )
488
- input_types = self._extract_types_from_signature(
489
- agent_instance.input
490
- )
491
- if input_types:
492
- logger.debug(
493
- f"Found input types in agent '{name}': {input_types}"
494
- )
495
- custom_types.update(
496
- self._get_type_definitions(input_types)
497
- )
498
-
499
- # Extract type information from agent outputs
500
- if agent_instance.output:
501
- logger.debug(
502
- f"Extracting type information from agent '{name}' output: {agent_instance.output}"
503
- )
504
- output_types = self._extract_types_from_signature(
505
- agent_instance.output
506
- )
507
- if output_types:
508
- logger.debug(
509
- f"Found output types in agent '{name}': {output_types}"
510
- )
511
- custom_types.update(
512
- self._get_type_definitions(output_types)
513
- )
514
-
515
- # Extract component information
516
- if (
517
- "evaluator" in agent_data
518
- and "type" in agent_data["evaluator"]
519
- ):
520
- component_type = agent_data["evaluator"]["type"]
521
- logger.debug(
522
- f"Adding evaluator component '{component_type}' from agent '{name}'"
523
- )
524
- components[component_type] = self._get_component_definition(
525
- component_type, path_type
526
- )
527
-
528
- # Extract module component information
529
- if "modules" in agent_data:
530
- for module_name, module_data in agent_data[
531
- "modules"
532
- ].items():
533
- if "type" in module_data:
534
- component_type = module_data["type"]
535
- logger.debug(
536
- f"Adding module component '{component_type}' from module '{module_name}' in agent '{name}'"
537
- )
538
- components[component_type] = (
539
- self._get_component_definition(
540
- component_type, path_type
541
- )
542
- )
543
-
544
- # Extract tool (callable) information
545
- if agent_data.get("tools"):
546
- logger.debug(
547
- f"Extracting tool information from agent '{name}': {agent_data['tools']}"
548
- )
549
- # Get references to the actual tool objects
550
- tool_objs = (
551
- agent_instance.tools if agent_instance.tools else []
552
- )
553
- for i, tool_name in enumerate(agent_data["tools"]):
554
- if i < len(tool_objs):
555
- tool = tool_objs[i]
556
- if callable(tool) and not isinstance(tool, type):
557
- # Get the fully qualified name for registry lookup
558
- path_str = (
559
- get_registry().get_callable_path_string(
560
- tool
561
- )
562
- )
563
- if path_str:
564
- logger.debug(
565
- f"Adding tool '{tool_name}' (from path '{path_str}') to components"
566
- )
567
- # Add definition using just the function name as the key
568
- components[tool_name] = (
569
- self._get_callable_definition(
570
- path_str, tool_name, path_type
571
- )
572
- )
573
-
574
- except Exception as e:
575
- logger.error(
576
- f"Failed to serialize agent '{name}' within Flock: {e}",
577
- exc_info=True,
578
- )
579
- # Optionally skip problematic agents or raise error
580
- # data["agents"][name] = {"error": f"Serialization failed: {e}"}
581
-
582
- # Add type definitions to the serialized output if any were found
583
- if custom_types:
584
- logger.info(
585
- f"Adding {len(custom_types)} custom type definitions to serialized output"
586
- )
587
- data["types"] = custom_types
588
-
589
- # Add component definitions to the serialized output if any were found
590
- if components:
591
- logger.info(
592
- f"Adding {len(components)} component definitions to serialized output"
593
- )
594
- data["components"] = components
595
-
596
- # Add dependencies section
597
- data["dependencies"] = self._get_dependencies()
598
-
599
- # Add serialization settings
600
- data["metadata"] = {"path_type": path_type}
601
-
602
- logger.debug(
603
- f"Flock serialization complete with {len(data['agents'])} agents, {len(custom_types)} types, {len(components)} components"
604
- )
605
-
606
- return data
607
-
608
- def _extract_types_from_signature(self, signature: str) -> list[str]:
609
- """Extract type names from an input/output signature string."""
610
- if not signature:
611
- return []
612
-
613
- signature_parts = split_top_level(signature)
614
-
615
- # Basic type extraction - handles simple cases like "result: TypeName" or "list[TypeName]"
616
- custom_types = []
617
-
618
- # Look for type annotations (everything after ":")
619
- for part in signature_parts:
620
- try:
621
- parts = part.split(":")
622
- if len(parts) > 1:
623
- type_part = parts[1].strip()
624
-
625
- pydantic_models = extract_pydantic_models_from_type_string(
626
- type_part
627
- )
628
- if pydantic_models:
629
- for model in pydantic_models:
630
- custom_types.append(model.__name__)
631
- except Exception:
632
- logger.warning(
633
- f"Could not extract types from signature '{signature}' (probably no type defined?)"
634
- )
635
- return []
636
-
637
- # # Extract from list[Type]
638
- # if "list[" in type_part:
639
- # inner_type = type_part.split("list[")[1].split("]")[0].strip()
640
- # if inner_type and inner_type.lower() not in [
641
- # "str",
642
- # "int",
643
- # "float",
644
- # "bool",
645
- # "dict",
646
- # "list",
647
- # ]:
648
- # custom_types.append(inner_type)
649
-
650
- # # Extract direct type references
651
- # elif type_part and type_part.lower() not in [
652
- # "str",
653
- # "int",
654
- # "float",
655
- # "bool",
656
- # "dict",
657
- # "list",
658
- # ]:
659
- # custom_types.append(
660
- # type_part.split()[0]
661
- # ) # Take the first word in case there's a description
662
-
663
- return custom_types
664
-
665
- def _get_type_definitions(self, type_names: list[str]) -> dict[str, Any]:
666
- """Get definitions for the specified custom types."""
667
- from flock.core.flock_registry import get_registry
668
-
669
- type_definitions = {}
670
- registry = get_registry()
671
-
672
- for type_name in type_names:
673
- try:
674
- # Try to get the type from registry
675
- type_obj = registry._types.get(type_name)
676
- if type_obj:
677
- type_def = self._extract_type_definition(
678
- type_name, type_obj
679
- )
680
- if type_def:
681
- type_definitions[type_name] = type_def
682
- except Exception as e:
683
- logger.warning(
684
- f"Could not extract definition for type {type_name}: {e}"
685
- )
686
-
687
- return type_definitions
688
-
689
- def _extract_type_definition(
690
- self, type_name: str, type_obj: type
691
- ) -> dict[str, Any]:
692
- """Extract a definition for a custom type."""
693
- import inspect
694
- from dataclasses import is_dataclass
695
-
696
- type_def = {
697
- "module_path": type_obj.__module__,
698
- }
699
-
700
- # Handle Pydantic models
701
- if hasattr(type_obj, "model_json_schema") and callable(
702
- getattr(type_obj, "model_json_schema")
703
- ):
704
- type_def["type"] = "pydantic.BaseModel"
705
- try:
706
- schema = type_obj.model_json_schema()
707
- # Clean up schema to remove unnecessary fields
708
- if "title" in schema and schema["title"] == type_name:
709
- del schema["title"]
710
- type_def["schema"] = schema
711
- except Exception as e:
712
- logger.warning(
713
- f"Could not extract schema for Pydantic model {type_name}: {e}"
714
- )
715
-
716
- # Handle dataclasses
717
- elif is_dataclass(type_obj):
718
- type_def["type"] = "dataclass"
719
- fields = {}
720
- for field_name, field in type_obj.__dataclass_fields__.items():
721
- fields[field_name] = {
722
- "type": str(field.type),
723
- "default": str(field.default)
724
- if field.default is not inspect.Parameter.empty
725
- else None,
428
+ "details": f"Flock run '{self.name}' failed.",
726
429
  }
727
- type_def["fields"] = fields
728
430
 
729
- # Handle other types - just store basic information
730
- else:
731
- type_def["type"] = "custom"
732
-
733
- # Extract import statement (simplified version)
734
- type_def["imports"] = [f"from {type_obj.__module__} import {type_name}"]
735
-
736
- return type_def
737
-
738
- def _get_component_definition(
739
- self, component_type: str, path_type: Literal["absolute", "relative"]
740
- ) -> dict[str, Any]:
741
- """Get definition for a component type."""
742
- import os
743
- import sys
744
-
745
- from flock.core.flock_registry import get_registry
746
-
747
- registry = get_registry()
748
- component_def = {}
749
-
750
- try:
751
- # Try to get the component class from registry
752
- component_class = registry._components.get(component_type)
753
- if component_class:
754
- # Get the standard module path
755
- module_path = component_class.__module__
756
-
757
- # Get the actual file system path if possible
758
- file_path = None
759
- try:
760
- if (
761
- hasattr(component_class, "__module__")
762
- and component_class.__module__
763
- ):
764
- module = sys.modules.get(component_class.__module__)
765
- if module and hasattr(module, "__file__"):
766
- file_path = os.path.abspath(module.__file__)
767
- # Convert to relative path if needed
768
- if path_type == "relative" and file_path:
769
- try:
770
- file_path = os.path.relpath(file_path)
771
- except ValueError:
772
- # Keep as absolute if can't make relative
773
- logger.warning(
774
- f"Could not convert path to relative: {file_path}"
775
- )
776
- except Exception as e:
777
- # If we can't get the file path, we'll just use the module path
778
- logger.warning(
779
- f"Error getting file path for component {component_type}: {e}"
780
- )
781
- pass
782
-
783
- component_def = {
784
- "type": "flock_component",
785
- "module_path": module_path,
786
- "file_path": file_path, # Include actual file system path
787
- "description": getattr(
788
- component_class, "__doc__", ""
789
- ).strip()
790
- or f"{component_type} component",
791
- }
792
- except Exception as e:
793
- logger.warning(
794
- f"Could not extract definition for component {component_type}: {e}"
795
- )
796
- # Provide minimal information if we can't extract details
797
- component_def = {
798
- "type": "flock_component",
799
- "module_path": "unknown",
800
- "file_path": None,
801
- "description": f"{component_type} component (definition incomplete)",
802
- }
803
-
804
- return component_def
805
-
806
- def _get_callable_definition(
431
+ # --- Batch Processing (Delegation) ---
432
+ async def run_batch_async(
807
433
  self,
808
- callable_ref: str,
809
- func_name: str,
810
- path_type: Literal["absolute", "relative"],
811
- ) -> dict[str, Any]:
812
- """Get definition for a callable reference.
813
-
814
- Args:
815
- callable_ref: The fully qualified path to the callable
816
- func_name: The simple function name (for display purposes)
817
- path_type: How file paths should be formatted ('absolute' or 'relative')
818
- """
819
- import inspect
820
- import os
821
- import sys
822
-
823
- from flock.core.flock_registry import get_registry
824
-
825
- registry = get_registry()
826
- callable_def = {}
434
+ start_agent: FlockAgent | str,
435
+ batch_inputs: list[dict[str, Any]] | DataFrame | str,
436
+ input_mapping: dict[str, str] | None = None,
437
+ static_inputs: dict[str, Any] | None = None,
438
+ parallel: bool = True,
439
+ max_workers: int = 5,
440
+ use_temporal: bool | None = None,
441
+ box_results: bool = True,
442
+ return_errors: bool = False,
443
+ silent_mode: bool = False,
444
+ write_to_csv: str | None = None,
445
+ ) -> list[Box | dict | None | Exception]:
446
+ """Runs the specified agent/workflow for each item in a batch asynchronously (delegated)."""
447
+ # Import processor locally
448
+ from flock.core.execution.batch_executor import BatchProcessor
449
+
450
+ processor = BatchProcessor(self) # Pass self
451
+ return await processor.run_batch_async(
452
+ start_agent=start_agent,
453
+ batch_inputs=batch_inputs,
454
+ input_mapping=input_mapping,
455
+ static_inputs=static_inputs,
456
+ parallel=parallel,
457
+ max_workers=max_workers,
458
+ use_temporal=use_temporal,
459
+ box_results=box_results,
460
+ return_errors=return_errors,
461
+ silent_mode=silent_mode,
462
+ write_to_csv=write_to_csv,
463
+ )
827
464
 
465
+ def run_batch(
466
+ self,
467
+ start_agent: FlockAgent | str,
468
+ batch_inputs: list[dict[str, Any]] | DataFrame | str,
469
+ input_mapping: dict[str, str] | None = None,
470
+ static_inputs: dict[str, Any] | None = None,
471
+ parallel: bool = True,
472
+ max_workers: int = 5,
473
+ use_temporal: bool | None = None,
474
+ box_results: bool = True,
475
+ return_errors: bool = False,
476
+ silent_mode: bool = False,
477
+ write_to_csv: str | None = None,
478
+ ) -> list[Box | dict | None | Exception]:
479
+ """Synchronous wrapper for run_batch_async."""
480
+ # (Standard asyncio run wrapper logic)
828
481
  try:
829
- # Try to get the callable from registry
830
- logger.debug(
831
- f"Getting callable definition for '{callable_ref}' (display name: '{func_name}')"
832
- )
833
- func = registry.get_callable(callable_ref)
834
- if func:
835
- # Get the standard module path
836
- module_path = func.__module__
837
-
838
- # Get the actual file system path if possible
839
- file_path = None
840
- try:
841
- if func.__module__ and func.__module__ != "builtins":
842
- module = sys.modules.get(func.__module__)
843
- if module and hasattr(module, "__file__"):
844
- file_path = os.path.abspath(module.__file__)
845
- # Convert to relative path if needed
846
- if path_type == "relative" and file_path:
847
- try:
848
- file_path = os.path.relpath(file_path)
849
- except ValueError:
850
- # Keep as absolute if can't make relative
851
- logger.warning(
852
- f"Could not convert path to relative: {file_path}"
853
- )
854
- except Exception as e:
855
- # If we can't get the file path, just use the module path
856
- logger.warning(
857
- f"Error getting file path for callable {callable_ref}: {e}"
858
- )
859
- pass
482
+ loop = asyncio.get_running_loop()
483
+ if loop.is_closed():
484
+ raise RuntimeError("Event loop is closed")
485
+ except RuntimeError: # No running loop
486
+ loop = asyncio.new_event_loop()
487
+ asyncio.set_event_loop(loop)
860
488
 
861
- # Get the docstring for description
862
- docstring = (
863
- inspect.getdoc(func) or f"Callable function {func_name}"
864
- )
489
+ coro = self.run_batch_async(
490
+ start_agent=start_agent,
491
+ batch_inputs=batch_inputs,
492
+ input_mapping=input_mapping,
493
+ static_inputs=static_inputs,
494
+ parallel=parallel,
495
+ max_workers=max_workers,
496
+ use_temporal=use_temporal,
497
+ box_results=box_results,
498
+ return_errors=return_errors,
499
+ silent_mode=silent_mode,
500
+ write_to_csv=write_to_csv,
501
+ )
865
502
 
866
- callable_def = {
867
- "type": "flock_callable",
868
- "module_path": module_path,
869
- "file_path": file_path,
870
- "description": docstring.strip(),
871
- }
872
- logger.debug(
873
- f"Created callable definition for '{func_name}': module={module_path}, file={file_path}"
874
- )
875
- except Exception as e:
876
- logger.warning(
877
- f"Could not extract definition for callable {callable_ref}: {e}"
878
- )
879
- # Provide minimal information
880
- callable_def = {
881
- "type": "flock_callable",
882
- "module_path": callable_ref.split(".")[0]
883
- if "." in callable_ref
884
- else "unknown",
885
- "file_path": None,
886
- "description": f"Callable {func_name} (definition incomplete)",
887
- }
888
-
889
- return callable_def
890
-
891
- def _get_dependencies(self) -> list[str]:
892
- """Get list of dependencies required by this Flock."""
893
- # This is a simplified version - in production, you might want to detect
894
- # actual versions of installed packages
895
- return [
896
- "pydantic>=2.0.0",
897
- "flock>=0.3.41", # Assuming this is the package name
898
- ]
503
+ if asyncio.get_event_loop() is loop and not loop.is_running():
504
+ results = loop.run_until_complete(coro)
505
+ return results
506
+ else:
507
+ future = asyncio.ensure_future(coro)
508
+ return loop.run_until_complete(future)
899
509
 
900
- @classmethod
901
- def from_dict(cls: type[T], data: dict[str, Any]) -> T:
902
- """Create Flock instance from dictionary representation."""
903
- logger.debug(
904
- f"Deserializing Flock from dict. Provided keys: {list(data.keys())}"
510
+ # --- Evaluation (Delegation) ---
511
+ async def evaluate_async(
512
+ self,
513
+ dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset,
514
+ start_agent: FlockAgent | str,
515
+ input_mapping: dict[str, str],
516
+ answer_mapping: dict[str, str],
517
+ metrics: list[
518
+ str
519
+ | Callable[[Any, Any], bool | float | dict[str, Any]]
520
+ | FlockAgent
521
+ | FlockEvaluator
522
+ ],
523
+ metric_configs: dict[str, dict[str, Any]] | None = None,
524
+ static_inputs: dict[str, Any] | None = None,
525
+ parallel: bool = True,
526
+ max_workers: int = 5,
527
+ use_temporal: bool | None = None,
528
+ error_handling: Literal["raise", "skip", "log"] = "log",
529
+ output_file: str | Path | None = None,
530
+ return_dataframe: bool = True,
531
+ silent_mode: bool = False,
532
+ metadata_columns: list[str] | None = None,
533
+ ) -> DataFrame | list[dict[str, Any]]:
534
+ """Evaluates the Flock's performance against a dataset (delegated)."""
535
+ # Import processor locally
536
+ from flock.core.execution.evaluation_executor import (
537
+ EvaluationExecutor,
905
538
  )
906
539
 
907
- # Check for serialization settings
908
- serialization_settings = data.pop("serialization_settings", {})
909
- path_type = serialization_settings.get("path_type", "absolute")
910
- logger.debug(
911
- f"Using path_type '{path_type}' from serialization settings"
540
+ processor = EvaluationExecutor(self) # Pass self
541
+ return await processor.evaluate_async(
542
+ dataset=dataset,
543
+ start_agent=start_agent,
544
+ input_mapping=input_mapping,
545
+ answer_mapping=answer_mapping,
546
+ metrics=metrics,
547
+ metric_configs=metric_configs,
548
+ static_inputs=static_inputs,
549
+ parallel=parallel,
550
+ max_workers=max_workers,
551
+ use_temporal=use_temporal,
552
+ error_handling=error_handling,
553
+ output_file=output_file,
554
+ return_dataframe=return_dataframe,
555
+ silent_mode=silent_mode,
556
+ metadata_columns=metadata_columns,
912
557
  )
913
558
 
914
- # First, handle type definitions if present
915
- if "types" in data:
916
- logger.info(f"Processing {len(data['types'])} type definitions")
917
- cls._register_type_definitions(data["types"])
918
-
919
- # Then, handle component definitions if present
920
- if "components" in data:
921
- logger.info(
922
- f"Processing {len(data['components'])} component definitions"
923
- )
924
- cls._register_component_definitions(data["components"], path_type)
925
-
926
- # Check dependencies if present
927
- if "dependencies" in data:
928
- logger.debug(f"Checking {len(data['dependencies'])} dependencies")
929
- cls._check_dependencies(data["dependencies"])
930
-
931
- # Ensure FlockAgent is importable for type checking later
932
- try:
933
- from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
934
- except ImportError:
935
- logger.error(
936
- "Cannot import FlockAgent, deserialization may fail for agents."
937
- )
938
- ConcreteFlockAgent = Any # Fallback
939
-
940
- # Extract agent data before initializing Flock base model
941
- agents_data = data.pop("agents", {})
942
- logger.info(f"Found {len(agents_data)} agents to deserialize")
943
-
944
- # Remove types, components, and dependencies sections as they're not part of Flock fields
945
- data.pop("types", None)
946
- data.pop("components", None)
947
- data.pop("dependencies", None)
948
- # Remove metadata if present
949
- data.pop("metadata", None)
950
-
951
- # Create Flock instance using Pydantic constructor for basic fields
559
+ def evaluate(
560
+ self,
561
+ dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset,
562
+ start_agent: FlockAgent | str,
563
+ input_mapping: dict[str, str],
564
+ answer_mapping: dict[str, str],
565
+ metrics: list[
566
+ str
567
+ | Callable[[Any, Any], bool | float | dict[str, Any]]
568
+ | FlockAgent
569
+ | FlockEvaluator
570
+ ],
571
+ metric_configs: dict[str, dict[str, Any]] | None = None,
572
+ static_inputs: dict[str, Any] | None = None,
573
+ parallel: bool = True,
574
+ max_workers: int = 5,
575
+ use_temporal: bool | None = None,
576
+ error_handling: Literal["raise", "skip", "log"] = "log",
577
+ output_file: str | Path | None = None,
578
+ return_dataframe: bool = True,
579
+ silent_mode: bool = False,
580
+ metadata_columns: list[str] | None = None,
581
+ ) -> DataFrame | list[dict[str, Any]]:
582
+ """Synchronous wrapper for evaluate_async."""
583
+ # (Standard asyncio run wrapper logic)
952
584
  try:
953
- # Pass only fields defined in Flock's Pydantic model
954
- init_data = {k: v for k, v in data.items() if k in cls.model_fields}
955
- logger.debug(
956
- f"Creating Flock instance with fields: {list(init_data.keys())}"
957
- )
958
- flock_instance = cls(**init_data)
959
- except Exception as e:
960
- logger.error(
961
- f"Pydantic validation/init failed for Flock: {e}", exc_info=True
962
- )
963
- raise ValueError(
964
- f"Failed to initialize Flock from dict: {e}"
965
- ) from e
966
-
967
- # Deserialize and add agents AFTER Flock instance exists
968
- for name, agent_data in agents_data.items():
969
- try:
970
- logger.debug(f"Deserializing agent '{name}'")
971
- # Ensure agent_data has the name, or add it from the key
972
- agent_data.setdefault("name", name)
973
- # Use FlockAgent's from_dict method
974
- agent_instance = ConcreteFlockAgent.from_dict(agent_data)
975
- flock_instance.add_agent(
976
- agent_instance
977
- ) # Adds to _agents and registers
978
- logger.debug(f"Successfully added agent '{name}' to Flock")
979
- except Exception as e:
980
- logger.error(
981
- f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
982
- exc_info=True,
983
- )
984
- # Decide: skip agent or raise error?
585
+ loop = asyncio.get_running_loop()
586
+ if loop.is_closed():
587
+ raise RuntimeError("Event loop is closed")
588
+ except RuntimeError: # No running loop
589
+ loop = asyncio.new_event_loop()
590
+ asyncio.set_event_loop(loop)
985
591
 
986
- logger.info(
987
- f"Successfully deserialized Flock instance '{flock_instance.name}' with {len(flock_instance._agents)} agents"
592
+ coro = self.evaluate_async(
593
+ dataset=dataset,
594
+ start_agent=start_agent,
595
+ input_mapping=input_mapping,
596
+ answer_mapping=answer_mapping,
597
+ metrics=metrics,
598
+ metric_configs=metric_configs,
599
+ static_inputs=static_inputs,
600
+ parallel=parallel,
601
+ max_workers=max_workers,
602
+ use_temporal=use_temporal,
603
+ error_handling=error_handling,
604
+ output_file=output_file,
605
+ return_dataframe=return_dataframe,
606
+ silent_mode=silent_mode,
607
+ metadata_columns=metadata_columns,
988
608
  )
989
- return flock_instance
990
-
991
- @classmethod
992
- def _register_type_definitions(cls, type_defs: dict[str, Any]) -> None:
993
- """Register type definitions from serialized data."""
994
- import importlib
995
-
996
- from flock.core.flock_registry import get_registry
997
-
998
- registry = get_registry()
999
-
1000
- for type_name, type_def in type_defs.items():
1001
- logger.debug(f"Registering type: {type_name}")
1002
-
1003
- try:
1004
- # First try to import the type directly
1005
- module_path = type_def.get("module_path")
1006
- if module_path:
1007
- try:
1008
- module = importlib.import_module(module_path)
1009
- if hasattr(module, type_name):
1010
- type_obj = getattr(module, type_name)
1011
- registry.register_type(type_obj, type_name)
1012
- logger.info(
1013
- f"Registered type {type_name} from module {module_path}"
1014
- )
1015
- continue
1016
- except ImportError:
1017
- logger.debug(
1018
- f"Could not import {module_path}, trying dynamic type creation"
1019
- )
1020
-
1021
- # If direct import fails, try to create the type dynamically
1022
- if (
1023
- type_def.get("type") == "pydantic.BaseModel"
1024
- and "schema" in type_def
1025
- ):
1026
- cls._create_pydantic_model(type_name, type_def)
1027
- elif (
1028
- type_def.get("type") == "dataclass" and "fields" in type_def
1029
- ):
1030
- cls._create_dataclass(type_name, type_def)
1031
- else:
1032
- logger.warning(
1033
- f"Unsupported type definition for {type_name}, type: {type_def.get('type')}"
1034
- )
1035
-
1036
- except Exception as e:
1037
- logger.error(f"Failed to register type {type_name}: {e}")
1038
-
1039
- @classmethod
1040
- def _create_pydantic_model(
1041
- cls, type_name: str, type_def: dict[str, Any]
1042
- ) -> None:
1043
- """Dynamically create a Pydantic model from a schema definition."""
1044
- from pydantic import create_model
1045
-
1046
- from flock.core.flock_registry import get_registry
1047
-
1048
- registry = get_registry()
1049
- schema = type_def.get("schema", {})
1050
-
1051
- try:
1052
- # Extract field definitions from schema
1053
- fields = {}
1054
- properties = schema.get("properties", {})
1055
- required = schema.get("required", [])
1056
-
1057
- for field_name, field_schema in properties.items():
1058
- # Determine the field type based on schema
1059
- field_type = cls._get_type_from_schema(field_schema)
1060
-
1061
- # Determine if field is required
1062
- default = ... if field_name in required else None
1063
-
1064
- # Add to fields dict
1065
- fields[field_name] = (field_type, default)
1066
-
1067
- # Create the model
1068
- DynamicModel = create_model(type_name, **fields)
1069
-
1070
- # Register it
1071
- registry.register_type(DynamicModel, type_name)
1072
- logger.info(f"Created and registered Pydantic model: {type_name}")
1073
609
 
1074
- except Exception as e:
1075
- logger.error(f"Failed to create Pydantic model {type_name}: {e}")
1076
-
1077
- @classmethod
1078
- def _get_type_from_schema(cls, field_schema: dict[str, Any]) -> Any:
1079
- """Convert JSON schema type to Python type."""
1080
- schema_type = field_schema.get("type")
1081
-
1082
- # Basic type mapping
1083
- type_mapping = {
1084
- "string": str,
1085
- "integer": int,
1086
- "number": float,
1087
- "boolean": bool,
1088
- "array": list,
1089
- "object": dict,
1090
- }
1091
-
1092
- # Handle basic types
1093
- if schema_type in type_mapping:
1094
- return type_mapping[schema_type]
1095
-
1096
- # Handle enums
1097
- if "enum" in field_schema:
1098
- from typing import Literal
1099
-
1100
- return Literal[tuple(field_schema["enum"])]
1101
-
1102
- # Default
1103
- return Any
1104
-
1105
- @classmethod
1106
- def _create_dataclass(
1107
- cls, type_name: str, type_def: dict[str, Any]
1108
- ) -> None:
1109
- """Dynamically create a dataclass from a field definition."""
1110
- from dataclasses import make_dataclass
1111
-
1112
- from flock.core.flock_registry import get_registry
1113
-
1114
- registry = get_registry()
1115
- fields_def = type_def.get("fields", {})
1116
-
1117
- try:
1118
- fields = []
1119
- for field_name, field_props in fields_def.items():
1120
- field_type = eval(
1121
- field_props.get("type", "str")
1122
- ) # Note: eval is used here for simplicity
1123
- fields.append((field_name, field_type))
1124
-
1125
- # Create the dataclass
1126
- DynamicDataclass = make_dataclass(type_name, fields)
1127
-
1128
- # Register it
1129
- registry.register_type(DynamicDataclass, type_name)
1130
- logger.info(f"Created and registered dataclass: {type_name}")
1131
-
1132
- except Exception as e:
1133
- logger.error(f"Failed to create dataclass {type_name}: {e}")
1134
-
1135
- @classmethod
1136
- def _register_component_definitions(
1137
- cls,
1138
- component_defs: dict[str, Any],
1139
- path_type: Literal["absolute", "relative"],
1140
- ) -> None:
1141
- """Register component definitions from serialized data."""
1142
- import importlib
1143
- import importlib.util
1144
- import os
1145
- import sys
1146
-
1147
- from flock.core.flock_registry import get_registry
1148
-
1149
- registry = get_registry()
1150
-
1151
- for component_name, component_def in component_defs.items():
1152
- logger.debug(f"Registering component: {component_name}")
1153
- component_type = component_def.get("type", "flock_component")
1154
-
1155
- try:
1156
- # Handle callables differently than components
1157
- if component_type == "flock_callable":
1158
- # For callables, component_name is just the function name
1159
- func_name = component_name
1160
- module_path = component_def.get("module_path")
1161
- file_path = component_def.get("file_path")
1162
-
1163
- # Convert relative path to absolute if needed
1164
- if (
1165
- path_type == "relative"
1166
- and file_path
1167
- and not os.path.isabs(file_path)
1168
- ):
1169
- try:
1170
- # Make absolute based on current directory
1171
- file_path = os.path.abspath(file_path)
1172
- logger.debug(
1173
- f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
1174
- )
1175
- except Exception as e:
1176
- logger.warning(
1177
- f"Could not convert relative path to absolute: {e}"
1178
- )
1179
-
1180
- logger.debug(
1181
- f"Processing callable '{func_name}' from module '{module_path}', file: {file_path}"
1182
- )
1183
-
1184
- # Try direct import first
1185
- if module_path:
1186
- try:
1187
- logger.debug(
1188
- f"Attempting to import module: {module_path}"
1189
- )
1190
- module = importlib.import_module(module_path)
1191
- if hasattr(module, func_name):
1192
- callable_obj = getattr(module, func_name)
1193
- # Register with just the name for easier lookup
1194
- registry.register_callable(
1195
- callable_obj, func_name
1196
- )
1197
- logger.info(
1198
- f"Registered callable with name: {func_name}"
1199
- )
1200
- # Also register with fully qualified path for compatibility
1201
- if module_path != "__main__":
1202
- full_path = f"{module_path}.{func_name}"
1203
- registry.register_callable(
1204
- callable_obj, full_path
1205
- )
1206
- logger.info(
1207
- f"Also registered callable with full path: {full_path}"
1208
- )
1209
- logger.info(
1210
- f"Successfully registered callable {func_name} from module {module_path}"
1211
- )
1212
- continue
1213
- else:
1214
- logger.warning(
1215
- f"Function '{func_name}' not found in module {module_path}"
1216
- )
1217
- except ImportError:
1218
- logger.debug(
1219
- f"Could not import module {module_path}, trying file path"
1220
- )
1221
-
1222
- # Try file path if module import fails
1223
- if file_path and os.path.exists(file_path):
1224
- try:
1225
- logger.debug(
1226
- f"Attempting to load file: {file_path}"
1227
- )
1228
- # Create a module name from file path
1229
- mod_name = f"{func_name}_module"
1230
- spec = importlib.util.spec_from_file_location(
1231
- mod_name, file_path
1232
- )
1233
- if spec and spec.loader:
1234
- module = importlib.util.module_from_spec(spec)
1235
- sys.modules[spec.name] = module
1236
- spec.loader.exec_module(module)
1237
- logger.debug(
1238
- f"Successfully loaded module from file, searching for function '{func_name}'"
1239
- )
1240
-
1241
- # Look for the function in the loaded module
1242
- if hasattr(module, func_name):
1243
- callable_obj = getattr(module, func_name)
1244
- registry.register_callable(
1245
- callable_obj, func_name
1246
- )
1247
- logger.info(
1248
- f"Successfully registered callable {func_name} from file {file_path}"
1249
- )
1250
- else:
1251
- logger.warning(
1252
- f"Function {func_name} not found in file {file_path}"
1253
- )
1254
- else:
1255
- logger.warning(
1256
- f"Could not create import spec for {file_path}"
1257
- )
1258
- except Exception as e:
1259
- logger.error(
1260
- f"Error loading callable {func_name} from file {file_path}: {e}",
1261
- exc_info=True,
1262
- )
1263
-
1264
- # Handle regular components (existing code)
1265
- else:
1266
- # First try using the module path (Python import)
1267
- module_path = component_def.get("module_path")
1268
- if module_path and module_path != "unknown":
1269
- try:
1270
- logger.debug(
1271
- f"Attempting to import module '{module_path}' for component '{component_name}'"
1272
- )
1273
- module = importlib.import_module(module_path)
1274
- # Find the component class in the module
1275
- for attr_name in dir(module):
1276
- if attr_name == component_name:
1277
- component_class = getattr(module, attr_name)
1278
- registry.register_component(
1279
- component_class, component_name
1280
- )
1281
- logger.info(
1282
- f"Registered component {component_name} from {module_path}"
1283
- )
1284
- break
1285
- else:
1286
- logger.warning(
1287
- f"Component {component_name} not found in module {module_path}"
1288
- )
1289
- # If we didn't find the component, try using file_path next
1290
- raise ImportError(
1291
- f"Component {component_name} not found in module {module_path}"
1292
- )
1293
- except ImportError:
1294
- # If module import fails, try file_path approach
1295
- file_path = component_def.get("file_path")
1296
-
1297
- # Convert relative path to absolute if needed
1298
- if (
1299
- path_type == "relative"
1300
- and file_path
1301
- and not os.path.isabs(file_path)
1302
- ):
1303
- try:
1304
- # Make absolute based on current directory
1305
- file_path = os.path.abspath(file_path)
1306
- logger.debug(
1307
- f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
1308
- )
1309
- except Exception as e:
1310
- logger.warning(
1311
- f"Could not convert relative path to absolute: {e}"
1312
- )
1313
-
1314
- if file_path and os.path.exists(file_path):
1315
- logger.debug(
1316
- f"Attempting to load {component_name} from file: {file_path}"
1317
- )
1318
- try:
1319
- # Load the module from file path
1320
- spec = (
1321
- importlib.util.spec_from_file_location(
1322
- f"{component_name}_module",
1323
- file_path,
1324
- )
1325
- )
1326
- if spec and spec.loader:
1327
- module = (
1328
- importlib.util.module_from_spec(
1329
- spec
1330
- )
1331
- )
1332
- sys.modules[spec.name] = module
1333
- spec.loader.exec_module(module)
1334
- logger.debug(
1335
- f"Successfully loaded module from file, searching for component class '{component_name}'"
1336
- )
1337
-
1338
- # Find the component class in the loaded module
1339
- for attr_name in dir(module):
1340
- if attr_name == component_name:
1341
- component_class = getattr(
1342
- module, attr_name
1343
- )
1344
- registry.register_component(
1345
- component_class,
1346
- component_name,
1347
- )
1348
- logger.info(
1349
- f"Registered component {component_name} from file {file_path}"
1350
- )
1351
- break
1352
- else:
1353
- logger.warning(
1354
- f"Component {component_name} not found in file {file_path}"
1355
- )
1356
- except Exception as e:
1357
- logger.error(
1358
- f"Error loading component {component_name} from file {file_path}: {e}",
1359
- exc_info=True,
1360
- )
1361
- else:
1362
- logger.warning(
1363
- f"No valid file path found for component {component_name}"
1364
- )
1365
- else:
1366
- logger.warning(
1367
- f"Missing or unknown module path for component {component_name}"
1368
- )
1369
- except Exception as e:
1370
- logger.error(
1371
- f"Failed to register component {component_name}: {e}",
1372
- exc_info=True,
1373
- )
610
+ if asyncio.get_event_loop() is loop and not loop.is_running():
611
+ results = loop.run_until_complete(coro)
612
+ return results
613
+ else:
614
+ future = asyncio.ensure_future(coro)
615
+ return loop.run_until_complete(future)
1374
616
 
1375
- @classmethod
1376
- def _check_dependencies(cls, dependencies: list[str]) -> None:
1377
- """Check if required dependencies are available."""
1378
- import importlib
1379
- import re
1380
-
1381
- for dependency in dependencies:
1382
- # Extract package name and version
1383
- match = re.match(r"([^>=<]+)([>=<].+)?", dependency)
1384
- if match:
1385
- package_name = match.group(1)
1386
- try:
1387
- importlib.import_module(package_name.replace("-", "_"))
1388
- logger.debug(f"Dependency {package_name} is available")
1389
- except ImportError:
1390
- logger.warning(f"Dependency {dependency} is not installed")
1391
-
1392
- # --- API Start Method ---
617
+ # --- API Server Starter ---
1393
618
  def start_api(
1394
619
  self,
1395
620
  host: str = "127.0.0.1",
@@ -1397,98 +622,46 @@ class Flock(BaseModel, Serializable):
1397
622
  server_name: str = "Flock API",
1398
623
  create_ui: bool = False,
1399
624
  ) -> None:
1400
- """Start a REST API server for this Flock instance."""
1401
- # Import locally to avoid making API components a hard dependency
1402
- try:
1403
- from flock.core.api import FlockAPI
1404
- except ImportError:
1405
- logger.error(
1406
- "API components not found. Cannot start API. "
1407
- "Ensure 'fastapi' and 'uvicorn' are installed."
1408
- )
1409
- return
625
+ """Starts a REST API server for this Flock instance."""
626
+ # Import runner locally
627
+ from flock.core.api.runner import start_flock_api
1410
628
 
1411
- logger.info(
1412
- f"Preparing to start API server on {host}:{port} {'with UI' if create_ui else 'without UI'}"
1413
- )
1414
- api_instance = FlockAPI(self) # Pass the current Flock instance
1415
- # Use the start method of FlockAPI
1416
- api_instance.start(
1417
- host=host, port=port, server_name=server_name, create_ui=create_ui
1418
- )
629
+ start_flock_api(self, host, port, server_name, create_ui)
1419
630
 
1420
- # --- CLI Start Method ---
631
+ # --- CLI Starter ---
1421
632
  def start_cli(
1422
633
  self,
1423
634
  server_name: str = "Flock CLI",
1424
635
  show_results: bool = False,
1425
636
  edit_mode: bool = False,
1426
637
  ) -> None:
1427
- """Start a CLI interface for this Flock instance.
638
+ """Starts an interactive CLI for this Flock instance."""
639
+ # Import runner locally
640
+ from flock.cli.runner import start_flock_cli
1428
641
 
1429
- This method loads the CLI with the current Flock instance already available,
1430
- allowing users to execute, edit, or manage agents from the existing configuration.
642
+ start_flock_cli(self, server_name, show_results, edit_mode)
1431
643
 
1432
- Args:
1433
- server_name: Optional name for the CLI interface
1434
- show_results: Whether to initially show results of previous runs
1435
- edit_mode: Whether to open directly in edit mode
1436
- """
1437
- # Import locally to avoid circular imports
1438
- try:
1439
- from flock.cli.loaded_flock_cli import start_loaded_flock_cli
1440
- except ImportError:
1441
- logger.error(
1442
- "CLI components not found. Cannot start CLI. "
1443
- "Ensure the CLI modules are properly installed."
1444
- )
1445
- return
644
+ # --- Serialization Delegation Methods ---
645
+ def to_dict(self, path_type: str = "relative") -> dict[str, Any]:
646
+ """Serialize Flock instance to dictionary using FlockSerializer."""
647
+ # Import locally to prevent circular imports at module level if structure is complex
648
+ from flock.core.serialization.flock_serializer import FlockSerializer
1446
649
 
1447
- logger.info(
1448
- f"Starting CLI interface with loaded Flock instance ({len(self._agents)} agents)"
1449
- )
650
+ return FlockSerializer.serialize(self, path_type=path_type)
1450
651
 
1451
- # Pass the current Flock instance to the CLI
1452
- start_loaded_flock_cli(
1453
- flock=self,
1454
- server_name=server_name,
1455
- show_results=show_results,
1456
- edit_mode=edit_mode,
1457
- )
652
+ @classmethod
653
+ def from_dict(cls: type[T], data: dict[str, Any]) -> T:
654
+ """Deserialize Flock instance from dictionary using FlockSerializer."""
655
+ # Import locally
656
+ from flock.core.serialization.flock_serializer import FlockSerializer
657
+
658
+ return FlockSerializer.deserialize(cls, data)
1458
659
 
1459
- # --- Static Method Loaders (Keep for convenience) ---
660
+ # --- Static Method Loader (Delegates to loader module) ---
1460
661
  @staticmethod
1461
662
  def load_from_file(file_path: str) -> Flock:
1462
- """Load a Flock instance from various file formats (detects type)."""
1463
- p = Path(file_path)
1464
- if not p.exists():
1465
- raise FileNotFoundError(f"Flock file not found: {file_path}")
663
+ """Load a Flock instance from various file formats (delegates to loader)."""
664
+ # Import locally
665
+ from flock.core.util.loader import load_flock_from_file
1466
666
 
1467
- try:
1468
- if p.suffix in [".yaml", ".yml"]:
1469
- return Flock.from_yaml_file(p)
1470
- elif p.suffix == ".json":
1471
- return Flock.from_json(p.read_text())
1472
- elif p.suffix == ".msgpack":
1473
- return Flock.from_msgpack_file(p)
1474
- elif p.suffix == ".pkl":
1475
- if PICKLE_AVAILABLE:
1476
- return Flock.from_pickle_file(p)
1477
- else:
1478
- raise RuntimeError(
1479
- "Cannot load Pickle file: cloudpickle not installed."
1480
- )
1481
- else:
1482
- raise ValueError(
1483
- f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
1484
- )
1485
- except Exception as e:
1486
- # Check if it's an exception about missing types
1487
- if "Could not get registered type name" in str(e):
1488
- logger.error(
1489
- f"Failed to load Flock from {file_path}: Missing type definition. "
1490
- "This may happen if the YAML was created on a system with different types registered. "
1491
- "Check if the file includes 'types' section with necessary type definitions."
1492
- )
1493
- logger.error(f"Error loading Flock from {file_path}: {e}")
1494
- raise
667
+ return load_flock_from_file(file_path)