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.
- flock/agent.py +149 -62
- flock/api/themes.py +6 -2
- flock/api_models.py +285 -0
- flock/artifact_collector.py +6 -3
- flock/batch_accumulator.py +3 -1
- flock/cli.py +3 -1
- flock/components.py +45 -56
- flock/context_provider.py +531 -0
- flock/correlation_engine.py +8 -4
- flock/dashboard/collector.py +48 -29
- flock/dashboard/events.py +10 -4
- flock/dashboard/launcher.py +3 -1
- flock/dashboard/models/graph.py +9 -3
- flock/dashboard/service.py +187 -93
- flock/dashboard/websocket.py +17 -4
- flock/engines/dspy_engine.py +174 -98
- flock/engines/examples/simple_batch_engine.py +9 -3
- flock/examples.py +6 -2
- flock/frontend/src/services/indexeddb.test.ts +4 -4
- flock/frontend/src/services/indexeddb.ts +1 -1
- flock/helper/cli_helper.py +14 -1
- flock/logging/auto_trace.py +6 -1
- flock/logging/formatters/enum_builder.py +3 -1
- flock/logging/formatters/theme_builder.py +32 -17
- flock/logging/formatters/themed_formatter.py +38 -22
- flock/logging/logging.py +21 -7
- flock/logging/telemetry.py +9 -3
- flock/logging/telemetry_exporter/duckdb_exporter.py +27 -25
- flock/logging/trace_and_logged.py +14 -5
- flock/mcp/__init__.py +3 -6
- flock/mcp/client.py +49 -19
- flock/mcp/config.py +12 -6
- flock/mcp/manager.py +6 -2
- flock/mcp/servers/sse/flock_sse_server.py +9 -3
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +6 -2
- flock/mcp/tool.py +18 -6
- flock/mcp/types/handlers.py +3 -1
- flock/mcp/types/types.py +9 -3
- flock/orchestrator.py +449 -58
- flock/orchestrator_component.py +15 -5
- flock/patches/dspy_streaming_patch.py +12 -4
- flock/registry.py +9 -3
- flock/runtime.py +69 -18
- flock/service.py +135 -64
- flock/store.py +29 -10
- flock/subscription.py +6 -4
- flock/system_artifacts.py +33 -0
- flock/utilities.py +41 -13
- flock/utility/output_utility_component.py +31 -11
- {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/METADATA +150 -26
- {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/RECORD +54 -51
- {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/WHEEL +0 -0
- {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.9.dist-info → flock_core-0.5.11.dist-info}/licenses/LICENSE +0 -0
flock/dashboard/service.py
CHANGED
|
@@ -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(
|
|
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 / (
|
|
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(
|
|
176
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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])
|
|
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])
|
|
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 {
|
|
669
|
+
return {
|
|
670
|
+
"error": "Only SELECT queries are allowed",
|
|
671
|
+
"results": [],
|
|
672
|
+
"columns": [],
|
|
673
|
+
}
|
|
623
674
|
|
|
624
675
|
# Check for dangerous keywords
|
|
625
|
-
dangerous = [
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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()[
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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(
|
|
941
|
+
messages.sort(
|
|
942
|
+
key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True
|
|
943
|
+
)
|
|
868
944
|
|
|
869
|
-
return {
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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(
|
|
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}
|
flock/dashboard/websocket.py
CHANGED
|
@@ -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
|
|
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(
|
|
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({
|
|
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
|
|
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:
|