flock-core 0.4.0b3__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,1054 +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
- parts = part.split(":")
621
- if len(parts) > 1:
622
- type_part = parts[1].strip()
623
-
624
- pydantic_models = extract_pydantic_models_from_type_string(
625
- type_part
626
- )
627
- if pydantic_models:
628
- for model in pydantic_models:
629
- custom_types.append(model.__name__)
630
-
631
- # # Extract from list[Type]
632
- # if "list[" in type_part:
633
- # inner_type = type_part.split("list[")[1].split("]")[0].strip()
634
- # if inner_type and inner_type.lower() not in [
635
- # "str",
636
- # "int",
637
- # "float",
638
- # "bool",
639
- # "dict",
640
- # "list",
641
- # ]:
642
- # custom_types.append(inner_type)
643
-
644
- # # Extract direct type references
645
- # elif type_part and type_part.lower() not in [
646
- # "str",
647
- # "int",
648
- # "float",
649
- # "bool",
650
- # "dict",
651
- # "list",
652
- # ]:
653
- # custom_types.append(
654
- # type_part.split()[0]
655
- # ) # Take the first word in case there's a description
656
-
657
- return custom_types
658
-
659
- def _get_type_definitions(self, type_names: list[str]) -> dict[str, Any]:
660
- """Get definitions for the specified custom types."""
661
- from flock.core.flock_registry import get_registry
662
-
663
- type_definitions = {}
664
- registry = get_registry()
665
-
666
- for type_name in type_names:
667
- try:
668
- # Try to get the type from registry
669
- type_obj = registry._types.get(type_name)
670
- if type_obj:
671
- type_def = self._extract_type_definition(
672
- type_name, type_obj
673
- )
674
- if type_def:
675
- type_definitions[type_name] = type_def
676
- except Exception as e:
677
- logger.warning(
678
- f"Could not extract definition for type {type_name}: {e}"
679
- )
680
-
681
- return type_definitions
682
-
683
- def _extract_type_definition(
684
- self, type_name: str, type_obj: type
685
- ) -> dict[str, Any]:
686
- """Extract a definition for a custom type."""
687
- import inspect
688
- from dataclasses import is_dataclass
689
-
690
- type_def = {
691
- "module_path": type_obj.__module__,
692
- }
693
-
694
- # Handle Pydantic models
695
- if hasattr(type_obj, "model_json_schema") and callable(
696
- getattr(type_obj, "model_json_schema")
697
- ):
698
- type_def["type"] = "pydantic.BaseModel"
699
- try:
700
- schema = type_obj.model_json_schema()
701
- # Clean up schema to remove unnecessary fields
702
- if "title" in schema and schema["title"] == type_name:
703
- del schema["title"]
704
- type_def["schema"] = schema
705
- except Exception as e:
706
- logger.warning(
707
- f"Could not extract schema for Pydantic model {type_name}: {e}"
708
- )
709
-
710
- # Handle dataclasses
711
- elif is_dataclass(type_obj):
712
- type_def["type"] = "dataclass"
713
- fields = {}
714
- for field_name, field in type_obj.__dataclass_fields__.items():
715
- fields[field_name] = {
716
- "type": str(field.type),
717
- "default": str(field.default)
718
- if field.default is not inspect.Parameter.empty
719
- else None,
720
- }
721
- type_def["fields"] = fields
722
-
723
- # Handle other types - just store basic information
724
- else:
725
- type_def["type"] = "custom"
726
-
727
- # Extract import statement (simplified version)
728
- type_def["imports"] = [f"from {type_obj.__module__} import {type_name}"]
729
-
730
- return type_def
731
-
732
- def _get_component_definition(
733
- self, component_type: str, path_type: Literal["absolute", "relative"]
734
- ) -> dict[str, Any]:
735
- """Get definition for a component type."""
736
- import os
737
- import sys
738
-
739
- from flock.core.flock_registry import get_registry
740
-
741
- registry = get_registry()
742
- component_def = {}
743
-
744
- try:
745
- # Try to get the component class from registry
746
- component_class = registry._components.get(component_type)
747
- if component_class:
748
- # Get the standard module path
749
- module_path = component_class.__module__
750
-
751
- # Get the actual file system path if possible
752
- file_path = None
753
- try:
754
- if (
755
- hasattr(component_class, "__module__")
756
- and component_class.__module__
757
- ):
758
- module = sys.modules.get(component_class.__module__)
759
- if module and hasattr(module, "__file__"):
760
- file_path = os.path.abspath(module.__file__)
761
- # Convert to relative path if needed
762
- if path_type == "relative" and file_path:
763
- try:
764
- file_path = os.path.relpath(file_path)
765
- except ValueError:
766
- # Keep as absolute if can't make relative
767
- logger.warning(
768
- f"Could not convert path to relative: {file_path}"
769
- )
770
- except Exception as e:
771
- # If we can't get the file path, we'll just use the module path
772
- logger.warning(
773
- f"Error getting file path for component {component_type}: {e}"
774
- )
775
- pass
776
-
777
- component_def = {
778
- "type": "flock_component",
779
- "module_path": module_path,
780
- "file_path": file_path, # Include actual file system path
781
- "description": getattr(
782
- component_class, "__doc__", ""
783
- ).strip()
784
- or f"{component_type} component",
428
+ "details": f"Flock run '{self.name}' failed.",
785
429
  }
