flock-core 0.5.0b71__py3-none-any.whl → 0.5.1__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 +39 -1
- flock/artifacts.py +17 -10
- flock/cli.py +1 -1
- flock/dashboard/__init__.py +2 -0
- flock/dashboard/collector.py +282 -6
- flock/dashboard/events.py +6 -0
- flock/dashboard/graph_builder.py +563 -0
- flock/dashboard/launcher.py +11 -6
- flock/dashboard/models/__init__.py +1 -0
- flock/dashboard/models/graph.py +156 -0
- flock/dashboard/service.py +175 -14
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
- flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
- flock/dashboard/static_v2/index.html +13 -0
- flock/dashboard/websocket.py +2 -2
- flock/engines/dspy_engine.py +294 -20
- flock/frontend/README.md +6 -6
- flock/frontend/src/App.tsx +23 -31
- flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
- flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
- flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
- flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
- flock/frontend/src/components/graph/AgentNode.tsx +8 -6
- flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
- flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
- flock/frontend/src/components/graph/MessageNode.tsx +16 -3
- flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
- flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
- flock/frontend/src/hooks/useModules.ts +12 -4
- flock/frontend/src/hooks/usePersistence.ts +5 -3
- flock/frontend/src/services/api.ts +3 -19
- flock/frontend/src/services/graphService.test.ts +330 -0
- flock/frontend/src/services/graphService.ts +75 -0
- flock/frontend/src/services/websocket.ts +104 -268
- flock/frontend/src/store/filterStore.test.ts +89 -1
- flock/frontend/src/store/filterStore.ts +38 -16
- flock/frontend/src/store/graphStore.test.ts +538 -173
- flock/frontend/src/store/graphStore.ts +374 -465
- flock/frontend/src/store/moduleStore.ts +51 -33
- flock/frontend/src/store/uiStore.ts +23 -11
- flock/frontend/src/types/graph.ts +77 -44
- flock/frontend/src/utils/mockData.ts +16 -3
- flock/frontend/vite.config.ts +2 -2
- flock/orchestrator.py +27 -7
- flock/patches/__init__.py +5 -0
- flock/patches/dspy_streaming_patch.py +82 -0
- flock/service.py +2 -2
- flock/store.py +169 -4
- flock/themes/darkmatrix.toml +2 -2
- flock/themes/deep.toml +2 -2
- flock/themes/neopolitan.toml +4 -4
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/METADATA +20 -13
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/RECORD +59 -53
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
- flock/frontend/src/services/websocket.test.ts +0 -595
- flock/frontend/src/utils/transforms.test.ts +0 -860
- flock/frontend/src/utils/transforms.ts +0 -323
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GraphTimeRangePreset(str, Enum):
|
|
11
|
+
LAST_5_MIN = "last5min"
|
|
12
|
+
LAST_10_MIN = "last10min"
|
|
13
|
+
LAST_1_HOUR = "last1hour"
|
|
14
|
+
ALL = "all"
|
|
15
|
+
CUSTOM = "custom"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GraphTimeRange(BaseModel):
|
|
19
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
20
|
+
|
|
21
|
+
preset: GraphTimeRangePreset = GraphTimeRangePreset.LAST_10_MIN
|
|
22
|
+
start: datetime | None = None
|
|
23
|
+
end: datetime | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GraphFilters(BaseModel):
|
|
27
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
28
|
+
|
|
29
|
+
correlation_id: str | None = None
|
|
30
|
+
time_range: GraphTimeRange = Field(default_factory=GraphTimeRange)
|
|
31
|
+
artifact_types: list[str] = Field(default_factory=list, alias="artifactTypes")
|
|
32
|
+
producers: list[str] = Field(default_factory=list)
|
|
33
|
+
tags: list[str] = Field(default_factory=list)
|
|
34
|
+
visibility: list[str] = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GraphRequestOptions(BaseModel):
|
|
38
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
39
|
+
|
|
40
|
+
include_statistics: bool = Field(default=True, alias="includeStatistics")
|
|
41
|
+
label_offset_strategy: str = Field(default="stack", alias="labelOffsetStrategy")
|
|
42
|
+
limit: int = 500
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GraphRequest(BaseModel):
|
|
46
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
47
|
+
|
|
48
|
+
view_mode: Literal["agent", "blackboard"] = Field(alias="viewMode")
|
|
49
|
+
filters: GraphFilters = Field(default_factory=GraphFilters)
|
|
50
|
+
options: GraphRequestOptions = Field(default_factory=GraphRequestOptions)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GraphPosition(BaseModel):
|
|
54
|
+
x: float = 0.0
|
|
55
|
+
y: float = 0.0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class GraphMarker(BaseModel):
|
|
59
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
60
|
+
|
|
61
|
+
type: str = "arrowclosed"
|
|
62
|
+
width: float = 20.0
|
|
63
|
+
height: float = 20.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GraphNode(BaseModel):
|
|
67
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
68
|
+
|
|
69
|
+
id: str
|
|
70
|
+
type: Literal["agent", "message"]
|
|
71
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
72
|
+
position: GraphPosition | None = None
|
|
73
|
+
hidden: bool = False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GraphEdge(BaseModel):
|
|
77
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
78
|
+
|
|
79
|
+
id: str
|
|
80
|
+
source: str
|
|
81
|
+
target: str
|
|
82
|
+
type: Literal["message_flow", "transformation"]
|
|
83
|
+
label: str | None = None
|
|
84
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
85
|
+
marker_end: GraphMarker | None = Field(default=None, alias="markerEnd")
|
|
86
|
+
hidden: bool = False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GraphAgentMetrics(BaseModel):
|
|
90
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
91
|
+
|
|
92
|
+
total: int = 0
|
|
93
|
+
by_type: dict[str, int] = Field(default_factory=dict, alias="byType")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GraphStatistics(BaseModel):
|
|
97
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
98
|
+
|
|
99
|
+
produced_by_agent: dict[str, GraphAgentMetrics] = Field(
|
|
100
|
+
default_factory=dict, alias="producedByAgent"
|
|
101
|
+
)
|
|
102
|
+
consumed_by_agent: dict[str, GraphAgentMetrics] = Field(
|
|
103
|
+
default_factory=dict, alias="consumedByAgent"
|
|
104
|
+
)
|
|
105
|
+
artifact_summary: dict[str, Any] = Field(default_factory=dict, alias="artifactSummary")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class GraphArtifact(BaseModel):
|
|
109
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
110
|
+
|
|
111
|
+
artifact_id: str = Field(alias="artifactId")
|
|
112
|
+
artifact_type: str = Field(alias="artifactType")
|
|
113
|
+
produced_by: str = Field(alias="producedBy")
|
|
114
|
+
consumed_by: list[str] = Field(default_factory=list, alias="consumedBy")
|
|
115
|
+
published_at: datetime = Field(alias="publishedAt")
|
|
116
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
117
|
+
correlation_id: str | None = Field(default=None, alias="correlationId")
|
|
118
|
+
visibility_kind: str | None = Field(default=None, alias="visibilityKind")
|
|
119
|
+
tags: list[str] = Field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class GraphRun(BaseModel):
|
|
123
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
124
|
+
|
|
125
|
+
run_id: str = Field(alias="runId")
|
|
126
|
+
agent_name: str = Field(alias="agentName")
|
|
127
|
+
correlation_id: str | None = Field(default=None, alias="correlationId")
|
|
128
|
+
status: Literal["active", "completed", "error"] = "active"
|
|
129
|
+
consumed_artifacts: list[str] = Field(default_factory=list, alias="consumedArtifacts")
|
|
130
|
+
produced_artifacts: list[str] = Field(default_factory=list, alias="producedArtifacts")
|
|
131
|
+
duration_ms: float | None = Field(default=None, alias="durationMs")
|
|
132
|
+
started_at: datetime | None = Field(default=None, alias="startedAt")
|
|
133
|
+
completed_at: datetime | None = Field(default=None, alias="completedAt")
|
|
134
|
+
metrics: dict[str, Any] = Field(default_factory=dict)
|
|
135
|
+
error_message: str | None = Field(default=None, alias="errorMessage")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class GraphState(BaseModel):
|
|
139
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
140
|
+
|
|
141
|
+
consumptions: dict[str, list[str]] = Field(default_factory=dict)
|
|
142
|
+
runs: list[GraphRun] = Field(default_factory=list)
|
|
143
|
+
agent_status: dict[str, str] = Field(default_factory=dict, alias="agentStatus")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class GraphSnapshot(BaseModel):
|
|
147
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
148
|
+
|
|
149
|
+
generated_at: datetime = Field(alias="generatedAt")
|
|
150
|
+
view_mode: Literal["agent", "blackboard"] = Field(alias="viewMode")
|
|
151
|
+
filters: GraphFilters
|
|
152
|
+
nodes: list[GraphNode] = Field(default_factory=list)
|
|
153
|
+
edges: list[GraphEdge] = Field(default_factory=list)
|
|
154
|
+
statistics: GraphStatistics | None = None
|
|
155
|
+
total_artifacts: int = Field(alias="totalArtifacts", default=0)
|
|
156
|
+
truncated: bool = False
|
flock/dashboard/service.py
CHANGED
|
@@ -20,6 +20,8 @@ from pydantic import ValidationError
|
|
|
20
20
|
|
|
21
21
|
from flock.dashboard.collector import DashboardEventCollector
|
|
22
22
|
from flock.dashboard.events import MessagePublishedEvent, VisibilitySpec
|
|
23
|
+
from flock.dashboard.graph_builder import GraphAssembler
|
|
24
|
+
from flock.dashboard.models.graph import GraphRequest, GraphSnapshot
|
|
23
25
|
from flock.dashboard.websocket import WebSocketManager
|
|
24
26
|
from flock.logging.logging import get_logger
|
|
25
27
|
from flock.orchestrator import Flock
|
|
@@ -45,6 +47,8 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
45
47
|
orchestrator: Flock,
|
|
46
48
|
websocket_manager: WebSocketManager | None = None,
|
|
47
49
|
event_collector: DashboardEventCollector | None = None,
|
|
50
|
+
*,
|
|
51
|
+
use_v2: bool = False,
|
|
48
52
|
) -> None:
|
|
49
53
|
"""Initialize DashboardHTTPService.
|
|
50
54
|
|
|
@@ -58,11 +62,19 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
58
62
|
|
|
59
63
|
# Initialize WebSocket manager and event collector
|
|
60
64
|
self.websocket_manager = websocket_manager or WebSocketManager()
|
|
61
|
-
self.event_collector = event_collector or DashboardEventCollector(
|
|
65
|
+
self.event_collector = event_collector or DashboardEventCollector(
|
|
66
|
+
store=self.orchestrator.store
|
|
67
|
+
)
|
|
68
|
+
self.use_v2 = use_v2
|
|
62
69
|
|
|
63
70
|
# Integrate collector with WebSocket manager
|
|
64
71
|
self.event_collector.set_websocket_manager(self.websocket_manager)
|
|
65
72
|
|
|
73
|
+
# Graph assembler powers both dashboards by default
|
|
74
|
+
self.graph_assembler: GraphAssembler | None = GraphAssembler(
|
|
75
|
+
self.orchestrator.store, self.event_collector, self.orchestrator
|
|
76
|
+
)
|
|
77
|
+
|
|
66
78
|
# Configure CORS if DASHBOARD_DEV environment variable is set
|
|
67
79
|
if os.environ.get("DASHBOARD_DEV") == "1":
|
|
68
80
|
logger.info("DASHBOARD_DEV mode enabled - adding CORS middleware")
|
|
@@ -122,13 +134,22 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
122
134
|
# Clean up: remove client from pool
|
|
123
135
|
await self.websocket_manager.remove_client(websocket)
|
|
124
136
|
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
if self.graph_assembler is not None:
|
|
138
|
+
|
|
139
|
+
@app.post("/api/dashboard/graph", response_model=GraphSnapshot)
|
|
140
|
+
async def get_dashboard_graph(request: GraphRequest) -> GraphSnapshot:
|
|
141
|
+
"""Return server-side assembled dashboard graph snapshot."""
|
|
142
|
+
return await self.graph_assembler.build_snapshot(request)
|
|
143
|
+
|
|
127
144
|
dashboard_dir = Path(__file__).parent
|
|
128
|
-
|
|
145
|
+
frontend_root = dashboard_dir.parent / ("frontend_v2" if self.use_v2 else "frontend")
|
|
146
|
+
static_dir = dashboard_dir / ("static_v2" if self.use_v2 else "static")
|
|
129
147
|
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
possible_dirs = [
|
|
149
|
+
static_dir,
|
|
150
|
+
frontend_root / "dist",
|
|
151
|
+
frontend_root / "build",
|
|
152
|
+
]
|
|
132
153
|
|
|
133
154
|
for dir_path in possible_dirs:
|
|
134
155
|
if dir_path.exists() and dir_path.is_dir():
|
|
@@ -142,9 +163,7 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
142
163
|
break
|
|
143
164
|
else:
|
|
144
165
|
logger.warning(
|
|
145
|
-
f"No static directory found
|
|
146
|
-
"Dashboard frontend will not be served. "
|
|
147
|
-
"Expected directories: static/, dist/, or build/"
|
|
166
|
+
f"No static directory found for dashboard frontend (expected one of: {possible_dirs})."
|
|
148
167
|
)
|
|
149
168
|
|
|
150
169
|
def _register_control_routes(self) -> None:
|
|
@@ -503,9 +522,10 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
503
522
|
"operations": ["Flock.publish", "Agent.execute", ...]
|
|
504
523
|
}
|
|
505
524
|
"""
|
|
506
|
-
import duckdb
|
|
507
525
|
from pathlib import Path
|
|
508
526
|
|
|
527
|
+
import duckdb
|
|
528
|
+
|
|
509
529
|
db_path = Path(".flock/traces.duckdb")
|
|
510
530
|
|
|
511
531
|
if not db_path.exists():
|
|
@@ -564,9 +584,10 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
564
584
|
|
|
565
585
|
Security: Only SELECT queries allowed, rate-limited.
|
|
566
586
|
"""
|
|
567
|
-
import duckdb
|
|
568
587
|
from pathlib import Path
|
|
569
588
|
|
|
589
|
+
import duckdb
|
|
590
|
+
|
|
570
591
|
query = request.get("query", "").strip()
|
|
571
592
|
|
|
572
593
|
if not query:
|
|
@@ -610,7 +631,7 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
610
631
|
|
|
611
632
|
return {"results": results, "columns": columns, "row_count": len(results)}
|
|
612
633
|
except Exception as e:
|
|
613
|
-
logger.
|
|
634
|
+
logger.exception(f"DuckDB query error: {e}")
|
|
614
635
|
return {"error": str(e), "results": [], "columns": []}
|
|
615
636
|
|
|
616
637
|
@app.get("/api/traces/stats")
|
|
@@ -627,9 +648,10 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
627
648
|
"database_size_mb": 12.5
|
|
628
649
|
}
|
|
629
650
|
"""
|
|
630
|
-
import duckdb
|
|
631
|
-
from pathlib import Path
|
|
632
651
|
from datetime import datetime
|
|
652
|
+
from pathlib import Path
|
|
653
|
+
|
|
654
|
+
import duckdb
|
|
633
655
|
|
|
634
656
|
db_path = Path(".flock/traces.duckdb")
|
|
635
657
|
|
|
@@ -737,6 +759,145 @@ class DashboardHTTPService(BlackboardHTTPService):
|
|
|
737
759
|
status_code=500, detail=f"Failed to get streaming history: {e!s}"
|
|
738
760
|
)
|
|
739
761
|
|
|
762
|
+
@app.get("/api/artifacts/history/{node_id}")
|
|
763
|
+
async def get_message_history(node_id: str) -> dict[str, Any]:
|
|
764
|
+
"""Get complete message history for a node (both produced and consumed).
|
|
765
|
+
|
|
766
|
+
Phase 4.1 Feature Gap Fix: Returns both messages produced by AND consumed by
|
|
767
|
+
the specified node, enabling complete message history view in MessageHistoryTab.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
node_id: ID of the node (agent name or message ID)
|
|
771
|
+
|
|
772
|
+
Returns:
|
|
773
|
+
{
|
|
774
|
+
"node_id": "agent_name",
|
|
775
|
+
"messages": [
|
|
776
|
+
{
|
|
777
|
+
"id": "artifact-uuid",
|
|
778
|
+
"type": "ArtifactType",
|
|
779
|
+
"direction": "published"|"consumed",
|
|
780
|
+
"payload": {...},
|
|
781
|
+
"timestamp": "2025-10-11T...",
|
|
782
|
+
"correlation_id": "uuid",
|
|
783
|
+
"produced_by": "producer_name",
|
|
784
|
+
"consumed_at": "2025-10-11T..." (only for consumed)
|
|
785
|
+
},
|
|
786
|
+
...
|
|
787
|
+
],
|
|
788
|
+
"total": 123
|
|
789
|
+
}
|
|
790
|
+
"""
|
|
791
|
+
try:
|
|
792
|
+
from flock.store import FilterConfig
|
|
793
|
+
|
|
794
|
+
messages = []
|
|
795
|
+
|
|
796
|
+
# 1. Get messages PRODUCED by this node
|
|
797
|
+
produced_filter = FilterConfig(produced_by={node_id})
|
|
798
|
+
produced_artifacts, _produced_count = await orchestrator.store.query_artifacts(
|
|
799
|
+
produced_filter, limit=100, offset=0, embed_meta=False
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
for artifact in produced_artifacts:
|
|
803
|
+
messages.append(
|
|
804
|
+
{
|
|
805
|
+
"id": str(artifact.id),
|
|
806
|
+
"type": artifact.type,
|
|
807
|
+
"direction": "published",
|
|
808
|
+
"payload": artifact.payload,
|
|
809
|
+
"timestamp": artifact.created_at.isoformat(),
|
|
810
|
+
"correlation_id": str(artifact.correlation_id)
|
|
811
|
+
if artifact.correlation_id
|
|
812
|
+
else None,
|
|
813
|
+
"produced_by": artifact.produced_by,
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
# 2. Get messages CONSUMED by this node
|
|
818
|
+
# Query all artifacts with consumption metadata
|
|
819
|
+
all_artifacts_filter = FilterConfig() # No filter = all artifacts
|
|
820
|
+
all_envelopes, _ = await orchestrator.store.query_artifacts(
|
|
821
|
+
all_artifacts_filter, limit=500, offset=0, embed_meta=True
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
for envelope in all_envelopes:
|
|
825
|
+
artifact = envelope.artifact
|
|
826
|
+
for consumption in envelope.consumptions:
|
|
827
|
+
if consumption.consumer == node_id:
|
|
828
|
+
messages.append(
|
|
829
|
+
{
|
|
830
|
+
"id": str(artifact.id),
|
|
831
|
+
"type": artifact.type,
|
|
832
|
+
"direction": "consumed",
|
|
833
|
+
"payload": artifact.payload,
|
|
834
|
+
"timestamp": artifact.created_at.isoformat(),
|
|
835
|
+
"correlation_id": str(artifact.correlation_id)
|
|
836
|
+
if artifact.correlation_id
|
|
837
|
+
else None,
|
|
838
|
+
"produced_by": artifact.produced_by,
|
|
839
|
+
"consumed_at": consumption.consumed_at.isoformat(),
|
|
840
|
+
}
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
# Sort by timestamp (most recent first)
|
|
844
|
+
messages.sort(key=lambda m: m.get("consumed_at", m["timestamp"]), reverse=True)
|
|
845
|
+
|
|
846
|
+
return {"node_id": node_id, "messages": messages, "total": len(messages)}
|
|
847
|
+
|
|
848
|
+
except Exception as e:
|
|
849
|
+
logger.exception(f"Failed to get message history for {node_id}: {e}")
|
|
850
|
+
raise HTTPException(status_code=500, detail=f"Failed to get message history: {e!s}")
|
|
851
|
+
|
|
852
|
+
@app.get("/api/agents/{agent_id}/runs")
|
|
853
|
+
async def get_agent_runs(agent_id: str) -> dict[str, Any]:
|
|
854
|
+
"""Get run history for an agent.
|
|
855
|
+
|
|
856
|
+
Phase 4.1 Feature Gap Fix: Returns agent execution history with metrics
|
|
857
|
+
for display in RunStatusTab.
|
|
858
|
+
|
|
859
|
+
Args:
|
|
860
|
+
agent_id: ID of the agent
|
|
861
|
+
|
|
862
|
+
Returns:
|
|
863
|
+
{
|
|
864
|
+
"agent_id": "agent_name",
|
|
865
|
+
"runs": [
|
|
866
|
+
{
|
|
867
|
+
"run_id": "uuid",
|
|
868
|
+
"start_time": "2025-10-11T...",
|
|
869
|
+
"end_time": "2025-10-11T...",
|
|
870
|
+
"duration_ms": 1234,
|
|
871
|
+
"status": "completed"|"active"|"error",
|
|
872
|
+
"metrics": {
|
|
873
|
+
"tokens_used": 123,
|
|
874
|
+
"cost_usd": 0.0012,
|
|
875
|
+
"artifacts_produced": 5
|
|
876
|
+
},
|
|
877
|
+
"error_message": "error details" (if status=error)
|
|
878
|
+
},
|
|
879
|
+
...
|
|
880
|
+
],
|
|
881
|
+
"total": 50
|
|
882
|
+
}
|
|
883
|
+
"""
|
|
884
|
+
try:
|
|
885
|
+
# TODO: Implement run history tracking in orchestrator
|
|
886
|
+
# For now, return empty array with proper structure
|
|
887
|
+
# This unblocks frontend development and can be enhanced later
|
|
888
|
+
|
|
889
|
+
runs = []
|
|
890
|
+
|
|
891
|
+
# FUTURE: Query run history from orchestrator or store
|
|
892
|
+
# Example implementation when run tracking is added:
|
|
893
|
+
# runs = await orchestrator.get_agent_run_history(agent_id, limit=50)
|
|
894
|
+
|
|
895
|
+
return {"agent_id": agent_id, "runs": runs, "total": len(runs)}
|
|
896
|
+
|
|
897
|
+
except Exception as e:
|
|
898
|
+
logger.exception(f"Failed to get run history for {agent_id}: {e}")
|
|
899
|
+
raise HTTPException(status_code=500, detail=f"Failed to get run history: {e!s}")
|
|
900
|
+
|
|
740
901
|
def _register_theme_routes(self) -> None:
|
|
741
902
|
"""Register theme API endpoints for dashboard customization."""
|
|
742
903
|
from pathlib import Path
|