flock-core 0.4.0b43__py3-none-any.whl → 0.4.0b45__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (44) hide show
  1. flock/cli/manage_agents.py +19 -4
  2. flock/core/api/__init__.py +1 -2
  3. flock/core/api/endpoints.py +150 -218
  4. flock/core/api/main.py +134 -653
  5. flock/core/api/service.py +214 -0
  6. flock/core/flock.py +192 -134
  7. flock/core/flock_agent.py +31 -0
  8. flock/webapp/app/api/agent_management.py +135 -164
  9. flock/webapp/app/api/execution.py +76 -85
  10. flock/webapp/app/api/flock_management.py +60 -33
  11. flock/webapp/app/chat.py +233 -0
  12. flock/webapp/app/config.py +6 -3
  13. flock/webapp/app/dependencies.py +95 -0
  14. flock/webapp/app/main.py +320 -906
  15. flock/webapp/app/services/flock_service.py +183 -161
  16. flock/webapp/run.py +176 -100
  17. flock/webapp/static/css/chat.css +227 -0
  18. flock/webapp/static/css/components.css +167 -0
  19. flock/webapp/static/css/header.css +39 -0
  20. flock/webapp/static/css/layout.css +46 -0
  21. flock/webapp/static/css/sidebar.css +127 -0
  22. flock/webapp/templates/base.html +6 -1
  23. flock/webapp/templates/chat.html +60 -0
  24. flock/webapp/templates/chat_settings.html +20 -0
  25. flock/webapp/templates/flock_editor.html +1 -1
  26. flock/webapp/templates/partials/_agent_detail_form.html +8 -7
  27. flock/webapp/templates/partials/_agent_list.html +3 -3
  28. flock/webapp/templates/partials/_agent_manager_view.html +3 -4
  29. flock/webapp/templates/partials/_chat_container.html +9 -0
  30. flock/webapp/templates/partials/_chat_messages.html +13 -0
  31. flock/webapp/templates/partials/_chat_settings_form.html +65 -0
  32. flock/webapp/templates/partials/_execution_form.html +2 -2
  33. flock/webapp/templates/partials/_execution_view_container.html +1 -1
  34. flock/webapp/templates/partials/_flock_properties_form.html +2 -2
  35. flock/webapp/templates/partials/_registry_viewer_content.html +3 -3
  36. flock/webapp/templates/partials/_sidebar.html +17 -1
  37. flock/webapp/templates/registry_viewer.html +3 -3
  38. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/METADATA +1 -1
  39. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/RECORD +42 -31
  40. flock/webapp/static/css/custom.css +0 -612
  41. flock/webapp/templates/partials/_agent_manager_view_old.html +0 -19
  42. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/WHEEL +0 -0
  43. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/entry_points.txt +0 -0
  44. {flock_core-0.4.0b43.dist-info → flock_core-0.4.0b45.dist-info}/licenses/LICENSE +0 -0
flock/core/flock.py CHANGED
@@ -20,23 +20,28 @@ from box import Box
20
20
  from temporalio import workflow
21
21
 
22
22
  with workflow.unsafe.imports_passed_through():
23
- from datasets import Dataset
23
+ from datasets import Dataset # type: ignore
24
24
 
25
+ # Assuming run_local_workflow is correctly placed and importable
25
26
  from flock.core.execution.local_executor import (
26
27
  run_local_workflow,
27
28
  )
28
29
  from opentelemetry import trace
29
30
  from opentelemetry.baggage import get_baggage, set_baggage
30
- from pandas import DataFrame
31
+ from pandas import DataFrame # type: ignore
31
32
  from pydantic import BaseModel, Field
32
33
 
33
34
  # Flock core components & utilities
34
35
  from flock.config import DEFAULT_MODEL, TELEMETRY
35
- from flock.core.api.custom_endpoint import FlockEndpoint
36
+ from flock.core.api.custom_endpoint import (
37
+ FlockEndpoint, # Keep for type hinting custom_endpoints
38
+ )
36
39
  from flock.core.context.context import FlockContext
37
40
  from flock.core.context.context_manager import initialize_context
41
+
42
+ # Assuming run_temporal_workflow is correctly placed and importable
38
43
  from flock.core.execution.temporal_executor import run_temporal_workflow