786
- except Exception as e:
787
- logger.warning(
788
- f"Could not extract definition for component {component_type}: {e}"
789
- )
790
- # Provide minimal information if we can't extract details
791
- component_def = {
792
- "type": "flock_component",
793
- "module_path": "unknown",
794
- "file_path": None,
795
- "description": f"{component_type} component (definition incomplete)",
796
- }
797
430
 
798
- return component_def
799
-
800
- def _get_callable_definition(
431
+ # --- Batch Processing (Delegation) ---
432
+ async def run_batch_async(
801
433
  self,
802
- callable_ref: str,
803
- func_name: str,
804
- path_type: Literal["absolute", "relative"],
805
- ) -> dict[str, Any]:
806
- """Get definition for a callable reference.
807
-
808
- Args:
809
- callable_ref: The fully qualified path to the callable
810
- func_name: The simple function name (for display purposes)
811
- path_type: How file paths should be formatted ('absolute' or 'relative')
812
- """
813
- import inspect
814
- import os
815
- import sys
816
-
817
- from flock.core.flock_registry import get_registry
818
-
819
- registry = get_registry()
820
- 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
+ )
821
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)
822
481
  try:
823
- # Try to get the callable from registry
824
- logger.debug(
825
- f"Getting callable definition for '{callable_ref}' (display name: '{func_name}')"
826
- )
827
- func = registry.get_callable(callable_ref)
828
- if func:
829
- # Get the standard module path
830
- module_path = func.__module__
831
-
832
- # Get the actual file system path if possible
833
- file_path = None
834
- try:
835
- if func.__module__ and func.__module__ != "builtins":
836
- module = sys.modules.get(func.__module__)
837
- if module and hasattr(module, "__file__"):
838
- file_path = os.path.abspath(module.__file__)
839
- # Convert to relative path if needed
840
- if path_type == "relative" and file_path:
841
- try:
842
- file_path = os.path.relpath(file_path)
843
- except ValueError:
844
- # Keep as absolute if can't make relative
845
- logger.warning(
846
- f"Could not convert path to relative: {file_path}"
847
- )
848
- except Exception as e:
849
- # If we can't get the file path, just use the module path
850
- logger.warning(
851
- f"Error getting file path for callable {callable_ref}: {e}"
852
- )
853
- 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)
854
488
 
855
- # Get the docstring for description
856
- docstring = (
857
- inspect.getdoc(func) or f"Callable function {func_name}"
858
- )
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
+ )
859
502
 
860
- callable_def = {
861
- "type": "flock_callable",
862
- "module_path": module_path,
863
- "file_path": file_path,
864
- "description": docstring.strip(),
865
- }
866
- logger.debug(
867
- f"Created callable definition for '{func_name}': module={module_path}, file={file_path}"
868
- )
869
- except Exception as e:
870
- logger.warning(
871
- f"Could not extract definition for callable {callable_ref}: {e}"
872
- )
873
- # Provide minimal information
874
- callable_def = {
875
- "type": "flock_callable",
876
- "module_path": callable_ref.split(".")[0]
877
- if "." in callable_ref
878
- else "unknown",
879
- "file_path": None,
880
- "description": f"Callable {func_name} (definition incomplete)",
881
- }
882
-
883
- return callable_def
884
-
885
- def _get_dependencies(self) -> list[str]:
886
- """Get list of dependencies required by this Flock."""
887
- # This is a simplified version - in production, you might want to detect
888
- # actual versions of installed packages
889
- return [
890
- "pydantic>=2.0.0",
891
- "flock>=0.3.41", # Assuming this is the package name
892
- ]
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)
893
509
 
