flock-core 0.5.9__py3-none-any.whl → 0.5.11__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 (54) hide show
  1. flock/agent.py +149 -62
  2. flock/api/themes.py +6 -2
  3. flock/api_models.py +285 -0
  4. flock/artifact_collector.py +6 -3
  5. flock/batch_accumulator.py +3 -1
  6. flock/cli.py +3 -1
  7. flock/components.py +45 -56
  8. flock/context_provider.py +531 -0
  9. flock/correlation_engine.py +8 -4
  10. flock/dashboard/collector.py +48 -29
  11. flock/dashboard/events.py +10 -4
  12. flock/dashboard/launcher.py +3 -1
  13. flock/dashboard/models/graph.py +9 -3
  14. flock/dashboard/service.py +187 -93
  15. flock/dashboard/websocket.py +17 -4
  16. flock/engines/dspy_engine.py +174 -98
  17. flock/engines/examples/simple_batch_engine.py +9 -3
  18. flock/examples.py +6 -2
  19. flock/frontend/src/services/indexeddb.test.ts +4 -4
  20. flock/frontend/src/services/indexeddb.ts +1 -1
  21. flock/helper/cli_helper.py +14 -1
  22. flock/logging/auto_trace.py +6 -1
  23. flock/logging/formatters/enum_builder.py +3 -1
  24. flock/logging/formatters/theme_builder.py +32 -17
  25. flock/logging/formatters/themed_formatter.py +38 -22
  26. flock/logging/logging.py +21 -7
  27. flock/logging/telemetry.py +9 -3
  28. flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
  29. flock/logging/trace_and_logged.py +14 -5
  30. flock/mcp/__init__.py +3 -6
  31. flock/mcp/client.py +49 -19
  32. flock/mcp/config.py +12 -6
  33. flock/mcp/manager.py +6 -2
  34. flock/mcp/servers/sse/flock_sse_server.py +9 -3
  35. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
  36. flock/mcp/tool.py +18 -6
  37. flock/mcp/types/handlers.py +3 -1
  38. flock/mcp/types/types.py +9 -3
  39. flock/orchestrator.py +449 -58
  40. flock/orchestrator_component.py +15 -5
  41. flock/patches/dspy_streaming_patch.py +12 -4
  42. flock/registry.py +9 -3
  43. flock/runtime.py +69 -18
  44. flock/service.py +135 -64
  45. flock/store.py +29 -10
  46. flock/subscription.py +6 -4
  47. flock/system_artifacts.py +33 -0
  48. flock/utilities.py +41 -13
  49. flock/utility/output_utility_component.py +31 -11
  50. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/METADATA +150 -26
  51. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/RECORD +54 -51
  52. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/WHEEL +0 -0
  53. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/entry_points.txt +0 -0
  54. {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@ from fastapi.middleware.cors import CORSMiddleware
19
19
  from fastapi.staticfiles import StaticFiles
20
20
  from pydantic import ValidationError
21
21
 
22
+ from flock.api_models import ArtifactTypeSchema, ArtifactTypesResponse
22
23
  from flock.dashboard.collector import DashboardEventCollector
23
24
  from flock.dashboard.events import MessagePublishedEvent, VisibilitySpec
24
25
  from flock.dashboard.graph_builder import GraphAssembler
@@ -61,6 +62,18 @@ class DashboardHTTPService(BlackboardHTTPService):
61
62
  # Initialize base service
62
63
  super().__init__(orchestrator)
63
64
 
65
+ # Add dashboard-specific tags to OpenAPI
66
+ self.app.openapi_tags.extend([
67
+ {
68
+ "name": "Dashboard UI",
69
+ "description": "**Internal endpoints** used by the Flock dashboard UI. Not intended for direct use.",
70
+ },
71
+ {
72
+ "name": "Schema Discovery",
73
+ "description": "Endpoints for discovering available artifact types and their schemas.",
74
+ },
75
+ ])
76
+
64
77
  # Initialize WebSocket manager and event collector
65
78
  self.websocket_manager = websocket_manager or WebSocketManager()
66
79
  self.event_collector = event_collector or DashboardEventCollector(
@@ -137,13 +150,19 @@ class DashboardHTTPService(BlackboardHTTPService):
137
150
 
138
151
  if self.graph_assembler is not None:
139
152
 
140
- @app.post("/api/dashboard/graph", response_model=GraphSnapshot)
153
+ @app.post(
154
+ "/api/dashboard/graph",
155
+ response_model=GraphSnapshot,
156
+ tags=["Dashboard UI"],
157
+ )
141
158
  async def get_dashboard_graph(request: GraphRequest) -> GraphSnapshot:
142
159
  """Return server-side assembled dashboard graph snapshot."""
143
160
  return await self.graph_assembler.build_snapshot(request)
144
161
 
145
162
  dashboard_dir = Path(__file__).parent
146
- frontend_root = dashboard_dir.parent / ("frontend_v2" if self.use_v2 else "frontend")
163
+ frontend_root = dashboard_dir.parent / (
164
+ "frontend_v2" if self.use_v2 else "frontend"
165
+ )
147
166
  static_dir = dashboard_dir / ("static_v2" if self.use_v2 else "static")
148
167
 
149
168
  possible_dirs = [
@@ -172,8 +191,12 @@ class DashboardHTTPService(BlackboardHTTPService):
172
191
  app = self.app
173
192
  orchestrator = self.orchestrator
174
193
 
175
- @app.get("/api/artifact-types")
176
- async def get_artifact_types() -> dict[str, Any]:
194
+ @app.get(
195
+ "/api/artifact-types",
196
+ response_model=ArtifactTypesResponse,
197
+ tags=["Schema Discovery"],
198
+ )
199
+ async def get_artifact_types() -> ArtifactTypesResponse:
177
200
  """Get all registered artifact types with their schemas.
178
201
 
179
202
  Returns:
@@ -194,13 +217,15 @@ class DashboardHTTPService(BlackboardHTTPService):
194
217
  model_class = type_registry.resolve(type_name)
195
218
  # Get Pydantic schema
196
219
  schema = model_class.model_json_schema()
197
- artifact_types.append({"name": type_name, "schema": schema})
220
+ artifact_types.append(
221
+ ArtifactTypeSchema(name=type_name, schema=schema)
222
+ )
198
223
  except Exception as e:
199
224
  logger.warning(f"Could not get schema for {type_name}: {e}")
200
225
 
201
- return {"artifact_types": artifact_types}
226
+ return ArtifactTypesResponse(artifact_types=artifact_types)
202
227
 
203
- @app.get("/api/agents")
228
+ @app.get("/api/agents", tags=["Dashboard UI"])
204
229
  async def get_agents() -> dict[str, Any]:
205
230
  """Get all registered agents with logic operations state.
206
231
 
@@ -244,14 +269,18 @@ class DashboardHTTPService(BlackboardHTTPService):
244
269
  # NEW Phase 1.2: Logic operations configuration
245
270
  logic_operations = []
246
271
  for idx, subscription in enumerate(agent.subscriptions):
247
- logic_config = _build_logic_config(agent, subscription, idx, orchestrator)
272
+ logic_config = _build_logic_config(
273
+ agent, subscription, idx, orchestrator
274
+ )
248
275
  if logic_config: # Only include if has join/batch
249
276
  logic_operations.append(logic_config)
250
277
 
251
278
  agent_data = {
252
279
  "name": agent.name,
253
280
  "description": agent.description or "",
254
- "status": _compute_agent_status(agent, orchestrator), # NEW: Dynamic status
281
+ "status": _compute_agent_status(
282
+ agent, orchestrator
283
+ ), # NEW: Dynamic status
255
284
  "subscriptions": consumed_types,
256
285
  "output_types": produced_types,
257
286
  }
@@ -263,7 +292,7 @@ class DashboardHTTPService(BlackboardHTTPService):
263
292
 
264
293
  return {"agents": agents}
265
294
 
266
- @app.get("/api/version")
295
+ @app.get("/api/version", tags=["Dashboard UI"])
267
296
  async def get_version() -> dict[str, str]:
268
297
  """Get version information for the backend and dashboard.
269
298
 
@@ -281,7 +310,7 @@ class DashboardHTTPService(BlackboardHTTPService):
281
310
 
282
311
  return {"backend_version": backend_version, "package_name": "flock-flow"}
283
312
 
284
- @app.post("/api/control/publish")
313
+ @app.post("/api/control/publish", tags=["Dashboard UI"])
285
314
  async def publish_artifact(body: dict[str, Any]) -> dict[str, str]:
286
315
  """Publish artifact with correlation tracking.
287
316
 
@@ -314,7 +343,9 @@ class DashboardHTTPService(BlackboardHTTPService):
314
343
  try:
315
344
  instance = model_class(**content)
316
345
  except ValidationError as e:
317
- raise HTTPException(status_code=422, detail=f"Validation error: {e!s}")
346
+ raise HTTPException(
347
+ status_code=422, detail=f"Validation error: {e!s}"
348
+ )
318
349
 
319
350
  # Generate correlation ID
320
351
  correlation_id = str(uuid4())
@@ -355,7 +386,7 @@ class DashboardHTTPService(BlackboardHTTPService):
355
386
  logger.exception(f"Error publishing artifact: {e}")
356
387
  raise HTTPException(status_code=500, detail=str(e))
357
388
 
358
- @app.post("/api/control/invoke")
389
+ @app.post("/api/control/invoke", tags=["Dashboard UI"])
359
390
  async def invoke_agent(body: dict[str, Any]) -> dict[str, Any]:
360
391
  """Directly invoke a specific agent.
361
392
 
@@ -384,13 +415,17 @@ class DashboardHTTPService(BlackboardHTTPService):
384
415
  # Get agent from orchestrator
385
416
  agent = orchestrator.get_agent(agent_name)
386
417
  except KeyError:
387
- raise HTTPException(status_code=404, detail=f"Agent not found: {agent_name}")
418
+ raise HTTPException(
419
+ status_code=404, detail=f"Agent not found: {agent_name}"
420
+ )
388
421
 
389
422
  try:
390
423
  # Parse input type and create instance
391
424
  input_type = input_data.get("type")
392
425
  if not input_type:
393
- raise HTTPException(status_code=400, detail="input.type is required")
426
+ raise HTTPException(
427
+ status_code=400, detail="input.type is required"
428
+ )
394
429
 
395
430
  # Resolve type from registry
396
431
  model_class = type_registry.resolve(input_type)
@@ -402,7 +437,9 @@ class DashboardHTTPService(BlackboardHTTPService):
402
437
  try:
403
438
  instance = model_class(**payload)
404
439
  except ValidationError as e:
405
- raise HTTPException(status_code=422, detail=f"Validation error: {e!s}")
440
+ raise HTTPException(
441
+ status_code=422, detail=f"Validation error: {e!s}"
442
+ )
406
443
 
407
444
  # Invoke agent
408
445
  outputs = await orchestrator.invoke(agent, instance)
@@ -426,30 +463,36 @@ class DashboardHTTPService(BlackboardHTTPService):
426
463
  except HTTPException:
427
464
  raise
428
465
  except KeyError:
429
- raise HTTPException(status_code=422, detail=f"Unknown type: {input_type}")
466
+ raise HTTPException(
467
+ status_code=422, detail=f"Unknown type: {input_type}"
468
+ )
430
469
  except Exception as e:
431
470
  logger.exception(f"Error invoking agent: {e}")
432
471
  raise HTTPException(status_code=500, detail=str(e))
433
472
 
434
- @app.post("/api/control/pause")
473
+ @app.post("/api/control/pause", tags=["Dashboard UI"])
435
474
  async def pause_orchestrator() -> dict[str, Any]:
436
475
  """Pause orchestrator (placeholder).
437
476
 
438
477
  Returns:
439
478
  501 Not Implemented
440
479
  """
441
- raise HTTPException(status_code=501, detail="Pause functionality coming in Phase 12")
480
+ raise HTTPException(
481
+ status_code=501, detail="Pause functionality coming in Phase 12"
482
+ )
442
483
 
443
- @app.post("/api/control/resume")
484
+ @app.post("/api/control/resume", tags=["Dashboard UI"])
444
485
  async def resume_orchestrator() -> dict[str, Any]:
445
486
  """Resume orchestrator (placeholder).
446
487
 
447
488
  Returns:
448
489
  501 Not Implemented
449
490
  """
450
- raise HTTPException(status_code=501, detail="Resume functionality coming in Phase 12")
491
+ raise HTTPException(
492
+ status_code=501, detail="Resume functionality coming in Phase 12"
493
+ )
451
494
 
452
- @app.get("/api/traces")
495
+ @app.get("/api/traces", tags=["Dashboard UI"])
453
496
  async def get_traces() -> list[dict[str, Any]]:
454
497
  """Get OpenTelemetry traces from DuckDB.
455
498
 
@@ -516,10 +559,14 @@ class DashboardHTTPService(BlackboardHTTPService):
516
559
  "status_code": row[10], # status_code
517
560
  "description": row[11], # status_description
518
561
  },
519
- "attributes": json.loads(row[12]) if row[12] else {}, # attributes
562
+ "attributes": json.loads(row[12])
563
+ if row[12]
564
+ else {}, # attributes
520
565
  "events": json.loads(row[13]) if row[13] else [], # events
521
566
  "links": json.loads(row[14]) if row[14] else [], # links
522
- "resource": json.loads(row[15]) if row[15] else {}, # resource
567
+ "resource": json.loads(row[15])
568
+ if row[15]
569
+ else {}, # resource
523
570
  }
524
571
 
525
572
  # Add parent_id if exists
@@ -535,7 +582,7 @@ class DashboardHTTPService(BlackboardHTTPService):
535
582
  logger.exception(f"Error reading traces from DuckDB: {e}")
536
583
  return []
537
584
 
538
- @app.get("/api/traces/services")
585
+ @app.get("/api/traces/services", tags=["Dashboard UI"])
539
586
  async def get_trace_services() -> dict[str, Any]:
540
587
  """Get list of unique services that have been traced.
541
588
 
@@ -581,7 +628,7 @@ class DashboardHTTPService(BlackboardHTTPService):
581
628
  logger.exception(f"Error reading trace services: {e}")
582
629
  return {"services": [], "operations": []}
583
630
 
584
- @app.post("/api/traces/clear")
631
+ @app.post("/api/traces/clear", tags=["Dashboard UI"])
585
632
  async def clear_traces() -> dict[str, Any]:
586
633
  """Clear all traces from DuckDB database.
587
634
 
@@ -600,7 +647,7 @@ class DashboardHTTPService(BlackboardHTTPService):
600
647
 
601
648
  return result
602
649
 
603
- @app.post("/api/traces/query")
650
+ @app.post("/api/traces/query", tags=["Dashboard UI"])
604
651
  async def execute_trace_query(request: dict[str, Any]) -> dict[str, Any]:
605
652
  """
606
653
  Execute a DuckDB SQL query on the traces database.
@@ -619,10 +666,22 @@ class DashboardHTTPService(BlackboardHTTPService):
619
666
  # Security: Only allow SELECT queries
620
667
  query_upper = query.upper().strip()
621
668
  if not query_upper.startswith("SELECT"):
622
- return {"error": "Only SELECT queries are allowed", "results": [], "columns": []}
669
+ return {
670
+ "error": "Only SELECT queries are allowed",
671
+ "results": [],
672
+ "columns": [],
673
+ }
623
674
 
624
675
  # Check for dangerous keywords
625
- dangerous = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "TRUNCATE"]
676
+ dangerous = [
677
+ "DROP",
678
+ "DELETE",
679
+ "INSERT",
680
+ "UPDATE",
681
+ "ALTER",
682
+ "CREATE",
683
+ "TRUNCATE",
684
+ ]
626
685
  if any(keyword in query_upper for keyword in dangerous):
627
686
  return {
628
687
  "error": "Query contains forbidden operations",
@@ -632,12 +691,20 @@ class DashboardHTTPService(BlackboardHTTPService):
632
691
 
633
692
  db_path = Path(".flock/traces.duckdb")
634
693
  if not db_path.exists():
635
- return {"error": "Trace database not found", "results": [], "columns": []}
694
+ return {
695
+ "error": "Trace database not found",
696
+ "results": [],
697
+ "columns": [],
698
+ }
636
699
 
637
700
  try:
638
701
  with duckdb.connect(str(db_path), read_only=True) as conn:
639
702
  result = conn.execute(query).fetchall()
640
- columns = [desc[0] for desc in conn.description] if conn.description else []
703
+ columns = (
704
+ [desc[0] for desc in conn.description]
705
+ if conn.description
706
+ else []
707
+ )
641
708
 
642
709
  # Convert to JSON-serializable format
643
710
  results = []
@@ -652,12 +719,16 @@ class DashboardHTTPService(BlackboardHTTPService):
652
719
  row_dict[col] = val
653
720
  results.append(row_dict)
654
721
 
655
- return {"results": results, "columns": columns, "row_count": len(results)}
722
+ return {
723
+ "results": results,
724
+ "columns": columns,
725
+ "row_count": len(results),
726
+ }
656
727
  except Exception as e:
657
728
  logger.exception(f"DuckDB query error: {e}")
658
729
  return {"error": str(e), "results": [], "columns": []}
659
730
 
660
- @app.get("/api/traces/stats")
731
+ @app.get("/api/traces/stats", tags=["Dashboard UI"])
661
732
  async def get_trace_stats() -> dict[str, Any]:
662
733
  """Get statistics about the trace database.
663
734
 
@@ -691,7 +762,9 @@ class DashboardHTTPService(BlackboardHTTPService):
691
762
  try:
692
763
  with duckdb.connect(str(db_path), read_only=True) as conn:
693
764
  # Get total spans
694
- total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[0]
765
+ total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[
766
+ 0
767
+ ]
695
768
 
696
769
  # Get total unique traces
697
770
  total_traces = conn.execute(
@@ -745,7 +818,7 @@ class DashboardHTTPService(BlackboardHTTPService):
745
818
  "database_size_mb": 0,
746
819
  }
747
820
 
748
- @app.get("/api/streaming-history/{agent_name}")
821
+ @app.get("/api/streaming-history/{agent_name}", tags=["Dashboard UI"])
749
822
  async def get_streaming_history(agent_name: str) -> dict[str, Any]:
750
823
  """Get historical streaming output for a specific agent.
751
824
 
@@ -777,12 +850,14 @@ class DashboardHTTPService(BlackboardHTTPService):
777
850
  "events": [event.model_dump() for event in history],
778
851
  }
779
852
  except Exception as e:
780
- logger.exception(f"Failed to get streaming history for {agent_name}: {e}")
853
+ logger.exception(
854
+ f"Failed to get streaming history for {agent_name}: {e}"
855
+ )
781
856
  raise HTTPException(
782
857
  status_code=500, detail=f"Failed to get streaming history: {e!s}"
783
858
  )
784
859
 
785
- @app.get("/api/artifacts/history/{node_id}")
860
+ @app.get("/api/artifacts/history/{node_id}", tags=["Dashboard UI"])
786
861
  async def get_message_history(node_id: str) -> dict[str, Any]:
787
862
  """Get complete message history for a node (both produced and consumed).
788
863
 
@@ -818,24 +893,25 @@ class DashboardHTTPService(BlackboardHTTPService):
818
893
 
819
894
  # 1. Get messages PRODUCED by this node
820
895
  produced_filter = FilterConfig(produced_by={node_id})
821
- produced_artifacts, _produced_count = await orchestrator.store.query_artifacts(
896
+ (
897
+ produced_artifacts,
898
+ _produced_count,
899
+ ) = await orchestrator.store.query_artifacts(
822
900
  produced_filter, limit=100, offset=0, embed_meta=False
823
901
  )
824
902
 
825
903
  for artifact in produced_artifacts:
826
- messages.append(
827
- {
828
- "id": str(artifact.id),
829
- "type": artifact.type,
830
- "direction": "published",
831
- "payload": artifact.payload,
832
- "timestamp": artifact.created_at.isoformat(),
833
- "correlation_id": str(artifact.correlation_id)
834
- if artifact.correlation_id
835
- else None,
836
- "produced_by": artifact.produced_by,
837
- }
838
- )
904
+ messages.append({
905
+ "id": str(artifact.id),
906
+ "type": artifact.type,
907
+ "direction": "published",
908
+ "payload": artifact.payload,
909
+ "timestamp": artifact.created_at.isoformat(),
910
+ "correlation_id": str(artifact.correlation_id)
911
+ if artifact.correlation_id
912
+ else None,
913
+ "produced_by": artifact.produced_by,
914
+ })
839
915
 
840
916
  # 2. Get messages CONSUMED by this node
841
917
  # Query all artifacts with consumption metadata
@@ -848,31 +924,37 @@ class DashboardHTTPService(BlackboardHTTPService):
848
924
  artifact = envelope.artifact
849
925
  for consumption in envelope.consumptions:
850
926
  if consumption.consumer == node_id:
851
- messages.append(
852
- {
853
- "id": str(artifact.id),
854
- "type": artifact.type,
855
- "direction": "consumed",
856
- "payload": artifact.payload,
857
- "timestamp": artifact.created_at.isoformat(),
858
- "correlation_id": str(artifact.correlation_id)
859
- if artifact.correlation_id
860
- else None,
861
- "produced_by": artifact.produced_by,
862
- "consumed_at": consumption.consumed_at.isoformat(),
863
- }
864
- )
927
+ messages.append({
928
+ "id": str(artifact.id),
929
+ "type": artifact.type,
930
+ "direction": "consumed",
931
+ "payload": artifact.payload,
932
+ "timestamp": artifact.created_at.isoformat(),
933
+ "correlation_id": str(artifact.correlation_id)
934
+ if artifact.correlation_id
935
+ else None,
936
+ "produced_by": artifact.produced_by,
937
+ "consumed_at": consumption.consumed_at.isoformat(),
938
+ })
865
939
 
866
940
  # Sort by timestamp (most recent first)
867
- messages.sort(key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True)
941
+ messages.sort(
942
+ key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True
943
+ )
868
944
 
869
- return {"node_id": node_id, "messages": messages, "total": len(messages)}
945
+ return {
946
+ "node_id": node_id,
947
+ "messages": messages,
948
+ "total": len(messages),
949
+ }
870
950
 
871
951
  except Exception as e:
872
952
  logger.exception(f"Failed to get message history for {node_id}: {e}")
873
- raise HTTPException(status_code=500, detail=f"Failed to get message history: {e!s}")
953
+ raise HTTPException(
954
+ status_code=500, detail=f"Failed to get message history: {e!s}"
955
+ )
874
956
 
875
- @app.get("/api/agents/{agent_id}/runs")
957
+ @app.get("/api/agents/{agent_id}/runs", tags=["Dashboard UI"])
876
958
  async def get_agent_runs(agent_id: str) -> dict[str, Any]:
877
959
  """Get run history for an agent.
878
960
 
@@ -919,7 +1001,9 @@ class DashboardHTTPService(BlackboardHTTPService):
919
1001
 
920
1002
  except Exception as e:
921
1003
  logger.exception(f"Failed to get run history for {agent_id}: {e}")
922
- raise HTTPException(status_code=500, detail=f"Failed to get run history: {e!s}")
1004
+ raise HTTPException(
1005
+ status_code=500, detail=f"Failed to get run history: {e!s}"
1006
+ )
923
1007
 
924
1008
  def _register_theme_routes(self) -> None:
925
1009
  """Register theme API endpoints for dashboard customization."""
@@ -930,7 +1014,7 @@ class DashboardHTTPService(BlackboardHTTPService):
930
1014
  app = self.app
931
1015
  themes_dir = Path(__file__).parent.parent / "themes"
932
1016
 
933
- @app.get("/api/themes")
1017
+ @app.get("/api/themes", tags=["Dashboard UI"])
934
1018
  async def list_themes() -> dict[str, Any]:
935
1019
  """Get list of available theme names.
936
1020
 
@@ -947,9 +1031,11 @@ class DashboardHTTPService(BlackboardHTTPService):
947
1031
  return {"themes": theme_names}
948
1032
  except Exception as e:
949
1033
  logger.exception(f"Failed to list themes: {e}")
950
- raise HTTPException(status_code=500, detail=f"Failed to list themes: {e!s}")
1034
+ raise HTTPException(
1035
+ status_code=500, detail=f"Failed to list themes: {e!s}"
1036
+ )
951
1037
 
952
- @app.get("/api/themes/{theme_name}")
1038
+ @app.get("/api/themes/{theme_name}", tags=["Dashboard UI"])
953
1039
  async def get_theme(theme_name: str) -> dict[str, Any]:
954
1040
  """Get theme data by name.
955
1041
 
@@ -966,12 +1052,16 @@ class DashboardHTTPService(BlackboardHTTPService):
966
1052
  """
967
1053
  try:
968
1054
  # Sanitize theme name to prevent path traversal
969
- theme_name = theme_name.replace("/", "").replace("\\", "").replace("..", "")
1055
+ theme_name = (
1056
+ theme_name.replace("/", "").replace("\\", "").replace("..", "")
1057
+ )
970
1058
 
971
1059
  theme_path = themes_dir / f"{theme_name}.toml"
972
1060
 
973
1061
  if not theme_path.exists():
974
- raise HTTPException(status_code=404, detail=f"Theme '{theme_name}' not found")
1062
+ raise HTTPException(
1063
+ status_code=404, detail=f"Theme '{theme_name}' not found"
1064
+ )
975
1065
 
976
1066
  # Load TOML theme
977
1067
  theme_data = toml.load(theme_path)
@@ -981,7 +1071,9 @@ class DashboardHTTPService(BlackboardHTTPService):
981
1071
  raise
982
1072
  except Exception as e:
983
1073
  logger.exception(f"Failed to load theme '{theme_name}': {e}")
984
- raise HTTPException(status_code=500, detail=f"Failed to load theme: {e!s}")
1074
+ raise HTTPException(
1075
+ status_code=500, detail=f"Failed to load theme: {e!s}"
1076
+ )
985
1077
 
986
1078
  async def start(self) -> None:
987
1079
  """Start the dashboard service.
@@ -1088,22 +1180,22 @@ def _get_correlation_groups(
1088
1180
  if collected_types.get(type_name, 0) < required_count
1089
1181
  ]
1090
1182
 
1091
- result.append(
1092
- {
1093
- "correlation_key": str(corr_key),
1094
- "created_at": group.created_at_time.isoformat() if group.created_at_time else None,
1095
- "elapsed_seconds": round(elapsed, 1),
1096
- "expires_in_seconds": round(expires_in_seconds, 1)
1097
- if expires_in_seconds is not None
1098
- else None,
1099
- "expires_in_artifacts": expires_in_artifacts,
1100
- "collected_types": collected_types,
1101
- "required_types": dict(group.type_counts),
1102
- "waiting_for": waiting_for,
1103
- "is_complete": group.is_complete(),
1104
- "is_expired": group.is_expired(engine.global_sequence),
1105
- }
1106
- )
1183
+ result.append({
1184
+ "correlation_key": str(corr_key),
1185
+ "created_at": group.created_at_time.isoformat()
1186
+ if group.created_at_time
1187
+ else None,
1188
+ "elapsed_seconds": round(elapsed, 1),
1189
+ "expires_in_seconds": round(expires_in_seconds, 1)
1190
+ if expires_in_seconds is not None
1191
+ else None,
1192
+ "expires_in_artifacts": expires_in_artifacts,
1193
+ "collected_types": collected_types,
1194
+ "required_types": dict(group.type_counts),
1195
+ "waiting_for": waiting_for,
1196
+ "is_complete": group.is_complete(),
1197
+ "is_expired": group.is_expired(engine.global_sequence),
1198
+ })
1107
1199
 
1108
1200
  return result
1109
1201
 
@@ -1328,7 +1420,9 @@ def _build_logic_config( # noqa: F821
1328
1420
  config["batch"]["timeout_seconds"] = int(batch_spec.timeout.total_seconds())
1329
1421
 
1330
1422
  # Get waiting state from BatchEngine
1331
- batch_state = _get_batch_state(orchestrator._batch_engine, agent.name, idx, batch_spec)
1423
+ batch_state = _get_batch_state(
1424
+ orchestrator._batch_engine, agent.name, idx, batch_spec
1425
+ )
1332
1426
  if batch_state:
1333
1427
  if "waiting_state" not in config:
1334
1428
  config["waiting_state"] = {"is_waiting": True}
@@ -72,7 +72,11 @@ class WebSocketManager:
72
72
  logger.info(f"WebSocket client added. Total clients: {len(self.clients)}")
73
73
 
74
74
  # Start heartbeat task if enabled and not already running
75
- if self.enable_heartbeat and self._heartbeat_task is None and not self._shutdown:
75
+ if (
76
+ self.enable_heartbeat
77
+ and self._heartbeat_task is None
78
+ and not self._shutdown
79
+ ):
76
80
  self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
77
81
 
78
82
  async def remove_client(self, websocket: WebSocket) -> None:
@@ -167,7 +171,9 @@ class WebSocketManager:
167
171
 
168
172
  # Send ping to all clients
169
173
  ping_tasks = []
170
- for client in list(self.clients): # Copy to avoid modification during iteration
174
+ for client in list(
175
+ self.clients
176
+ ): # Copy to avoid modification during iteration
171
177
  ping_tasks.append(self._ping_client(client))
172
178
 
173
179
  # Execute pings concurrently
@@ -186,7 +192,10 @@ class WebSocketManager:
186
192
  client: WebSocket client to ping
187
193
  """
188
194
  try:
189
- await client.send_json({"type": "ping", "timestamp": asyncio.get_event_loop().time()})
195
+ await client.send_json({
196
+ "type": "ping",
197
+ "timestamp": asyncio.get_event_loop().time(),
198
+ })
190
199
  except Exception as e:
191
200
  logger.warning(f"Failed to ping client: {e}")
192
201
  await self.remove_client(client)
@@ -197,7 +206,11 @@ class WebSocketManager:
197
206
  In production, heartbeat is disabled by default (enable_heartbeat=False).
198
207
  Only starts if enable_heartbeat=True.
199
208
  """
200
- if self.enable_heartbeat and self._heartbeat_task is None and not self._shutdown:
209
+ if (
210
+ self.enable_heartbeat
211
+ and self._heartbeat_task is None
212
+ and not self._shutdown
213
+ ):
201
214
  self._heartbeat_task = asyncio.create_task(self._heartbeat_loop())
202
215
 
203
216
  async def shutdown(self) -> None: