flock-core 0.5.0b71__py3-none-any.whl → 0.5.0b75__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 (62) hide show
  1. flock/agent.py +39 -1
  2. flock/artifacts.py +17 -10
  3. flock/cli.py +1 -1
  4. flock/dashboard/__init__.py +2 -0
  5. flock/dashboard/collector.py +282 -6
  6. flock/dashboard/events.py +6 -0
  7. flock/dashboard/graph_builder.py +563 -0
  8. flock/dashboard/launcher.py +11 -6
  9. flock/dashboard/models/graph.py +156 -0
  10. flock/dashboard/service.py +175 -14
  11. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
  12. flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
  13. flock/dashboard/static_v2/index.html +13 -0
  14. flock/dashboard/websocket.py +2 -2
  15. flock/engines/dspy_engine.py +27 -8
  16. flock/frontend/README.md +6 -6
  17. flock/frontend/src/App.tsx +23 -31
  18. flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
  19. flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
  20. flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
  21. flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
  22. flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
  23. flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
  24. flock/frontend/src/components/graph/AgentNode.tsx +8 -6
  25. flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
  26. flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
  27. flock/frontend/src/components/graph/MessageNode.tsx +16 -3
  28. flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
  29. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
  30. flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
  31. flock/frontend/src/hooks/useModules.ts +12 -4
  32. flock/frontend/src/hooks/usePersistence.ts +5 -3
  33. flock/frontend/src/services/api.ts +3 -19
  34. flock/frontend/src/services/graphService.test.ts +330 -0
  35. flock/frontend/src/services/graphService.ts +75 -0
  36. flock/frontend/src/services/websocket.ts +104 -268
  37. flock/frontend/src/store/filterStore.test.ts +89 -1
  38. flock/frontend/src/store/filterStore.ts +38 -16
  39. flock/frontend/src/store/graphStore.test.ts +538 -173
  40. flock/frontend/src/store/graphStore.ts +374 -465
  41. flock/frontend/src/store/moduleStore.ts +51 -33
  42. flock/frontend/src/store/uiStore.ts +23 -11
  43. flock/frontend/src/types/graph.ts +77 -44
  44. flock/frontend/src/utils/mockData.ts +16 -3
  45. flock/frontend/vite.config.ts +2 -2
  46. flock/orchestrator.py +24 -6
  47. flock/service.py +2 -2
  48. flock/store.py +169 -4
  49. flock/themes/darkmatrix.toml +2 -2
  50. flock/themes/deep.toml +2 -2
  51. flock/themes/neopolitan.toml +4 -4
  52. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
  53. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/RECORD +56 -53
  54. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
  55. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
  56. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
  57. flock/frontend/src/services/websocket.test.ts +0 -595
  58. flock/frontend/src/utils/transforms.test.ts +0 -860
  59. flock/frontend/src/utils/transforms.ts +0 -323
  60. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
  61. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
  62. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.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
@@ -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
- # Serve static files for dashboard frontend
126
- # Look for static files in dashboard directory
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
- static_dir = dashboard_dir / "static"
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
- # Also check for 'dist' or 'build' directories (common build output names)
131
- possible_dirs = [static_dir, dashboard_dir / "dist", dashboard_dir / "build"]
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 in {dashboard_dir}. "
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.error(f"DuckDB query error: {e}")
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