39
- from flock.core.flock_evaluator import FlockEvaluator
44
+ from flock.core.flock_evaluator import FlockEvaluator # For type hint
40
45
  from flock.core.logging.logging import LOGGERS, get_logger, get_module_loggers
41
46
  from flock.core.serialization.serializable import Serializable
42
47
  from flock.core.util.cli_helper import init_console
@@ -52,14 +57,14 @@ if TYPE_CHECKING:
52
57
  from flock.core.flock_registry import get_registry
53
58
 
54
59
  try:
55
- import pandas as pd
60
+ import pandas as pd # type: ignore
56
61
 
57
62
  PANDAS_AVAILABLE = True
58
63
  except ImportError:
59
- pd = None
64
+ pd = None # type: ignore
60
65
  PANDAS_AVAILABLE = False
61
66
 
62
- logger = get_logger("flock")
67
+ logger = get_logger("flock.api")
63
68
  TELEMETRY.setup_tracing() # Setup OpenTelemetry
64
69
  tracer = trace.get_tracer(__name__)
65
70
  FlockRegistry = get_registry() # Get the registry instance
@@ -67,10 +72,6 @@ FlockRegistry = get_registry() # Get the registry instance
67
72
  # Define TypeVar for generic class methods like from_dict
68
73
  T = TypeVar("T", bound="Flock")
69
74
 
70
- # from rich.traceback import install
71
-
72
- # install(show_locals=True)
73
-
74
75
 
75
76
  class Flock(BaseModel, Serializable):
76
77
  """Orchestrator for managing and executing agent systems.
@@ -115,6 +116,7 @@ class Flock(BaseModel, Serializable):
115
116
  description="If True (default) and enable_temporal=True, start a temporary in-process worker for development/testing convenience. Set to False when using dedicated workers.",
116
117
  )
117
118
  # Internal agent storage - not part of the Pydantic model for direct serialization
119
+ # Marked with underscore to indicate it's managed internally and accessed via property
118
120
  _agents: dict[str, FlockAgent]
119
121
  _start_agent_name: str | None = None # For potential pre-configuration
120
122
  _start_input: dict = {} # Instance attribute overwritten in __init__; kept for typing clarity
@@ -122,6 +124,7 @@ class Flock(BaseModel, Serializable):
122
124
  # Pydantic v2 model config
123
125
  model_config = {
124
126
  "arbitrary_types_allowed": True,
127
+ # Assuming FlockRegistry type might not be serializable by default
125
128
  "ignored_types": (type(FlockRegistry),),
126
129
  }
127
130
 
@@ -148,7 +151,7 @@ class Flock(BaseModel, Serializable):
148
151
  model=model,
149
152
  description=description,
150
153
  enable_temporal=enable_temporal,
151
- enable_logging=enable_logging,
154
+ enable_logging=bool(enable_logging), # Store as bool, specific loggers handled by _configure
152
155
  show_flock_banner=show_flock_banner,
153
156
  temporal_config=temporal_config,
154
157
  temporal_start_in_process_worker=temporal_start_in_process_worker,
@@ -161,11 +164,13 @@ class Flock(BaseModel, Serializable):
161
164
  self._start_input = {}
162
165
 
163
166
  # Set up logging based on the enable_logging flag
164
- self._configure_logging(enable_logging) # Use instance attribute
167
+ self._configure_logging(enable_logging) # Pass original value to _configure_logging
165
168
 
166
169
  # Register passed agents
167
170
  if agents:
168
- from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
171
+ from flock.core.flock_agent import (
172
+ FlockAgent as ConcreteFlockAgent, # Local import
173
+ )
169
174
 
170
175
  for agent in agents:
171
176
  if isinstance(agent, ConcreteFlockAgent):
@@ -176,7 +181,8 @@ class Flock(BaseModel, Serializable):
176
181
  )
177
182
 
178
183
  # Initialize console if needed for banner
179
- init_console(clear_screen=True, show_banner=self.show_flock_banner)
184
+ if self.show_flock_banner: # Check instance attribute
185
+ init_console(clear_screen=True, show_banner=self.show_flock_banner)
180
186
 
181
187
  # Set Temporal debug environment variable
182
188
  self._set_temporal_debug_flag()
@@ -191,33 +197,37 @@ class Flock(BaseModel, Serializable):
191
197
  enable_temporal=self.enable_temporal,
192
198
  )
193
199
 
194
- def _configure_logging(self, enable_logging: bool | list[str]):
200
+ def _configure_logging(self, enable_logging_config: bool | list[str]):
195
201
  """Configure logging levels based on the enable_logging flag."""
196
202
  is_enabled_globally = False
197
- enabled_loggers = []
203
+ specific_loggers_to_enable = []
198
204
 
199
- if isinstance(enable_logging, bool):
200
- is_enabled_globally = enable_logging
201
- elif isinstance(enable_logging, list):
202
- is_enabled_globally = bool(enable_logging)
203
- enabled_loggers = enable_logging
205
+ if isinstance(enable_logging_config, bool):
206
+ is_enabled_globally = enable_logging_config
207
+ elif isinstance(enable_logging_config, list):
208
+ is_enabled_globally = bool(enable_logging_config) # True if list is not empty
209
+ specific_loggers_to_enable = enable_logging_config
204
210
 
205
211
  # Configure core loggers
206
- for log_name in LOGGERS:
212
+ for log_name in LOGGERS: # Assuming LOGGERS is a list of known logger names
207
213
  log_instance = get_logger(log_name)
208
- if is_enabled_globally or log_name in enabled_loggers:
214
+ if is_enabled_globally or log_name in specific_loggers_to_enable:
209
215
  log_instance.enable_logging = True
210
216
  else:
211
217
  log_instance.enable_logging = False
212
218
 
213
219
  # Configure module loggers (existing ones)
214
- module_loggers = get_module_loggers()
220
+ module_loggers = get_module_loggers() # Assuming this returns list of FlockLogger instances
215
221
  for mod_log in module_loggers:
216
- if is_enabled_globally or mod_log.name in enabled_loggers:
222
+ if is_enabled_globally or mod_log.name in specific_loggers_to_enable:
217
223
  mod_log.enable_logging = True
218
224
  else:
219
225
  mod_log.enable_logging = False
220
226
 
227
+ # Update the instance's Pydantic field
228
+ self.enable_logging = is_enabled_globally or bool(specific_loggers_to_enable)
229
+
230
+
221
231
  def _set_temporal_debug_flag(self):
222
232
  """Set or remove LOCAL_DEBUG env var based on enable_temporal."""
223
233
  if not self.enable_temporal:
@@ -242,7 +252,9 @@ class Flock(BaseModel, Serializable):
242
252
 
243
253
  def add_agent(self, agent: FlockAgent) -> FlockAgent:
244
254
  """Adds an agent instance to this Flock configuration and registry."""
245
- from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
255
+ from flock.core.flock_agent import (
256
+ FlockAgent as ConcreteFlockAgent, # Local import
257
+ )
246
258
 
247
259
  if not isinstance(agent, ConcreteFlockAgent):
248
260
  raise TypeError("Provided object is not a FlockAgent instance.")
@@ -250,7 +262,13 @@ class Flock(BaseModel, Serializable):
250
262
  raise ValueError("Agent must have a name.")
251
263
 
252
264
  if agent.name in self._agents:
253
- raise ValueError("Agent with this name already exists.")
265
+ # Allow re-adding the same instance, but raise error for different instance with same name
266
+ if self._agents[agent.name] is not agent:
267
+ raise ValueError(f"Agent with name '{agent.name}' already exists with a different instance.")
268
+ else:
269
+ logger.debug(f"Agent '{agent.name}' is already added. Skipping.")
270
+ return agent # Return existing agent
271
+
254
272
  self._agents[agent.name] = agent
255
273
  FlockRegistry.register_agent(agent) # Register globally
256
274
 
@@ -286,7 +304,6 @@ class Flock(BaseModel, Serializable):
286
304
  """Entry point for running an agent system synchronously."""
287
305
  try:
288
306
  loop = asyncio.get_running_loop()
289
- # If loop exists, check if it's closed
290
307
  if loop.is_closed():
291
308
  raise RuntimeError("Event loop is closed")
292
309
  except RuntimeError: # No running loop
@@ -307,6 +324,12 @@ class Flock(BaseModel, Serializable):
307
324
  )
308
325
  return result
309
326
  else:
327
+ # If called from an already running loop (e.g. Jupyter, FastAPI endpoint)
328
+ # Create a future and run it to completion. This is tricky and
329
+ # ideally, one should use `await self.run_async` directly in async contexts.
330
+ # This simple run_until_complete on a future might block the existing loop.
331
+ # For truly non-blocking execution in an existing loop, one might need
332
+ # to schedule the coroutine differently or advise users to use `await`.
310
333
  future = asyncio.ensure_future(
311
334
  self.run_async(
312
335
  start_agent=start_agent,
@@ -319,6 +342,7 @@ class Flock(BaseModel, Serializable):
319
342
  )
320
343
  return loop.run_until_complete(future)
321
344
 
345
+
322
346
  async def run_async(
323
347
  self,
324
348
  start_agent: FlockAgent | str | None = None,
@@ -330,8 +354,9 @@ class Flock(BaseModel, Serializable):
330
354
  memo: dict[str, Any] | None = None,
331
355
  ) -> Box | dict:
332
356
  """Entry point for running an agent system asynchronously."""
333
- # Import here to allow forward reference resolution
334
- from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
357
+ from flock.core.flock_agent import (
358
+ FlockAgent as ConcreteFlockAgent, # Local import
359
+ )
335
360
 
336
361
  with tracer.start_as_current_span("flock.run_async") as span:
337
362
  # Add passed agents first
@@ -348,11 +373,11 @@ class Flock(BaseModel, Serializable):
348
373
  start_agent_name: str | None = None
349
374
  if isinstance(start_agent, ConcreteFlockAgent):
350
375
  start_agent_name = start_agent.name
351
- if start_agent_name not in self._agents:
376
+ if start_agent_name not in self._agents: # Add if not already present
352
377
  self.add_agent(start_agent)
353
378
  elif isinstance(start_agent, str):
354
379
  start_agent_name = start_agent
355
- else:
380
+ else: # start_agent is None
356
381
  start_agent_name = self._start_agent_name
357
382
 
358
383
  # Default to first agent if only one exists and none specified
@@ -360,7 +385,7 @@ class Flock(BaseModel, Serializable):
360
385
  start_agent_name = list(self._agents.keys())[0]
361
386
  elif not start_agent_name:
362
387
  raise ValueError(
363
- "No start_agent specified and multiple/no agents exist."
388
+ "No start_agent specified and multiple/no agents exist in the Flock instance."
364
389
  )
365
390
 
366
391
  # Check if start_agent is in agents
@@ -390,41 +415,31 @@ class Flock(BaseModel, Serializable):
390
415
 
391
416
  try:
392
417
  resolved_start_agent = self._agents.get(start_agent_name)
393
- if not resolved_start_agent:
394
- resolved_start_agent = FlockRegistry.get_agent(
395
- start_agent_name
396
- )
397
- if not resolved_start_agent:
398
- raise ValueError(
399
- f"Start agent '{start_agent_name}' not found."
400
- )
401
- self.add_agent(resolved_start_agent)
418
+ if not resolved_start_agent: # Should have been handled by now
419
+ raise ValueError(f"Start agent '{start_agent_name}' not found after checks.")
402
420
 
403
421
  run_context = context if context else FlockContext()
404
- set_baggage("run_id", effective_run_id)
422
+ set_baggage("run_id", effective_run_id) # Set for OpenTelemetry
405
423
 
406
424
  initialize_context(
407
425
  run_context,
408
426
  start_agent_name,
409
427
  run_input,
410
428
  effective_run_id,
411
- not self.enable_temporal,
429
+ not self.enable_temporal, # local_debug is inverse of enable_temporal
412
430
  self.model or resolved_start_agent.model or DEFAULT_MODEL,
413
431
  )
414
432
  # Add agent definitions to context for routing/serialization within workflow
415
- for agent_name, agent_instance in self.agents.items():
416
- # Agents already handle their serialization
417
- agent_dict_repr = agent_instance.to_dict()
433
+ for agent_name_iter, agent_instance_iter in self.agents.items():
434
+ agent_dict_repr = agent_instance_iter.to_dict() # Agents handle their own serialization
418
435
  run_context.add_agent_definition(
419
- agent_type=type(agent_instance),
420
- agent_name=agent_name,
421
- agent_data=agent_dict_repr, # Pass the serialized dict
436
+ agent_type=type(agent_instance_iter),
437
+ agent_name=agent_name_iter,
438
+ agent_data=agent_dict_repr,
422
439
  )
423
440
 
424
441
  # Add temporal config to context if enabled
425
442
  if self.enable_temporal and self.temporal_config:
426
- # Store the workflow config dict for the executor/workflow to use
427
- # Using a specific key to avoid potential clashes in state
428
443
  run_context.set_variable(
429
444
  "flock.temporal_workflow_config",
430
445
  self.temporal_config.model_dump(mode="json"),
@@ -439,17 +454,13 @@ class Flock(BaseModel, Serializable):
439
454
  # Execute workflow
440
455
  if not self.enable_temporal:
441
456
  result = await run_local_workflow(
442
- run_context, box_result=False
457
+ run_context, box_result=False # Boxing handled below
443
458
  )
444
459
  else:
445
- # Pass the Flock instance itself to the executor
446
- # so it can access the temporal_config directly if needed
447
- # This avoids putting potentially large/complex config objects
448
- # directly into the context state that gets passed around.
449
460
  result = await run_temporal_workflow(
450
- self, # Pass the Flock instance
461
+ self, # Pass the Flock instance
451
462
  run_context,
452
- box_result=False,
463
+ box_result=False, # Boxing handled below
453
464
  memo=memo,
454
465
  )
455
466
 
@@ -465,9 +476,9 @@ class Flock(BaseModel, Serializable):
465
476
  try:
466
477
  logger.debug("Boxing final result.")
467
478
  return Box(result)
468
- except ImportError:
479
+ except ImportError: # Should not happen as Box is a direct dependency
469
480
  logger.warning(
470
- "Box library not installed, returning raw dict."
481
+ "Box library not installed (should be direct dependency), returning raw dict."
471
482
  )
472
483
  return result
473
484
  else:
@@ -479,10 +490,15 @@ class Flock(BaseModel, Serializable):
479
490
  )
480
491
  span.record_exception(e)
481
492
  span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
482
- return {
493
+ # Return a consistent error structure
494
+ error_output = {
483
495
  "error": str(e),
484
496
  "details": f"Flock run '{self.name}' failed.",
497
+ "run_id": effective_run_id,
498
+ "start_agent": start_agent_name,
485
499
  }
500
+ return Box(error_output) if box_result else error_output
501
+
486
502
 
487
503
  # --- Batch Processing (Delegation) ---
488
504
  async def run_batch_async(
@@ -539,7 +555,6 @@ class Flock(BaseModel, Serializable):
539
555
  delimiter: str = ",",
540
556
  ) -> list[Box | dict | None | Exception]:
541
557
  """Synchronous wrapper for run_batch_async."""
542
- # (Standard asyncio run wrapper logic)
543
558
  try:
544
559
  loop = asyncio.get_running_loop()
545
560
  if loop.is_closed():
@@ -574,15 +589,15 @@ class Flock(BaseModel, Serializable):
574
589
  # --- Evaluation (Delegation) ---
575
590
  async def evaluate_async(
576
591
  self,
577
- dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset,
592
+ dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset, # type: ignore
578
593
  start_agent: FlockAgent | str,
579
594
  input_mapping: dict[str, str],
580
595
  answer_mapping: dict[str, str],
581
596
  metrics: list[
582
597
  str
583
598
  | Callable[[Any, Any], bool | float | dict[str, Any]]
584
- | FlockAgent
585
- | FlockEvaluator
599
+ | FlockAgent # Type hint only
600
+ | FlockEvaluator # Type hint only
586
601
  ],
587
602
  metric_configs: dict[str, dict[str, Any]] | None = None,
588
603
  static_inputs: dict[str, Any] | None = None,
@@ -594,7 +609,7 @@ class Flock(BaseModel, Serializable):
594
609
  return_dataframe: bool = True,
595
610
  silent_mode: bool = False,
596
611
  metadata_columns: list[str] | None = None,
597
- ) -> DataFrame | list[dict[str, Any]]:
612
+ ) -> DataFrame | list[dict[str, Any]]: # type: ignore
598
613
  """Evaluates the Flock's performance against a dataset (delegated)."""
599
614
  # Import processor locally
600
615
  from flock.core.execution.evaluation_executor import (
@@ -622,15 +637,15 @@ class Flock(BaseModel, Serializable):
622
637
 
623
638
  def evaluate(
624
639
  self,
625
- dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset,
640
+ dataset: str | Path | list[dict[str, Any]] | DataFrame | Dataset, # type: ignore
626
641
  start_agent: FlockAgent | str,
627
642
  input_mapping: dict[str, str],
628
643
  answer_mapping: dict[str, str],
629
644
  metrics: list[
630
645
  str
631
646
  | Callable[[Any, Any], bool | float | dict[str, Any]]
632
- | FlockAgent
633
- | FlockEvaluator
647
+ | FlockAgent # Type hint only
648
+ | FlockEvaluator # Type hint only
634
649
  ],
635
650
  metric_configs: dict[str, dict[str, Any]] | None = None,
636
651
  static_inputs: dict[str, Any] | None = None,
@@ -642,9 +657,8 @@ class Flock(BaseModel, Serializable):
642
657
  return_dataframe: bool = True,
643
658
  silent_mode: bool = False,
644
659
  metadata_columns: list[str] | None = None,
645
- ) -> DataFrame | list[dict[str, Any]]:
660
+ ) -> DataFrame | list[dict[str, Any]]: # type: ignore
646
661
  """Synchronous wrapper for evaluate_async."""
647
- # (Standard asyncio run wrapper logic)
648
662
  try:
649
663
  loop = asyncio.get_running_loop()
650
664
  if loop.is_closed():
@@ -678,104 +692,148 @@ class Flock(BaseModel, Serializable):
678
692
  future = asyncio.ensure_future(coro)
679
693
  return loop.run_until_complete(future)
680
694
 
681
- # --- API Server Starter ---
695
+ # --- Server & CLI Starters (Delegation) ---
682
696
  def start_api(
683
697
  self,
684
698
  host: str = "127.0.0.1",
685
699
  port: int = 8344,
686
- server_name: str = "Flock API",
687
- create_ui: bool = False,
700
+ server_name: str = "Flock Server",
701
+ create_ui: bool = True, # Default to True for the integrated experience
688
702
  ui_theme: str | None = None,
689
703
  custom_endpoints: Sequence[FlockEndpoint] | dict[tuple[str, list[str] | None], Callable[..., Any]] | None = None,
690
704
  ) -> None:
691
- """Starts a REST API server for this Flock instance.
692
- If create_ui is True, integrates the web UI, potentially with a specific theme.
693
- """
694
- # Import runner locally
695
- # We need to decide if start_api now *always* tries to use the integrated approach
696
- # or if it still calls the API-only runner when create_ui=False.
697
- # Let's assume it uses the integrated approach starter if create_ui=True
698
- # and the API-only starter (FlockAPI) if create_ui=False.
705
+ """Starts a unified REST API server and/or Web UI for this Flock instance."""
706
+ import warnings
707
+ warnings.warn(
708
+ "start_api() is deprecated and will be removed in a future release. "
709
+ "Use serve() instead.",
710
+ DeprecationWarning,
711
+ stacklevel=2,
712
+ )
713
+ # Delegate to the new serve() method (create_ui maps to ui)
714
+ return self.serve(
715
+ host=host,
716
+ port=port,
717
+ server_name=server_name,
718
+ ui=create_ui,
719
+ ui_theme=ui_theme,
720
+ custom_endpoints=custom_endpoints,
721
+ )
699
722
 