894
- @classmethod
895
- def from_dict(cls: type[T], data: dict[str, Any]) -> T:
896
- """Create Flock instance from dictionary representation."""
897
- logger.debug(
898
- 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,
899
538
  )
900
539
 
901
- # Check for serialization settings
902
- serialization_settings = data.pop("serialization_settings", {})
903
- path_type = serialization_settings.get("path_type", "absolute")
904
- logger.debug(
905
- 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,
906
557
  )
907
558
 
908
- # First, handle type definitions if present
909
- if "types" in data:
910
- logger.info(f"Processing {len(data['types'])} type definitions")
911
- cls._register_type_definitions(data["types"])
912
-
913
- # Then, handle component definitions if present
914
- if "components" in data:
915
- logger.info(
916
- f"Processing {len(data['components'])} component definitions"
917
- )
918
- cls._register_component_definitions(data["components"], path_type)
919
-
920
- # Check dependencies if present
921
- if "dependencies" in data:
922
- logger.debug(f"Checking {len(data['dependencies'])} dependencies")
923
- cls._check_dependencies(data["dependencies"])
924
-
925
- # Ensure FlockAgent is importable for type checking later
926
- try:
927
- from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
928
- except ImportError:
929
- logger.error(
930
- "Cannot import FlockAgent, deserialization may fail for agents."
931
- )
932
- ConcreteFlockAgent = Any # Fallback
933
-
934
- # Extract agent data before initializing Flock base model
935
- agents_data = data.pop("agents", {})
936
- logger.info(f"Found {len(agents_data)} agents to deserialize")
937
-
938
- # Remove types, components, and dependencies sections as they're not part of Flock fields
939
- data.pop("types", None)
940
- data.pop("components", None)
941
- data.pop("dependencies", None)
942
- # Remove metadata if present
943
- data.pop("metadata", None)
944
-
945
- # 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)
946
584
  try:
947
- # Pass only fields defined in Flock's Pydantic model
948
- init_data = {k: v for k, v in data.items() if k in cls.model_fields}
949
- logger.debug(
950
- f"Creating Flock instance with fields: {list(init_data.keys())}"
951
- )
952
- flock_instance = cls(**init_data)
953
- except Exception as e:
954
- logger.error(
955
- f"Pydantic validation/init failed for Flock: {e}", exc_info=True
956
- )
957
- raise ValueError(
958
- f"Failed to initialize Flock from dict: {e}"
959
- ) from e
960
-
961
- # Deserialize and add agents AFTER Flock instance exists
962
- for name, agent_data in agents_data.items():
963
- try:
964
- logger.debug(f"Deserializing agent '{name}'")
965
- # Ensure agent_data has the name, or add it from the key
966
- agent_data.setdefault("name", name)
967
- # Use FlockAgent's from_dict method
968
- agent_instance = ConcreteFlockAgent.from_dict(agent_data)
969
- flock_instance.add_agent(
970
- agent_instance
971
- ) # Adds to _agents and registers
972
- logger.debug(f"Successfully added agent '{name}' to Flock")
973
- except Exception as e:
974
- logger.error(
975
- f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
976
- exc_info=True,
977
- )
978
- # 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)
979
591
 
980
- logger.info(
981
- 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,
982
608
  )
