flock-core 0.5.8__py3-none-any.whl → 0.5.10__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/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 +143 -72
- 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 +204 -50
- 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 +19 -6
- flock/store.py +29 -10
- flock/subscription.py +6 -4
- flock/utilities.py +41 -13
- flock/utility/output_utility_component.py +31 -11
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/METADATA +134 -4
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/RECORD +52 -51
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/WHEEL +0 -0
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.8.dist-info → flock_core-0.5.10.dist-info}/licenses/LICENSE +0 -0
flock/dashboard/collector.py
CHANGED
|
@@ -54,7 +54,9 @@ class RunRecord:
|
|
|
54
54
|
error_message: str | None = None
|
|
55
55
|
|
|
56
56
|
def to_graph_run(self) -> GraphRun:
|
|
57
|
-
status =
|
|
57
|
+
status = (
|
|
58
|
+
self.status if self.status in {"active", "completed", "error"} else "active"
|
|
59
|
+
)
|
|
58
60
|
return GraphRun(
|
|
59
61
|
run_id=self.run_id,
|
|
60
62
|
agent_name=self.agent_name,
|
|
@@ -102,7 +104,10 @@ class DashboardEventCollector(AgentComponent):
|
|
|
102
104
|
|
|
103
105
|
# Use PrivateAttr for non-Pydantic fields (AgentComponent extends BaseModel)
|
|
104
106
|
_events: deque[
|
|
105
|
-
AgentActivatedEvent
|
|
107
|
+
AgentActivatedEvent
|
|
108
|
+
| MessagePublishedEvent
|
|
109
|
+
| AgentCompletedEvent
|
|
110
|
+
| AgentErrorEvent
|
|
106
111
|
] = PrivateAttr(default=None)
|
|
107
112
|
|
|
108
113
|
# Track run start times for duration calculation
|
|
@@ -114,7 +119,9 @@ class DashboardEventCollector(AgentComponent):
|
|
|
114
119
|
# Graph assembly helpers
|
|
115
120
|
_graph_lock: asyncio.Lock = PrivateAttr(default_factory=asyncio.Lock)
|
|
116
121
|
_run_registry: dict[str, RunRecord] = PrivateAttr(default_factory=dict)
|
|
117
|
-
_artifact_consumers: dict[str, set[str]] = PrivateAttr(
|
|
122
|
+
_artifact_consumers: dict[str, set[str]] = PrivateAttr(
|
|
123
|
+
default_factory=lambda: defaultdict(set)
|
|
124
|
+
)
|
|
118
125
|
_agent_status: dict[str, str] = PrivateAttr(default_factory=dict)
|
|
119
126
|
_agent_snapshots: dict[str, AgentSnapshot] = PrivateAttr(default_factory=dict)
|
|
120
127
|
|
|
@@ -185,13 +192,15 @@ class DashboardEventCollector(AgentComponent):
|
|
|
185
192
|
await self._update_agent_snapshot_locked(agent)
|
|
186
193
|
|
|
187
194
|
# Build subscription info from agent's subscriptions
|
|
188
|
-
subscription_info = SubscriptionInfo(from_agents=[],
|
|
195
|
+
subscription_info = SubscriptionInfo(from_agents=[], tags=[], mode="both")
|
|
189
196
|
|
|
190
197
|
if agent.subscriptions:
|
|
191
198
|
# Get first subscription's config (agents typically have one)
|
|
192
199
|
sub = agent.subscriptions[0]
|
|
193
|
-
subscription_info.from_agents =
|
|
194
|
-
|
|
200
|
+
subscription_info.from_agents = (
|
|
201
|
+
list(sub.from_agents) if sub.from_agents else []
|
|
202
|
+
)
|
|
203
|
+
subscription_info.tags = list(sub.tags) if sub.tags else []
|
|
195
204
|
subscription_info.mode = sub.mode
|
|
196
205
|
|
|
197
206
|
# Create and store event
|
|
@@ -210,7 +219,9 @@ class DashboardEventCollector(AgentComponent):
|
|
|
210
219
|
)
|
|
211
220
|
|
|
212
221
|
self._events.append(event)
|
|
213
|
-
logger.info(
|
|
222
|
+
logger.info(
|
|
223
|
+
f"Agent activated: {agent.name} (correlation_id={event.correlation_id})"
|
|
224
|
+
)
|
|
214
225
|
|
|
215
226
|
# Broadcast via WebSocket if manager is configured
|
|
216
227
|
if self._websocket_manager:
|
|
@@ -220,7 +231,9 @@ class DashboardEventCollector(AgentComponent):
|
|
|
220
231
|
|
|
221
232
|
return inputs
|
|
222
233
|
|
|
223
|
-
async def on_post_publish(
|
|
234
|
+
async def on_post_publish(
|
|
235
|
+
self, agent: "Agent", ctx: Context, artifact: "Artifact"
|
|
236
|
+
) -> None:
|
|
224
237
|
"""Emit message_published event when artifact is published.
|
|
225
238
|
|
|
226
239
|
Args:
|
|
@@ -391,7 +404,9 @@ class DashboardEventCollector(AgentComponent):
|
|
|
391
404
|
}
|
|
392
405
|
runs = [record.to_graph_run() for record in self._run_registry.values()]
|
|
393
406
|
agent_status = dict(self._agent_status)
|
|
394
|
-
return GraphState(
|
|
407
|
+
return GraphState(
|
|
408
|
+
consumptions=consumptions, runs=runs, agent_status=agent_status
|
|
409
|
+
)
|
|
395
410
|
|
|
396
411
|
async def snapshot_agent_registry(self) -> dict[str, AgentSnapshot]:
|
|
397
412
|
"""Return a snapshot of all known agents (active and inactive)."""
|
|
@@ -456,21 +471,17 @@ class DashboardEventCollector(AgentComponent):
|
|
|
456
471
|
async def _update_agent_snapshot_locked(self, agent: "Agent") -> None:
|
|
457
472
|
now = datetime.now(UTC)
|
|
458
473
|
description = agent.description or ""
|
|
459
|
-
subscriptions = sorted(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if getattr(output, "spec", None) is not None
|
|
471
|
-
and getattr(output.spec, "type_name", "")
|
|
472
|
-
}
|
|
473
|
-
)
|
|
474
|
+
subscriptions = sorted({
|
|
475
|
+
type_name
|
|
476
|
+
for subscription in getattr(agent, "subscriptions", [])
|
|
477
|
+
for type_name in getattr(subscription, "type_names", [])
|
|
478
|
+
})
|
|
479
|
+
output_types = sorted({
|
|
480
|
+
output.spec.type_name
|
|
481
|
+
for output in getattr(agent, "outputs", [])
|
|
482
|
+
if getattr(output, "spec", None) is not None
|
|
483
|
+
and getattr(output.spec, "type_name", "")
|
|
484
|
+
})
|
|
474
485
|
labels = sorted(agent.labels)
|
|
475
486
|
|
|
476
487
|
signature_payload = {
|
|
@@ -519,7 +530,9 @@ class DashboardEventCollector(AgentComponent):
|
|
|
519
530
|
first_seen=snapshot.first_seen,
|
|
520
531
|
last_seen=snapshot.last_seen,
|
|
521
532
|
signature=snapshot.signature,
|
|
522
|
-
logic_operations=[
|
|
533
|
+
logic_operations=[
|
|
534
|
+
dict(op) for op in snapshot.logic_operations
|
|
535
|
+
], # Phase 1.2
|
|
523
536
|
)
|
|
524
537
|
|
|
525
538
|
def _snapshot_to_record(self, snapshot: AgentSnapshot) -> AgentSnapshotRecord:
|
|
@@ -545,19 +558,25 @@ class DashboardEventCollector(AgentComponent):
|
|
|
545
558
|
"""
|
|
546
559
|
# Get visibility kind from class name, stripping "Visibility" suffix
|
|
547
560
|
class_name = type(visibility).__name__
|
|
548
|
-
kind = class_name
|
|
561
|
+
kind = class_name.removesuffix("Visibility")
|
|
549
562
|
|
|
550
563
|
spec = VisibilitySpec(kind=kind)
|
|
551
564
|
|
|
552
565
|
# Extract type-specific fields
|
|
553
566
|
if kind == "Private":
|
|
554
|
-
spec.agents =
|
|
567
|
+
spec.agents = (
|
|
568
|
+
list(visibility.agents) if hasattr(visibility, "agents") else []
|
|
569
|
+
)
|
|
555
570
|
elif kind == "Labelled":
|
|
556
571
|
spec.required_labels = (
|
|
557
|
-
list(visibility.required_labels)
|
|
572
|
+
list(visibility.required_labels)
|
|
573
|
+
if hasattr(visibility, "required_labels")
|
|
574
|
+
else []
|
|
558
575
|
)
|
|
559
576
|
elif kind == "Tenant":
|
|
560
|
-
spec.tenant_id =
|
|
577
|
+
spec.tenant_id = (
|
|
578
|
+
visibility.tenant_id if hasattr(visibility, "tenant_id") else None
|
|
579
|
+
)
|
|
561
580
|
|
|
562
581
|
return spec
|
|
563
582
|
|
flock/dashboard/events.py
CHANGED
|
@@ -14,7 +14,7 @@ class SubscriptionInfo(BaseModel):
|
|
|
14
14
|
"""Subscription configuration for an agent."""
|
|
15
15
|
|
|
16
16
|
from_agents: list[str] = Field(default_factory=list)
|
|
17
|
-
|
|
17
|
+
tags: list[str] = Field(default_factory=list)
|
|
18
18
|
mode: str = "both" # "both" | "events" | "direct"
|
|
19
19
|
|
|
20
20
|
|
|
@@ -123,8 +123,12 @@ class StreamingOutputEvent(BaseModel):
|
|
|
123
123
|
is_final: bool = False # True when agent completes this output stream
|
|
124
124
|
|
|
125
125
|
# Artifact tracking (Phase 6: for message streaming preview)
|
|
126
|
-
artifact_id: str | None =
|
|
127
|
-
|
|
126
|
+
artifact_id: str | None = (
|
|
127
|
+
None # Pre-generated artifact ID for streaming message nodes
|
|
128
|
+
)
|
|
129
|
+
artifact_type: str | None = (
|
|
130
|
+
None # Artifact type name (e.g., "__main__.BookOutline")
|
|
131
|
+
)
|
|
128
132
|
|
|
129
133
|
|
|
130
134
|
class AgentCompletedEvent(BaseModel):
|
|
@@ -149,7 +153,9 @@ class AgentCompletedEvent(BaseModel):
|
|
|
149
153
|
artifacts_produced: list[str] = Field(default_factory=list) # [artifact_id]
|
|
150
154
|
|
|
151
155
|
# Metrics and state
|
|
152
|
-
metrics: dict[str, Any] = Field(
|
|
156
|
+
metrics: dict[str, Any] = Field(
|
|
157
|
+
default_factory=dict
|
|
158
|
+
) # {"tokens_used": 1234, "cost": 0.05}
|
|
153
159
|
final_state: dict[str, Any] = Field(default_factory=dict) # Context.state snapshot
|
|
154
160
|
|
|
155
161
|
|
flock/dashboard/launcher.py
CHANGED
|
@@ -94,7 +94,9 @@ class DashboardLauncher:
|
|
|
94
94
|
|
|
95
95
|
def _start_dev_server(self) -> None:
|
|
96
96
|
"""Start npm dev server for hot-reload development."""
|
|
97
|
-
print(
|
|
97
|
+
print(
|
|
98
|
+
f"[Dashboard] Starting dev server (DASHBOARD_DEV=1) on port {self.port}..."
|
|
99
|
+
)
|
|
98
100
|
|
|
99
101
|
try:
|
|
100
102
|
self._npm_process = subprocess.Popen(
|
flock/dashboard/models/graph.py
CHANGED
|
@@ -104,7 +104,9 @@ class GraphStatistics(BaseModel):
|
|
|
104
104
|
consumed_by_agent: dict[str, GraphAgentMetrics] = Field(
|
|
105
105
|
default_factory=dict, alias="consumedByAgent"
|
|
106
106
|
)
|
|
107
|
-
artifact_summary: dict[str, Any] = Field(
|
|
107
|
+
artifact_summary: dict[str, Any] = Field(
|
|
108
|
+
default_factory=dict, alias="artifactSummary"
|
|
109
|
+
)
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
class GraphArtifact(BaseModel):
|
|
@@ -128,8 +130,12 @@ class GraphRun(BaseModel):
|
|
|
128
130
|
agent_name: str = Field(alias="agentName")
|
|
129
131
|
correlation_id: str | None = Field(default=None, alias="correlationId")
|
|
130
132
|
status: Literal["active", "completed", "error"] = "active"
|
|
131
|
-
consumed_artifacts: list[str] = Field(
|
|
132
|
-
|
|
133
|
+
consumed_artifacts: list[str] = Field(
|
|
134
|
+
default_factory=list, alias="consumedArtifacts"
|
|
135
|
+
)
|
|
136
|
+
produced_artifacts: list[str] = Field(
|
|
137
|
+
default_factory=list, alias="producedArtifacts"
|
|
138
|
+
)
|
|
133
139
|
duration_ms: float | None = Field(default=None, alias="durationMs")
|
|
134
140
|
started_at: datetime | None = Field(default=None, alias="startedAt")
|
|
135
141
|
completed_at: datetime | None = Field(default=None, alias="completedAt")
|
flock/dashboard/service.py
CHANGED
|
@@ -143,7 +143,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
143
143
|
return await self.graph_assembler.build_snapshot(request)
|
|
144
144
|
|
|
145
145
|
dashboard_dir = Path(__file__).parent
|
|
146
|
-
frontend_root = dashboard_dir.parent / (
|
|
146
|
+
frontend_root = dashboard_dir.parent / (
|
|
147
|
+
"frontend_v2" if self.use_v2 else "frontend"
|
|
148
|
+
)
|
|
147
149
|
static_dir = dashboard_dir / ("static_v2" if self.use_v2 else "static")
|
|
148
150
|
|
|
149
151
|
possible_dirs = [
|
|
@@ -244,14 +246,18 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
244
246
|
# NEW Phase 1.2: Logic operations configuration
|
|
245
247
|
logic_operations = []
|
|
246
248
|
for idx, subscription in enumerate(agent.subscriptions):
|
|
247
|
-
logic_config = _build_logic_config(
|
|
249
|
+
logic_config = _build_logic_config(
|
|
250
|
+
agent, subscription, idx, orchestrator
|
|
251
|
+
)
|
|
248
252
|
if logic_config: # Only include if has join/batch
|
|
249
253
|
logic_operations.append(logic_config)
|
|
250
254
|
|
|
251
255
|
agent_data = {
|
|
252
256
|
"name": agent.name,
|
|
253
257
|
"description": agent.description or "",
|
|
254
|
-
"status": _compute_agent_status(
|
|
258
|
+
"status": _compute_agent_status(
|
|
259
|
+
agent, orchestrator
|
|
260
|
+
), # NEW: Dynamic status
|
|
255
261
|
"subscriptions": consumed_types,
|
|
256
262
|
"output_types": produced_types,
|
|
257
263
|
}
|
|
@@ -314,7 +320,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
314
320
|
try:
|
|
315
321
|
instance = model_class(**content)
|
|
316
322
|
except ValidationError as e:
|
|
317
|
-
raise HTTPException(
|
|
323
|
+
raise HTTPException(
|
|
324
|
+
status_code=422, detail=f"Validation error: {e!s}"
|
|
325
|
+
)
|
|
318
326
|
|
|
319
327
|
# Generate correlation ID
|
|
320
328
|
correlation_id = str(uuid4())
|
|
@@ -384,13 +392,17 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
384
392
|
# Get agent from orchestrator
|
|
385
393
|
agent = orchestrator.get_agent(agent_name)
|
|
386
394
|
except KeyError:
|
|
387
|
-
raise HTTPException(
|
|
395
|
+
raise HTTPException(
|
|
396
|
+
status_code=404, detail=f"Agent not found: {agent_name}"
|
|
397
|
+
)
|
|
388
398
|
|
|
389
399
|
try:
|
|
390
400
|
# Parse input type and create instance
|
|
391
401
|
input_type = input_data.get("type")
|
|
392
402
|
if not input_type:
|
|
393
|
-
raise HTTPException(
|
|
403
|
+
raise HTTPException(
|
|
404
|
+
status_code=400, detail="input.type is required"
|
|
405
|
+
)
|
|
394
406
|
|
|
395
407
|
# Resolve type from registry
|
|
396
408
|
model_class = type_registry.resolve(input_type)
|
|
@@ -402,7 +414,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
402
414
|
try:
|
|
403
415
|
instance = model_class(**payload)
|
|
404
416
|
except ValidationError as e:
|
|
405
|
-
raise HTTPException(
|
|
417
|
+
raise HTTPException(
|
|
418
|
+
status_code=422, detail=f"Validation error: {e!s}"
|
|
419
|
+
)
|
|
406
420
|
|
|
407
421
|
# Invoke agent
|
|
408
422
|
outputs = await orchestrator.invoke(agent, instance)
|
|
@@ -426,7 +440,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
426
440
|
except HTTPException:
|
|
427
441
|
raise
|
|
428
442
|
except KeyError:
|
|
429
|
-
raise HTTPException(
|
|
443
|
+
raise HTTPException(
|
|
444
|
+
status_code=422, detail=f"Unknown type: {input_type}"
|
|
445
|
+
)
|
|
430
446
|
except Exception as e:
|
|
431
447
|
logger.exception(f"Error invoking agent: {e}")
|
|
432
448
|
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -438,7 +454,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
438
454
|
Returns:
|
|
439
455
|
501 Not Implemented
|
|
440
456
|
"""
|
|
441
|
-
raise HTTPException(
|
|
457
|
+
raise HTTPException(
|
|
458
|
+
status_code=501, detail="Pause functionality coming in Phase 12"
|
|
459
|
+
)
|
|
442
460
|
|
|
443
461
|
@app.post("/api/control/resume")
|
|
444
462
|
async def resume_orchestrator() -> dict[str, Any]:
|
|
@@ -447,7 +465,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
447
465
|
Returns:
|
|
448
466
|
501 Not Implemented
|
|
449
467
|
"""
|
|
450
|
-
raise HTTPException(
|
|
468
|
+
raise HTTPException(
|
|
469
|
+
status_code=501, detail="Resume functionality coming in Phase 12"
|
|
470
|
+
)
|
|
451
471
|
|
|
452
472
|
@app.get("/api/traces")
|
|
453
473
|
async def get_traces() -> list[dict[str, Any]]:
|
|
@@ -516,10 +536,14 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
516
536
|
"status_code": row[10], # status_code
|
|
517
537
|
"description": row[11], # status_description
|
|
518
538
|
},
|
|
519
|
-
"attributes": json.loads(row[12])
|
|
539
|
+
"attributes": json.loads(row[12])
|
|
540
|
+
if row[12]
|
|
541
|
+
else {}, # attributes
|
|
520
542
|
"events": json.loads(row[13]) if row[13] else [], # events
|
|
521
543
|
"links": json.loads(row[14]) if row[14] else [], # links
|
|
522
|
-
"resource": json.loads(row[15])
|
|
544
|
+
"resource": json.loads(row[15])
|
|
545
|
+
if row[15]
|
|
546
|
+
else {}, # resource
|
|
523
547
|
}
|
|
524
548
|
|
|
525
549
|
# Add parent_id if exists
|
|
@@ -619,10 +643,22 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
619
643
|
# Security: Only allow SELECT queries
|
|
620
644
|
query_upper = query.upper().strip()
|
|
621
645
|
if not query_upper.startswith("SELECT"):
|
|
622
|
-
return {
|
|
646
|
+
return {
|
|
647
|
+
"error": "Only SELECT queries are allowed",
|
|
648
|
+
"results": [],
|
|
649
|
+
"columns": [],
|
|
650
|
+
}
|
|
623
651
|
|
|
624
652
|
# Check for dangerous keywords
|
|
625
|
-
dangerous = [
|
|
653
|
+
dangerous = [
|
|
654
|
+
"DROP",
|
|
655
|
+
"DELETE",
|
|
656
|
+
"INSERT",
|
|
657
|
+
"UPDATE",
|
|
658
|
+
"ALTER",
|
|
659
|
+
"CREATE",
|
|
660
|
+
"TRUNCATE",
|
|
661
|
+
]
|
|
626
662
|
if any(keyword in query_upper for keyword in dangerous):
|
|
627
663
|
return {
|
|
628
664
|
"error": "Query contains forbidden operations",
|
|
@@ -632,12 +668,20 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
632
668
|
|
|
633
669
|
db_path = Path(".flock/traces.duckdb")
|
|
634
670
|
if not db_path.exists():
|
|
635
|
-
return {
|
|
671
|
+
return {
|
|
672
|
+
"error": "Trace database not found",
|
|
673
|
+
"results": [],
|
|
674
|
+
"columns": [],
|
|
675
|
+
}
|
|
636
676
|
|
|
637
677
|
try:
|
|
638
678
|
with duckdb.connect(str(db_path), read_only=True) as conn:
|
|
639
679
|
result = conn.execute(query).fetchall()
|
|
640
|
-
columns =
|
|
680
|
+
columns = (
|
|
681
|
+
[desc[0] for desc in conn.description]
|
|
682
|
+
if conn.description
|
|
683
|
+
else []
|
|
684
|
+
)
|
|
641
685
|
|
|
642
686
|
# Convert to JSON-serializable format
|
|
643
687
|
results = []
|
|
@@ -652,7 +696,11 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
652
696
|
row_dict[col] = val
|
|
653
697
|
results.append(row_dict)
|
|
654
698
|
|
|
655
|
-
return {
|
|
699
|
+
return {
|
|
700
|
+
"results": results,
|
|
701
|
+
"columns": columns,
|
|
702
|
+
"row_count": len(results),
|
|
703
|
+
}
|
|
656
704
|
except Exception as e:
|
|
657
705
|
logger.exception(f"DuckDB query error: {e}")
|
|
658
706
|
return {"error": str(e), "results": [], "columns": []}
|
|
@@ -691,7 +739,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
691
739
|
try:
|
|
692
740
|
with duckdb.connect(str(db_path), read_only=True) as conn:
|
|
693
741
|
# Get total spans
|
|
694
|
-
total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[
|
|
742
|
+
total_spans = conn.execute("SELECT COUNT(*) FROM spans").fetchone()[
|
|
743
|
+
0
|
|
744
|
+
]
|
|
695
745
|
|
|
696
746
|
# Get total unique traces
|
|
697
747
|
total_traces = conn.execute(
|
|
@@ -777,7 +827,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
777
827
|
"events": [event.model_dump() for event in history],
|
|
778
828
|
}
|
|
779
829
|
except Exception as e:
|
|
780
|
-
logger.exception(
|
|
830
|
+
logger.exception(
|
|
831
|
+
f"Failed to get streaming history for {agent_name}: {e}"
|
|
832
|
+
)
|
|
781
833
|
raise HTTPException(
|
|
782
834
|
status_code=500, detail=f"Failed to get streaming history: {e!s}"
|
|
783
835
|
)
|
|
@@ -818,24 +870,25 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
818
870
|
|
|
819
871
|
# 1. Get messages PRODUCED by this node
|
|
820
872
|
produced_filter = FilterConfig(produced_by={node_id})
|
|
821
|
-
|
|
873
|
+
(
|
|
874
|
+
produced_artifacts,
|
|
875
|
+
_produced_count,
|
|
876
|
+
) = await orchestrator.store.query_artifacts(
|
|
822
877
|
produced_filter, limit=100, offset=0, embed_meta=False
|
|
823
878
|
)
|
|
824
879
|
|
|
825
880
|
for artifact in produced_artifacts:
|
|
826
|
-
messages.append(
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
)
|
|
881
|
+
messages.append({
|
|
882
|
+
"id": str(artifact.id),
|
|
883
|
+
"type": artifact.type,
|
|
884
|
+
"direction": "published",
|
|
885
|
+
"payload": artifact.payload,
|
|
886
|
+
"timestamp": artifact.created_at.isoformat(),
|
|
887
|
+
"correlation_id": str(artifact.correlation_id)
|
|
888
|
+
if artifact.correlation_id
|
|
889
|
+
else None,
|
|
890
|
+
"produced_by": artifact.produced_by,
|
|
891
|
+
})
|
|
839
892
|
|
|
840
893
|
# 2. Get messages CONSUMED by this node
|
|
841
894
|
# Query all artifacts with consumption metadata
|
|
@@ -848,29 +901,35 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
848
901
|
artifact = envelope.artifact
|
|
849
902
|
for consumption in envelope.consumptions:
|
|
850
903
|
if consumption.consumer == node_id:
|
|
851
|
-
messages.append(
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
)
|
|
904
|
+
messages.append({
|
|
905
|
+
"id": str(artifact.id),
|
|
906
|
+
"type": artifact.type,
|
|
907
|
+
"direction": "consumed",
|
|
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
|
+
"consumed_at": consumption.consumed_at.isoformat(),
|
|
915
|
+
})
|
|
865
916
|
|
|
866
917
|
# Sort by timestamp (most recent first)
|
|
867
|
-
messages.sort(
|
|
918
|
+
messages.sort(
|
|
919
|
+
key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True
|
|
920
|
+
)
|
|
868
921
|
|
|
869
|
-
return {
|
|
922
|
+
return {
|
|
923
|
+
"node_id": node_id,
|
|
924
|
+
"messages": messages,
|
|
925
|
+
"total": len(messages),
|
|
926
|
+
}
|
|
870
927
|
|
|
871
928
|
except Exception as e:
|
|
872
929
|
logger.exception(f"Failed to get message history for {node_id}: {e}")
|
|
873
|
-
raise HTTPException(
|
|
930
|
+
raise HTTPException(
|
|
931
|
+
status_code=500, detail=f"Failed to get message history: {e!s}"
|
|
932
|
+
)
|
|
874
933
|
|
|
875
934
|
@app.get("/api/agents/{agent_id}/runs")
|
|
876
935
|
async def get_agent_runs(agent_id: str) -> dict[str, Any]:
|
|
@@ -919,7 +978,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
919
978
|
|
|
920
979
|
except Exception as e:
|
|
921
980
|
logger.exception(f"Failed to get run history for {agent_id}: {e}")
|
|
922
|
-
raise HTTPException(
|
|
981
|
+
raise HTTPException(
|
|
982
|
+
status_code=500, detail=f"Failed to get run history: {e!s}"
|
|
983
|
+
)
|
|
923
984
|
|
|
924
985
|
def _register_theme_routes(self) -> None:
|
|
925
986
|
"""Register theme API endpoints for dashboard customization."""
|
|
@@ -947,7 +1008,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
947
1008
|
return {"themes": theme_names}
|
|
948
1009
|
except Exception as e:
|
|
949
1010
|
logger.exception(f"Failed to list themes: {e}")
|
|
950
|
-
raise HTTPException(
|
|
1011
|
+
raise HTTPException(
|
|
1012
|
+
status_code=500, detail=f"Failed to list themes: {e!s}"
|
|
1013
|
+
)
|
|
951
1014
|
|
|
952
1015
|
@app.get("/api/themes/{theme_name}")
|
|
953
1016
|
async def get_theme(theme_name: str) -> dict[str, Any]:
|
|
@@ -966,12 +1029,16 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
966
1029
|
"""
|
|
967
1030
|
try:
|
|
968
1031
|
# Sanitize theme name to prevent path traversal
|
|
969
|
-
theme_name =
|
|
1032
|
+
theme_name = (
|
|
1033
|
+
theme_name.replace("/", "").replace("\\", "").replace("..", "")
|
|
1034
|
+
)
|
|
970
1035
|
|
|
971
1036
|
theme_path = themes_dir / f"{theme_name}.toml"
|
|
972
1037
|
|
|
973
1038
|
if not theme_path.exists():
|
|
974
|
-
raise HTTPException(
|
|
1039
|
+
raise HTTPException(
|
|
1040
|
+
status_code=404, detail=f"Theme '{theme_name}' not found"
|
|
1041
|
+
)
|
|
975
1042
|
|
|
976
1043
|
# Load TOML theme
|
|
977
1044
|
theme_data = toml.load(theme_path)
|
|
@@ -981,7 +1048,9 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
981
1048
|
raise
|
|
982
1049
|
except Exception as e:
|
|
983
1050
|
logger.exception(f"Failed to load theme '{theme_name}': {e}")
|
|
984
|
-
raise HTTPException(
|
|
1051
|
+
raise HTTPException(
|
|
1052
|
+
status_code=500, detail=f"Failed to load theme: {e!s}"
|
|
1053
|
+
)
|
|
985
1054
|
|
|
986
1055
|
async def start(self) -> None:
|
|
987
1056
|
"""Start the dashboard service.
|
|
@@ -1088,22 +1157,22 @@ def _get_correlation_groups(
|
|
|
1088
1157
|
if collected_types.get(type_name, 0) < required_count
|
|
1089
1158
|
]
|
|
1090
1159
|
|
|
1091
|
-
result.append(
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
)
|
|
1160
|
+
result.append({
|
|
1161
|
+
"correlation_key": str(corr_key),
|
|
1162
|
+
"created_at": group.created_at_time.isoformat()
|
|
1163
|
+
if group.created_at_time
|
|
1164
|
+
else None,
|
|
1165
|
+
"elapsed_seconds": round(elapsed, 1),
|
|
1166
|
+
"expires_in_seconds": round(expires_in_seconds, 1)
|
|
1167
|
+
if expires_in_seconds is not None
|
|
1168
|
+
else None,
|
|
1169
|
+
"expires_in_artifacts": expires_in_artifacts,
|
|
1170
|
+
"collected_types": collected_types,
|
|
1171
|
+
"required_types": dict(group.type_counts),
|
|
1172
|
+
"waiting_for": waiting_for,
|
|
1173
|
+
"is_complete": group.is_complete(),
|
|
1174
|
+
"is_expired": group.is_expired(engine.global_sequence),
|
|
1175
|
+
})
|
|
1107
1176
|
|
|
1108
1177
|
return result
|
|
1109
1178
|
|
|
@@ -1328,7 +1397,9 @@ def _build_logic_config( # noqa: F821
|
|
|
1328
1397
|
config["batch"]["timeout_seconds"] = int(batch_spec.timeout.total_seconds())
|
|
1329
1398
|
|
|
1330
1399
|
# Get waiting state from BatchEngine
|
|
1331
|
-
batch_state = _get_batch_state(
|
|
1400
|
+
batch_state = _get_batch_state(
|
|
1401
|
+
orchestrator._batch_engine, agent.name, idx, batch_spec
|
|
1402
|
+
)
|
|
1332
1403
|
if batch_state:
|
|
1333
1404
|
if "waiting_state" not in config:
|
|
1334
1405
|
config["waiting_state"] = {"is_waiting": True}
|