700
- if create_ui:
701
- # Use the integrated server approach
702
- try:
703
- # We need a function similar to start_flock_api but for the integrated app
704
- # Let's assume it exists in webapp.run for now, called start_integrated_server
705
- from flock.webapp.run import start_integrated_server
706
-
707
- start_integrated_server(
708
- flock_instance=self,
709
- host=host,
710
- port=port,
711
- server_name=server_name,
712
- theme_name=ui_theme,
713
- custom_endpoints=custom_endpoints,
714
- )
715
- except ImportError:
716
- # Log error - cannot start integrated UI
717
- from flock.core.logging.logging import get_logger
723
+ # ------------------------------------------------------------------
724
+ # New preferred method name
725
+ # ------------------------------------------------------------------
718
726
 
719
- logger = get_logger("flock.core")
720
- logger.error(
721
- "Cannot start integrated UI: Required components (e.g., flock.webapp.run.start_integrated_server) not found."
722
- )
723
- except Exception as e:
724
- from flock.core.logging.logging import get_logger
725
-
726
- logger = get_logger("flock.core")
727
- logger.error(
728
- f"Failed to start integrated UI server: {e}", exc_info=True
729
- )
730
- else:
731
- # Use the API-only server approach
732
- from flock.core.api.runner import start_flock_api
733
-
734
- start_flock_api(
735
- flock=self,
736
- host=host,
737
- port=port,
738
- server_name=server_name,
739
- create_ui=False, # Explicitly false for API only runner
740
- custom_endpoints=custom_endpoints,
727
+ def serve(
728
+ self,
729
+ host: str = "127.0.0.1",
730
+ port: int = 8344,
731
+ server_name: str = "Flock Server",
732
+ ui: bool = True,
733
+ chat: bool = False,
734
+ chat_agent: str | None = None, # Reserved for future real agent chat
735
+ chat_message_key: str = "message",
736
+ chat_history_key: str = "history",
737
+ chat_response_key: str = "response",
738
+ ui_theme: str | None = None,
739
+ custom_endpoints: Sequence[FlockEndpoint] | dict[tuple[str, list[str] | None], Callable[..., Any]] | None = None,
740
+ ) -> None:
741
+ """Launch an HTTP server that exposes the core REST API and, optionally, the
742
+ browser-based UI.
743
+
744
+ Args:
745
+ host: Bind address for the server (default "127.0.0.1").
746
+ port: TCP port to listen on (default 8344).
747
+ server_name: Title shown in the OpenAPI docs / logs.
748
+ ui: If True (default) the Pico/HTMX web UI routes are included. If False
749
+ only the JSON API groups (core & custom) are served.
750
+ chat: If True, enable chat routes.
751
+ chat_agent: Name of the agent to use for chat.
752
+ chat_message_key: Key for chat message in input.
753
+ chat_history_key: Key for chat history in input.
754
+ chat_response_key: Key for chat response in output.
755
+ ui_theme: Optional UI theme name or "random".
756
+ custom_endpoints: Additional API routes to add, either as a list of
757
+ FlockEndpoint objects or the legacy dict format.
758
+ """
759
+ try:
760
+ from flock.webapp.run import start_unified_server
761
+ except ImportError:
762
+ logger.error(
763
+ "Web application components not found (flock.webapp.run). "
764
+ "Cannot start HTTP server. Ensure webapp dependencies are installed."
741
765
  )
766
+ return
767
+
768
+ logger.info(
769
+ f"Attempting to start server for Flock '{self.name}' on {host}:{port}. UI enabled: {ui}"
770
+ )
771
+
772
+ start_unified_server(
773
+ flock_instance=self,
774
+ host=host,
775
+ port=port,
776
+ server_title=server_name,
777
+ enable_ui_routes=ui,
778
+ enable_chat_routes=chat,
779
+ ui_theme=ui_theme,
780
+ custom_endpoints=custom_endpoints,
781
+ )
742
782
 
743
- # --- CLI Starter ---
744
783
  def start_cli(
745
784
  self,
785
+ start_agent: FlockAgent | str | None = None, # Added start_agent to match method signature in file_26
746
786
  server_name: str = "Flock CLI",
747
787
  show_results: bool = False,
748
788
  edit_mode: bool = False,
749
789
  ) -> None:
750
790
  """Starts an interactive CLI for this Flock instance."""
751
791
  # Import runner locally
752
- from flock.cli.runner import start_flock_cli
792
+ try:
793
+ from flock.cli.runner import start_flock_cli
794
+ except ImportError:
795
+ logger.error(
796
+ "CLI components not found. Cannot start CLI. "
797
+ "Ensure CLI dependencies are installed."
798
+ )
799
+ return
800
+
801
+ # The start_flock_cli function in file_50 doesn't take start_agent
802
+ # but the original docs for start_cli did.
803
+ # For now, I'll pass it through, assuming start_flock_cli will be updated or ignore it.
804
+ # If start_agent is crucial here, start_flock_cli needs to handle it.
805
+ logger.info(f"Starting CLI for Flock '{self.name}'...")
806
+ start_flock_cli(
807
+ flock=self, # Pass the Flock instance
808
+ # start_agent=start_agent, # This argument is not in the definition of start_flock_cli in file_50
809
+ server_name=server_name,
810
+ show_results=show_results,
811
+ edit_mode=edit_mode
812
+ )
753
813
 
754
- start_flock_cli(self, server_name, show_results, edit_mode)
755
814
 
756
815
  # --- Serialization Delegation Methods ---
757
816
  def to_dict(self, path_type: str = "relative") -> dict[str, Any]:
758
817
  """Serialize Flock instance to dictionary using FlockSerializer."""
759
- # Import locally to prevent circular imports at module level if structure is complex
760
818
  from flock.core.serialization.flock_serializer import FlockSerializer
761
819
 
762
- # Assuming FlockSerializer handles the nested temporal_config serialization
763
820
  return FlockSerializer.serialize(self, path_type=path_type)
764
821
 
765
822
  @classmethod
766
823
  def from_dict(cls: type[T], data: dict[str, Any]) -> T:
767
824
  """Deserialize Flock instance from dictionary using FlockSerializer."""
768
- # Import locally
769
825
  from flock.core.serialization.flock_serializer import FlockSerializer
770
826
 
771
- # Assuming FlockSerializer handles the nested temporal_config deserialization
772
827
  return FlockSerializer.deserialize(cls, data)
773
828
 
774
829
  # --- Static Method Loader (Delegates to loader module) ---
775
830
  @staticmethod
776
- def load_from_file(file_path: str) -> Flock:
831
+ def load_from_file(file_path: str) -> Flock: # Ensure return type is Flock
777
832
  """Load a Flock instance from various file formats (delegates to loader)."""
778
- # Import locally
779
833
  from flock.core.util.loader import load_flock_from_file
780
834
 
781
- return load_flock_from_file(file_path)
835
+ loaded_flock = load_flock_from_file(file_path)
836
+ # Ensure the loaded object is indeed a Flock instance
837
+ if not isinstance(loaded_flock, Flock):
838
+ raise TypeError(f"Loaded object from {file_path} is not a Flock instance, but {type(loaded_flock)}")
839
+ return loaded_flock
flock/core/flock_agent.py CHANGED
@@ -195,6 +195,37 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
195
195
  """Get a list of currently enabled modules attached to this agent."""
196
196
  return [m for m in self.modules.values() if m.config.enabled]
197
197
 
198
+ @property
199
+ def resolved_description(self) -> str | None:
200
+ """Returns the resolved agent description.
201
+ If the description is a callable, it attempts to call it.
202
+ Returns None if the description is None or a callable that fails.
203
+ """
204
+ if callable(self.description):
205
+ try:
206
+ # Attempt to call without context first.
207
+ # If callables consistently need context, this might need adjustment
208
+ # or the template-facing property might need to be simpler,
209
+ # relying on prior resolution via resolve_callables.
210
+ return self.description()
211
+ except TypeError:
212
+ # Log a warning that context might be needed?
213
+ # For now, treat as unresolvable in this simple property.
214
+ logger.warning(
215
+ f"Callable description for agent '{self.name}' could not be resolved "
216
+ f"without context via the simple 'resolved_description' property. "
217
+ f"Consider calling 'agent.resolve_callables(context)' beforehand if context is required."
218
+ )
219
+ return None # Or a placeholder like "[Callable Description]"
220
+ except Exception as e:
221
+ logger.error(
222
+ f"Error resolving callable description for agent '{self.name}': {e}"
223
+ )
224
+ return None
225
+ elif isinstance(self.description, str):
226
+ return self.description
227
+ return None
228
+
198
229
  # --- Lifecycle Hooks (Keep as they were) ---
199
230
  async def initialize(self, inputs: dict[str, Any]) -> None:
200
231
  """Initialize agent and run module initializers."""