983
- return flock_instance
984
-
985
- @classmethod
986
- def _register_type_definitions(cls, type_defs: dict[str, Any]) -> None:
987
- """Register type definitions from serialized data."""
988
- import importlib
989
-
990
- from flock.core.flock_registry import get_registry
991
-
992
- registry = get_registry()
993
-
994
- for type_name, type_def in type_defs.items():
995
- logger.debug(f"Registering type: {type_name}")
996
-
997
- try:
998
- # First try to import the type directly
999
- module_path = type_def.get("module_path")
1000
- if module_path:
1001
- try:
1002
- module = importlib.import_module(module_path)
1003
- if hasattr(module, type_name):
1004
- type_obj = getattr(module, type_name)
1005
- registry.register_type(type_obj, type_name)
1006
- logger.info(
1007
- f"Registered type {type_name} from module {module_path}"
1008
- )
1009
- continue
1010
- except ImportError:
1011
- logger.debug(
1012
- f"Could not import {module_path}, trying dynamic type creation"
1013
- )
1014
-
1015
- # If direct import fails, try to create the type dynamically
1016
- if (
1017
- type_def.get("type") == "pydantic.BaseModel"
1018
- and "schema" in type_def
1019
- ):
1020
- cls._create_pydantic_model(type_name, type_def)
1021
- elif (
1022
- type_def.get("type") == "dataclass" and "fields" in type_def
1023
- ):
1024
- cls._create_dataclass(type_name, type_def)
1025
- else:
1026
- logger.warning(
1027
- f"Unsupported type definition for {type_name}, type: {type_def.get('type')}"
1028
- )
1029
-
1030
- except Exception as e:
1031
- logger.error(f"Failed to register type {type_name}: {e}")
1032
-
1033
- @classmethod
1034
- def _create_pydantic_model(
1035
- cls, type_name: str, type_def: dict[str, Any]
1036
- ) -> None:
1037
- """Dynamically create a Pydantic model from a schema definition."""
1038
- from pydantic import create_model
1039
-
1040
- from flock.core.flock_registry import get_registry
1041
-
1042
- registry = get_registry()
1043
- schema = type_def.get("schema", {})
1044
-
1045
- try:
1046
- # Extract field definitions from schema
1047
- fields = {}
1048
- properties = schema.get("properties", {})
1049
- required = schema.get("required", [])
1050
-
1051
- for field_name, field_schema in properties.items():
1052
- # Determine the field type based on schema
1053
- field_type = cls._get_type_from_schema(field_schema)
1054
-
1055
- # Determine if field is required
1056
- default = ... if field_name in required else None
1057
-
1058
- # Add to fields dict
1059
- fields[field_name] = (field_type, default)
1060
-
1061
- # Create the model
1062
- DynamicModel = create_model(type_name, **fields)
1063
-
1064
- # Register it
1065
- registry.register_type(DynamicModel, type_name)
1066
- logger.info(f"Created and registered Pydantic model: {type_name}")
1067
-
1068
- except Exception as e:
1069
- logger.error(f"Failed to create Pydantic model {type_name}: {e}")
1070
609
 
1071
- @classmethod
1072
- def _get_type_from_schema(cls, field_schema: dict[str, Any]) -> Any:
1073
- """Convert JSON schema type to Python type."""
1074
- schema_type = field_schema.get("type")
1075
-
1076
- # Basic type mapping
1077
- type_mapping = {
1078
- "string": str,
1079
- "integer": int,
1080
- "number": float,
1081
- "boolean": bool,
1082
- "array": list,
1083
- "object": dict,
1084
- }
1085
-
1086
- # Handle basic types
1087
- if schema_type in type_mapping:
1088
- return type_mapping[schema_type]
1089
-
1090
- # Handle enums
1091
- if "enum" in field_schema:
1092
- from typing import Literal
1093
-
1094
- return Literal[tuple(field_schema["enum"])]
1095
-
1096
- # Default
1097
- return Any
1098
-
1099
- @classmethod
1100
- def _create_dataclass(
1101
- cls, type_name: str, type_def: dict[str, Any]
1102
- ) -> None:
1103
- """Dynamically create a dataclass from a field definition."""
1104
- from dataclasses import make_dataclass
1105
-
1106
- from flock.core.flock_registry import get_registry
1107
-
1108
- registry = get_registry()
1109
- fields_def = type_def.get("fields", {})
1110
-
1111
- try:
1112
- fields = []
1113
- for field_name, field_props in fields_def.items():
1114
- field_type = eval(
1115
- field_props.get("type", "str")
1116
- ) # Note: eval is used here for simplicity
1117
- fields.append((field_name, field_type))
1118
-
1119
- # Create the dataclass
1120
- DynamicDataclass = make_dataclass(type_name, fields)
1121
-
1122
- # Register it
1123
- registry.register_type(DynamicDataclass, type_name)
1124
- logger.info(f"Created and registered dataclass: {type_name}")
1125
-
1126
- except Exception as e:
1127
- logger.error(f"Failed to create dataclass {type_name}: {e}")
1128
-
1129
- @classmethod
1130
- def _register_component_definitions(
1131
- cls,
1132
- component_defs: dict[str, Any],
1133
- path_type: Literal["absolute", "relative"],
1134
- ) -> None:
1135
- """Register component definitions from serialized data."""
1136
- import importlib
1137
- import importlib.util
1138
- import os
1139
- import sys
1140
-
1141
- from flock.core.flock_registry import get_registry
1142
-
1143
- registry = get_registry()
1144
-
1145
- for component_name, component_def in component_defs.items():
1146
- logger.debug(f"Registering component: {component_name}")
1147
- component_type = component_def.get("type", "flock_component")
1148
-
1149
- try:
1150
- # Handle callables differently than components
1151
- if component_type == "flock_callable":
1152
- # For callables, component_name is just the function name
1153
- func_name = component_name
1154
- module_path = component_def.get("module_path")
1155
- file_path = component_def.get("file_path")
1156
-
1157
- # Convert relative path to absolute if needed
1158
- if (
1159
- path_type == "relative"
1160
- and file_path
1161
- and not os.path.isabs(file_path)
1162
- ):
1163
- try:
1164
- # Make absolute based on current directory
1165
- file_path = os.path.abspath(file_path)
1166
- logger.debug(
1167
- f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
1168
- )
1169
- except Exception as e:
1170
- logger.warning(
1171
- f"Could not convert relative path to absolute: {e}"
1172
- )
1173
-
1174
- logger.debug(
1175
- f"Processing callable '{func_name}' from module '{module_path}', file: {file_path}"
1176
- )
1177
-
1178
- # Try direct import first
1179
- if module_path:
1180
- try:
1181
- logger.debug(
1182
- f"Attempting to import module: {module_path}"
1183
- )
1184
- module = importlib.import_module(module_path)
1185
- if hasattr(module, func_name):
1186
- callable_obj = getattr(module, func_name)
1187
- # Register with just the name for easier lookup
1188
- registry.register_callable(
1189
- callable_obj, func_name
1190
- )
1191
- logger.info(
1192
- f"Registered callable with name: {func_name}"
1193
- )
1194
- # Also register with fully qualified path for compatibility
1195
- if module_path != "__main__":
1196
- full_path = f"{module_path}.{func_name}"
1197
- registry.register_callable(
1198
- callable_obj, full_path
1199
- )
1200
- logger.info(
1201
- f"Also registered callable with full path: {full_path}"
1202
- )
1203
- logger.info(
1204
- f"Successfully registered callable {func_name} from module {module_path}"
1205
- )
1206
- continue
1207
- else:
1208
- logger.warning(
1209
- f"Function '{func_name}' not found in module {module_path}"
1210
- )
1211
- except ImportError:
1212
- logger.debug(
1213
- f"Could not import module {module_path}, trying file path"
1214
- )
1215
-
1216
- # Try file path if module import fails
1217
- if file_path and os.path.exists(file_path):
1218
- try:
1219
- logger.debug(
1220
- f"Attempting to load file: {file_path}"
1221
- )
1222
- # Create a module name from file path
1223
- mod_name = f"{func_name}_module"
1224
- spec = importlib.util.spec_from_file_location(
1225
- mod_name, file_path
1226
- )
1227
- if spec and spec.loader:
1228
- module = importlib.util.module_from_spec(spec)
1229
- sys.modules[spec.name] = module
1230
- spec.loader.exec_module(module)
1231
- logger.debug(
1232
- f"Successfully loaded module from file, searching for function '{func_name}'"
1233
- )
1234
-
1235
- # Look for the function in the loaded module
1236
- if hasattr(module, func_name):
1237
- callable_obj = getattr(module, func_name)
1238
- registry.register_callable(
1239
- callable_obj, func_name
1240
- )
1241
- logger.info(
1242
- f"Successfully registered callable {func_name} from file {file_path}"
1243
- )
1244
- else:
1245
- logger.warning(
1246
- f"Function {func_name} not found in file {file_path}"
1247
- )
1248
- else:
1249
- logger.warning(
1250
- f"Could not create import spec for {file_path}"
1251
- )
1252
- except Exception as e:
1253
- logger.error(
1254
- f"Error loading callable {func_name} from file {file_path}: {e}",
1255
- exc_info=True,
1256
- )
1257
-
1258
- # Handle regular components (existing code)
1259
- else:
1260
- # First try using the module path (Python import)
1261
- module_path = component_def.get("module_path")
1262
- if module_path and module_path != "unknown":
1263
- try:
1264
- logger.debug(
1265
- f"Attempting to import module '{module_path}' for component '{component_name}'"
1266
- )
1267
- module = importlib.import_module(module_path)
1268
- # Find the component class in the module
1269
- for attr_name in dir(module):
1270
- if attr_name == component_name:
1271
- component_class = getattr(module, attr_name)
1272
- registry.register_component(
1273
- component_class, component_name
1274
- )
1275
- logger.info(
1276
- f"Registered component {component_name} from {module_path}"
1277
- )
1278
- break
1279
- else:
1280
- logger.warning(
1281
- f"Component {component_name} not found in module {module_path}"
1282
- )
1283
- # If we didn't find the component, try using file_path next
1284
- raise ImportError(
1285
- f"Component {component_name} not found in module {module_path}"
1286
- )
1287
- except ImportError:
1288
- # If module import fails, try file_path approach
1289
- file_path = component_def.get("file_path")
1290
-
1291
- # Convert relative path to absolute if needed
1292
- if (
1293
- path_type == "relative"
1294
- and file_path
1295
- and not os.path.isabs(file_path)
1296
- ):
1297
- try:
1298
- # Make absolute based on current directory
1299
- file_path = os.path.abspath(file_path)
1300
- logger.debug(
1301
- f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
1302
- )
1303
- except Exception as e:
1304
- logger.warning(
1305
- f"Could not convert relative path to absolute: {e}"
1306
- )
1307
-
1308
- if file_path and os.path.exists(file_path):
1309
- logger.debug(
1310
- f"Attempting to load {component_name} from file: {file_path}"
1311
- )
1312
- try:
1313
- # Load the module from file path
1314
- spec = (
1315
- importlib.util.spec_from_file_location(
1316
- f"{component_name}_module",
1317
- file_path,
1318
- )
1319
- )
1320
- if spec and spec.loader:
1321
- module = (
1322
- importlib.util.module_from_spec(
1323
- spec
1324
- )
1325
- )
1326
- sys.modules[spec.name] = module
1327
- spec.loader.exec_module(module)
1328
- logger.debug(
1329
- f"Successfully loaded module from file, searching for component class '{component_name}'"
1330
- )
1331
-
1332
- # Find the component class in the loaded module
1333
- for attr_name in dir(module):
1334
- if attr_name == component_name:
1335
- component_class = getattr(
1336
- module, attr_name
1337
- )
1338
- registry.register_component(
1339
- component_class,
1340
- component_name,
1341
- )
1342
- logger.info(
1343
- f"Registered component {component_name} from file {file_path}"
1344
- )
1345
- break
1346
- else:
1347
- logger.warning(
1348
- f"Component {component_name} not found in file {file_path}"
1349
- )
1350
- except Exception as e:
1351
- logger.error(
1352
- f"Error loading component {component_name} from file {file_path}: {e}",
1353
- exc_info=True,
1354
- )
1355
- else:
1356
- logger.warning(
1357
- f"No valid file path found for component {component_name}"
1358
- )
1359
- else:
1360
- logger.warning(
1361
- f"Missing or unknown module path for component {component_name}"
1362
- )
1363
- except Exception as e:
1364
- logger.error(
1365
- f"Failed to register component {component_name}: {e}",
1366
- exc_info=True,
1367
- )
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)
1368
616
 
1369
- @classmethod
1370
- def _check_dependencies(cls, dependencies: list[str]) -> None:
1371
- """Check if required dependencies are available."""
1372
- import importlib
1373
- import re
1374
-
1375
- for dependency in dependencies:
1376
- # Extract package name and version
1377
- match = re.match(r"([^>=<]+)([>=<].+)?", dependency)
1378
- if match:
1379
- package_name = match.group(1)
1380
- try:
1381
- importlib.import_module(package_name.replace("-", "_"))
1382
- logger.debug(f"Dependency {package_name} is available")
1383
- except ImportError:
1384
- logger.warning(f"Dependency {dependency} is not installed")
1385
-
1386
- # --- API Start Method ---
617
+ # --- API Server Starter ---
1387
618
  def start_api(
1388
619
  self,
1389
620
  host: str = "127.0.0.1",
@@ -1391,98 +622,46 @@ class Flock(BaseModel, Serializable):
1391
622
  server_name: str = "Flock API",
1392
623
  create_ui: bool = False,
1393
624
  ) -> None:
1394
- """Start a REST API server for this Flock instance."""
1395
- # Import locally to avoid making API components a hard dependency
1396
- try:
1397
- from flock.core.api import FlockAPI
1398
- except ImportError:
1399
- logger.error(
1400
- "API components not found. Cannot start API. "
1401
- "Ensure 'fastapi' and 'uvicorn' are installed."
1402
- )
1403
- 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
1404
628
 
1405
- logger.info(
1406
- f"Preparing to start API server on {host}:{port} {'with UI' if create_ui else 'without UI'}"
1407
- )
1408
- api_instance = FlockAPI(self) # Pass the current Flock instance
1409
- # Use the start method of FlockAPI
1410
- api_instance.start(
1411
- host=host, port=port, server_name=server_name, create_ui=create_ui
1412
- )
629
+ start_flock_api(self, host, port, server_name, create_ui)
1413
630
 
1414
- # --- CLI Start Method ---
631
+ # --- CLI Starter ---
1415
632
  def start_cli(
1416
633
  self,
1417
634
  server_name: str = "Flock CLI",
1418
635
  show_results: bool = False,
1419
636
  edit_mode: bool = False,
1420
637
  ) -> None:
1421
- """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
1422
641
 
1423
- This method loads the CLI with the current Flock instance already available,
1424
- allowing users to execute, edit, or manage agents from the existing configuration.
642
+ start_flock_cli(self, server_name, show_results, edit_mode)
1425
643
 
1426
- Args:
1427
- server_name: Optional name for the CLI interface
1428
- show_results: Whether to initially show results of previous runs
1429
- edit_mode: Whether to open directly in edit mode
1430
- """
1431
- # Import locally to avoid circular imports
1432
- try:
1433
- from flock.cli.loaded_flock_cli import start_loaded_flock_cli
1434
- except ImportError:
1435
- logger.error(
1436
- "CLI components not found. Cannot start CLI. "
1437
- "Ensure the CLI modules are properly installed."
1438
- )
1439
- 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
1440
649
 
1441
- logger.info(
1442
- f"Starting CLI interface with loaded Flock instance ({len(self._agents)} agents)"
1443
- )
650
+ return FlockSerializer.serialize(self, path_type=path_type)
1444
651
 
1445
- # Pass the current Flock instance to the CLI
1446
- start_loaded_flock_cli(
1447
- flock=self,
1448
- server_name=server_name,
1449
- show_results=show_results,
1450
- edit_mode=edit_mode,
1451
- )
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)
1452
659
 
1453
- # --- Static Method Loaders (Keep for convenience) ---
660
+ # --- Static Method Loader (Delegates to loader module) ---
1454
661
  @staticmethod
1455
662
  def load_from_file(file_path: str) -> Flock:
1456
- """Load a Flock instance from various file formats (detects type)."""
1457
- p = Path(file_path)
1458
- if not p.exists():
1459
- 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
1460
666
 
1461
- try:
1462
- if p.suffix in [".yaml", ".yml"]:
1463
- return Flock.from_yaml_file(p)
1464
- elif p.suffix == ".json":
1465
- return Flock.from_json(p.read_text())
1466
- elif p.suffix == ".msgpack":
1467
- return Flock.from_msgpack_file(p)
1468
- elif p.suffix == ".pkl":
1469
- if PICKLE_AVAILABLE:
1470
- return Flock.from_pickle_file(p)
1471
- else:
1472
- raise RuntimeError(
1473
- "Cannot load Pickle file: cloudpickle not installed."
1474
- )
1475
- else:
1476
- raise ValueError(
1477
- f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
1478
- )
1479
- except Exception as e:
1480
- # Check if it's an exception about missing types
1481
- if "Could not get registered type name" in str(e):
1482
- logger.error(
1483
- f"Failed to load Flock from {file_path}: Missing type definition. "
1484
- "This may happen if the YAML was created on a system with different types registered. "
1485
- "Check if the file includes 'types' section with necessary type definitions."
1486
- )
1487
- logger.error(f"Error loading Flock from {file_path}: {e}")
1488
- raise
667
+ return load_flock_from_file(file_path)