flock-core 0.5.3__py3-none-any.whl → 0.5.5__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 +20 -1
- flock/artifact_collector.py +2 -3
- flock/batch_accumulator.py +4 -4
- flock/components.py +32 -0
- flock/correlation_engine.py +9 -4
- flock/dashboard/collector.py +4 -0
- flock/dashboard/events.py +74 -0
- flock/dashboard/graph_builder.py +272 -0
- flock/dashboard/models/graph.py +3 -1
- flock/dashboard/service.py +363 -14
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +1 -1
- flock/dashboard/static_v2/index.html +3 -3
- flock/engines/dspy_engine.py +41 -3
- flock/engines/examples/__init__.py +6 -0
- flock/engines/examples/simple_batch_engine.py +61 -0
- flock/frontend/README.md +4 -4
- flock/frontend/docs/DESIGN_SYSTEM.md +1 -1
- flock/frontend/package-lock.json +2 -2
- flock/frontend/package.json +2 -2
- flock/frontend/src/components/controls/PublishControl.test.tsx +11 -11
- flock/frontend/src/components/controls/PublishControl.tsx +1 -1
- flock/frontend/src/components/graph/AgentNode.tsx +4 -0
- flock/frontend/src/components/graph/GraphCanvas.tsx +4 -0
- flock/frontend/src/components/graph/LogicOperationsDisplay.tsx +463 -0
- flock/frontend/src/components/graph/PendingBatchEdge.tsx +141 -0
- flock/frontend/src/components/graph/PendingJoinEdge.tsx +144 -0
- flock/frontend/src/components/settings/SettingsPanel.css +1 -1
- flock/frontend/src/components/settings/ThemeSelector.tsx +2 -2
- flock/frontend/src/services/graphService.ts +3 -1
- flock/frontend/src/services/indexeddb.ts +1 -1
- flock/frontend/src/services/websocket.ts +99 -1
- flock/frontend/src/store/graphStore.test.ts +2 -1
- flock/frontend/src/store/graphStore.ts +36 -5
- flock/frontend/src/styles/variables.css +1 -1
- flock/frontend/src/types/graph.ts +86 -0
- flock/orchestrator.py +268 -13
- flock/patches/__init__.py +1 -0
- flock/patches/dspy_streaming_patch.py +1 -0
- flock/runtime.py +3 -0
- {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/METADATA +11 -1
- {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/RECORD +44 -39
- {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/WHEEL +0 -0
- {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.3.dist-info → flock_core-0.5.5.dist-info}/licenses/LICENSE +0 -0
flock/agent.py
CHANGED
|
@@ -249,7 +249,26 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
249
249
|
accumulated_metrics: dict[str, float] = {}
|
|
250
250
|
for engine in engines:
|
|
251
251
|
current_inputs = await engine.on_pre_evaluate(self, ctx, current_inputs)
|
|
252
|
-
|
|
252
|
+
use_batch_mode = bool(getattr(ctx, "is_batch", False))
|
|
253
|
+
try:
|
|
254
|
+
if use_batch_mode:
|
|
255
|
+
logger.debug(
|
|
256
|
+
"Agent %s: routing %d artifacts to %s.evaluate_batch",
|
|
257
|
+
self.name,
|
|
258
|
+
len(current_inputs.artifacts),
|
|
259
|
+
engine.__class__.__name__,
|
|
260
|
+
)
|
|
261
|
+
result = await engine.evaluate_batch(self, ctx, current_inputs)
|
|
262
|
+
else:
|
|
263
|
+
result = await engine.evaluate(self, ctx, current_inputs)
|
|
264
|
+
except NotImplementedError:
|
|
265
|
+
if use_batch_mode:
|
|
266
|
+
logger.error(
|
|
267
|
+
"Agent %s: engine %s does not implement evaluate_batch()",
|
|
268
|
+
self.name,
|
|
269
|
+
engine.__class__.__name__,
|
|
270
|
+
)
|
|
271
|
+
raise
|
|
253
272
|
|
|
254
273
|
# AUTO-WRAP: If engine returns BaseModel instead of EvalResult, wrap it
|
|
255
274
|
from flock.runtime import EvalResult as ER
|
flock/artifact_collector.py
CHANGED
|
@@ -110,9 +110,8 @@ class ArtifactCollector:
|
|
|
110
110
|
|
|
111
111
|
del self._waiting_pools[pool_key] # Clear for next cycle
|
|
112
112
|
return (True, artifacts)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return (False, [])
|
|
113
|
+
# Incomplete - still waiting for more artifacts
|
|
114
|
+
return (False, [])
|
|
116
115
|
|
|
117
116
|
def get_waiting_status(
|
|
118
117
|
self, agent: Agent, subscription_index: int
|
flock/batch_accumulator.py
CHANGED
|
@@ -11,10 +11,10 @@ Supports BatchSpec-based batching:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
from
|
|
15
|
-
from datetime import datetime, timedelta
|
|
14
|
+
from datetime import datetime
|
|
16
15
|
from typing import TYPE_CHECKING
|
|
17
16
|
|
|
17
|
+
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from flock.artifacts import Artifact
|
|
20
20
|
from flock.subscription import BatchSpec, Subscription
|
|
@@ -184,7 +184,7 @@ class BatchEngine:
|
|
|
184
184
|
# This will work correctly if all groups are the same size
|
|
185
185
|
|
|
186
186
|
# Actually, let's track group count properly:
|
|
187
|
-
if not hasattr(accumulator,
|
|
187
|
+
if not hasattr(accumulator, "_group_count"):
|
|
188
188
|
accumulator._group_count = 0
|
|
189
189
|
|
|
190
190
|
accumulator._group_count += 1
|
|
@@ -249,4 +249,4 @@ class BatchEngine:
|
|
|
249
249
|
return results
|
|
250
250
|
|
|
251
251
|
|
|
252
|
-
__all__ = ["
|
|
252
|
+
__all__ = ["BatchAccumulator", "BatchEngine"]
|
flock/components.py
CHANGED
|
@@ -109,6 +109,38 @@ class EngineComponent(AgentComponent):
|
|
|
109
109
|
"""Override this method in your engine implementation."""
|
|
110
110
|
raise NotImplementedError
|
|
111
111
|
|
|
112
|
+
async def evaluate_batch(self, agent: Agent, ctx: Context, inputs: EvalInputs) -> EvalResult:
|
|
113
|
+
"""Process batch of accumulated artifacts (BatchSpec).
|
|
114
|
+
|
|
115
|
+
Override this method if your engine supports batch processing.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
agent: Agent instance executing this engine
|
|
119
|
+
ctx: Execution context (ctx.is_batch will be True)
|
|
120
|
+
inputs: EvalInputs with inputs.artifacts containing batch items
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
EvalResult with processed artifacts
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
NotImplementedError: If engine doesn't support batching
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> async def evaluate_batch(self, agent, ctx, inputs):
|
|
130
|
+
... events = inputs.all_as(Event) # Get ALL items
|
|
131
|
+
... results = await bulk_process(events)
|
|
132
|
+
... return EvalResult.from_objects(*results, agent=agent)
|
|
133
|
+
"""
|
|
134
|
+
raise NotImplementedError(
|
|
135
|
+
f"{self.__class__.__name__} does not support batch processing.\n\n"
|
|
136
|
+
f"To fix this:\n"
|
|
137
|
+
f"1. Remove BatchSpec from agent subscription, OR\n"
|
|
138
|
+
f"2. Implement evaluate_batch() in {self.__class__.__name__}, OR\n"
|
|
139
|
+
f"3. Use a batch-aware engine (e.g., CustomBatchEngine)\n\n"
|
|
140
|
+
f"Agent: {agent.name}\n"
|
|
141
|
+
f"Engine: {self.__class__.__name__}"
|
|
142
|
+
)
|
|
143
|
+
|
|
112
144
|
async def fetch_conversation_context(
|
|
113
145
|
self,
|
|
114
146
|
ctx: Context,
|
flock/correlation_engine.py
CHANGED
|
@@ -14,6 +14,7 @@ from collections import defaultdict
|
|
|
14
14
|
from datetime import datetime, timedelta
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from flock.artifacts import Artifact
|
|
19
20
|
from flock.subscription import JoinSpec, Subscription
|
|
@@ -51,7 +52,9 @@ class CorrelationGroup:
|
|
|
51
52
|
def add_artifact(self, artifact: Artifact, current_sequence: int) -> None:
|
|
52
53
|
"""Add artifact to this correlation group's waiting pool."""
|
|
53
54
|
if self.created_at_time is None:
|
|
54
|
-
|
|
55
|
+
from datetime import timezone
|
|
56
|
+
|
|
57
|
+
self.created_at_time = datetime.now(timezone.utc)
|
|
55
58
|
|
|
56
59
|
self.waiting_artifacts[artifact.type].append(artifact)
|
|
57
60
|
|
|
@@ -67,11 +70,13 @@ class CorrelationGroup:
|
|
|
67
70
|
if isinstance(self.window_spec, int):
|
|
68
71
|
# Count window: expired if current sequence exceeds created + window
|
|
69
72
|
return (current_sequence - self.created_at_sequence) > self.window_spec
|
|
70
|
-
|
|
73
|
+
if isinstance(self.window_spec, timedelta):
|
|
71
74
|
# Time window: expired if current time exceeds created + window
|
|
72
75
|
if self.created_at_time is None:
|
|
73
76
|
return False
|
|
74
|
-
|
|
77
|
+
from datetime import timezone
|
|
78
|
+
|
|
79
|
+
elapsed = datetime.now(timezone.utc) - self.created_at_time
|
|
75
80
|
return elapsed > self.window_spec
|
|
76
81
|
return False
|
|
77
82
|
|
|
@@ -154,7 +159,7 @@ class CorrelationEngine:
|
|
|
154
159
|
|
|
155
160
|
try:
|
|
156
161
|
correlation_key = join_spec.by(payload_instance)
|
|
157
|
-
except Exception
|
|
162
|
+
except Exception:
|
|
158
163
|
# Key extraction failed - skip this artifact
|
|
159
164
|
# TODO: Log warning?
|
|
160
165
|
return None
|
flock/dashboard/collector.py
CHANGED
|
@@ -80,6 +80,9 @@ class AgentSnapshot:
|
|
|
80
80
|
first_seen: datetime
|
|
81
81
|
last_seen: datetime
|
|
82
82
|
signature: str
|
|
83
|
+
logic_operations: list[dict] = field(
|
|
84
|
+
default_factory=list
|
|
85
|
+
) # Phase 1.2: JoinSpec/BatchSpec config
|
|
83
86
|
|
|
84
87
|
|
|
85
88
|
class DashboardEventCollector(AgentComponent):
|
|
@@ -514,6 +517,7 @@ class DashboardEventCollector(AgentComponent):
|
|
|
514
517
|
first_seen=snapshot.first_seen,
|
|
515
518
|
last_seen=snapshot.last_seen,
|
|
516
519
|
signature=snapshot.signature,
|
|
520
|
+
logic_operations=[dict(op) for op in snapshot.logic_operations], # Phase 1.2
|
|
517
521
|
)
|
|
518
522
|
|
|
519
523
|
def _snapshot_to_record(self, snapshot: AgentSnapshot) -> AgentSnapshotRecord:
|
flock/dashboard/events.py
CHANGED
|
@@ -177,10 +177,84 @@ class AgentErrorEvent(BaseModel):
|
|
|
177
177
|
failed_at: str # ISO timestamp of failure
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
class CorrelationGroupUpdatedEvent(BaseModel):
|
|
181
|
+
"""Event emitted when artifact added to correlation group.
|
|
182
|
+
|
|
183
|
+
Phase 1.2: Logic Operations UX Enhancement
|
|
184
|
+
Emitted when an artifact is added to a JoinSpec correlation group
|
|
185
|
+
that has not yet collected all required types.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
# Event metadata
|
|
189
|
+
timestamp: str = Field(
|
|
190
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Agent identification
|
|
194
|
+
agent_name: str
|
|
195
|
+
subscription_index: int
|
|
196
|
+
|
|
197
|
+
# Correlation group
|
|
198
|
+
correlation_key: str # "patient_123"
|
|
199
|
+
|
|
200
|
+
# Progress
|
|
201
|
+
collected_types: dict[str, int] # {"XRayImage": 1, "LabResults": 0}
|
|
202
|
+
required_types: dict[str, int] # {"XRayImage": 1, "LabResults": 1}
|
|
203
|
+
waiting_for: list[str] # ["LabResults"]
|
|
204
|
+
|
|
205
|
+
# Window progress
|
|
206
|
+
elapsed_seconds: float
|
|
207
|
+
expires_in_seconds: float | None # For time windows
|
|
208
|
+
expires_in_artifacts: int | None # For count windows
|
|
209
|
+
|
|
210
|
+
# Artifact that triggered this event
|
|
211
|
+
artifact_id: str
|
|
212
|
+
artifact_type: str
|
|
213
|
+
|
|
214
|
+
is_complete: bool # Will trigger agent in next orchestrator cycle
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class BatchItemAddedEvent(BaseModel):
|
|
218
|
+
"""Event emitted when artifact added to batch accumulator.
|
|
219
|
+
|
|
220
|
+
Phase 1.2: Logic Operations UX Enhancement
|
|
221
|
+
Emitted when an artifact is added to a BatchSpec accumulator
|
|
222
|
+
that has not yet reached its flush threshold.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
# Event metadata
|
|
226
|
+
timestamp: str = Field(
|
|
227
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Agent identification
|
|
231
|
+
agent_name: str
|
|
232
|
+
subscription_index: int
|
|
233
|
+
|
|
234
|
+
# Batch progress
|
|
235
|
+
items_collected: int
|
|
236
|
+
items_target: int | None # None if no size limit
|
|
237
|
+
items_remaining: int | None
|
|
238
|
+
|
|
239
|
+
# Timeout progress
|
|
240
|
+
elapsed_seconds: float
|
|
241
|
+
timeout_seconds: float | None
|
|
242
|
+
timeout_remaining_seconds: float | None
|
|
243
|
+
|
|
244
|
+
# Trigger prediction
|
|
245
|
+
will_flush: str # "on_size" | "on_timeout" | "unknown"
|
|
246
|
+
|
|
247
|
+
# Artifact that triggered this event
|
|
248
|
+
artifact_id: str
|
|
249
|
+
artifact_type: str
|
|
250
|
+
|
|
251
|
+
|
|
180
252
|
__all__ = [
|
|
181
253
|
"AgentActivatedEvent",
|
|
182
254
|
"AgentCompletedEvent",
|
|
183
255
|
"AgentErrorEvent",
|
|
256
|
+
"BatchItemAddedEvent",
|
|
257
|
+
"CorrelationGroupUpdatedEvent",
|
|
184
258
|
"MessagePublishedEvent",
|
|
185
259
|
"StreamingOutputEvent",
|
|
186
260
|
"SubscriptionInfo",
|
flock/dashboard/graph_builder.py
CHANGED
|
@@ -73,6 +73,9 @@ class GraphAssembler(metaclass=AutoTracedMeta):
|
|
|
73
73
|
agent_snapshots,
|
|
74
74
|
)
|
|
75
75
|
edges = self._derive_agent_edges(artifacts)
|
|
76
|
+
# Phase 1.5: Add pending edges for artifacts waiting in correlation/batch
|
|
77
|
+
pending_edges = self._derive_pending_edges(artifacts)
|
|
78
|
+
edges.extend(pending_edges)
|
|
76
79
|
else:
|
|
77
80
|
nodes = self._build_message_nodes(artifacts)
|
|
78
81
|
edges = self._derive_blackboard_edges(artifacts, graph_state)
|
|
@@ -192,6 +195,14 @@ class GraphAssembler(metaclass=AutoTracedMeta):
|
|
|
192
195
|
consumed = consumed_metrics.get(agent.name)
|
|
193
196
|
snapshot = agent_snapshots.get(agent.name)
|
|
194
197
|
|
|
198
|
+
# Phase 1.2: Build logic_operations from live agent subscriptions
|
|
199
|
+
logic_operations = []
|
|
200
|
+
for idx, subscription in enumerate(agent.subscriptions):
|
|
201
|
+
if subscription.join or subscription.batch:
|
|
202
|
+
logic_config = self._build_logic_config_for_subscription(agent, subscription, idx)
|
|
203
|
+
if logic_config:
|
|
204
|
+
logic_operations.append(logic_config)
|
|
205
|
+
|
|
195
206
|
node_data = {
|
|
196
207
|
"name": agent.name,
|
|
197
208
|
"status": agent_status.get(agent.name, "idle"),
|
|
@@ -206,6 +217,7 @@ class GraphAssembler(metaclass=AutoTracedMeta):
|
|
|
206
217
|
"firstSeen": snapshot.first_seen.isoformat() if snapshot else None,
|
|
207
218
|
"lastSeen": snapshot.last_seen.isoformat() if snapshot else None,
|
|
208
219
|
"signature": snapshot.signature if snapshot else None,
|
|
220
|
+
"logicOperations": logic_operations, # Phase 1.2
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
nodes.append(
|
|
@@ -241,6 +253,7 @@ class GraphAssembler(metaclass=AutoTracedMeta):
|
|
|
241
253
|
"firstSeen": snapshot.first_seen.isoformat(),
|
|
242
254
|
"lastSeen": snapshot.last_seen.isoformat(),
|
|
243
255
|
"signature": snapshot.signature,
|
|
256
|
+
"logicOperations": list(snapshot.logic_operations), # Phase 1.2: From snapshot
|
|
244
257
|
}
|
|
245
258
|
|
|
246
259
|
nodes.append(
|
|
@@ -378,6 +391,196 @@ class GraphAssembler(metaclass=AutoTracedMeta):
|
|
|
378
391
|
|
|
379
392
|
return edges
|
|
380
393
|
|
|
394
|
+
def _derive_pending_edges(
|
|
395
|
+
self,
|
|
396
|
+
artifacts: Mapping[str, GraphArtifact],
|
|
397
|
+
) -> list[GraphEdge]:
|
|
398
|
+
"""
|
|
399
|
+
Phase 1.5: Build pending edges for artifacts waiting in correlation/batch.
|
|
400
|
+
|
|
401
|
+
Creates visual edges (purple/orange dashed) from artifact producers to agents
|
|
402
|
+
that have those artifacts waiting in JoinSpec/BatchSpec queues.
|
|
403
|
+
|
|
404
|
+
Returns edges with type "pending_join" or "pending_batch".
|
|
405
|
+
"""
|
|
406
|
+
edges: list[GraphEdge] = []
|
|
407
|
+
edge_counter = 0
|
|
408
|
+
|
|
409
|
+
# Build map of artifact_id -> artifact for quick lookup
|
|
410
|
+
artifact_map = {art.artifact_id: art for art in artifacts.values()}
|
|
411
|
+
|
|
412
|
+
# Iterate through all active agents with JoinSpec/BatchSpec subscriptions
|
|
413
|
+
for agent in self._orchestrator.agents:
|
|
414
|
+
for sub_idx, subscription in enumerate(agent.subscriptions):
|
|
415
|
+
# JoinSpec pending edges
|
|
416
|
+
if subscription.join:
|
|
417
|
+
pending_join_edges = self._build_pending_join_edges(
|
|
418
|
+
agent.name,
|
|
419
|
+
sub_idx,
|
|
420
|
+
subscription,
|
|
421
|
+
artifact_map,
|
|
422
|
+
edge_counter
|
|
423
|
+
)
|
|
424
|
+
edges.extend(pending_join_edges)
|
|
425
|
+
edge_counter += len(pending_join_edges)
|
|
426
|
+
|
|
427
|
+
# BatchSpec pending edges
|
|
428
|
+
if subscription.batch:
|
|
429
|
+
pending_batch_edges = self._build_pending_batch_edges(
|
|
430
|
+
agent.name,
|
|
431
|
+
sub_idx,
|
|
432
|
+
subscription,
|
|
433
|
+
artifact_map,
|
|
434
|
+
edge_counter
|
|
435
|
+
)
|
|
436
|
+
edges.extend(pending_batch_edges)
|
|
437
|
+
edge_counter += len(pending_batch_edges)
|
|
438
|
+
|
|
439
|
+
return edges
|
|
440
|
+
|
|
441
|
+
def _build_pending_join_edges(
|
|
442
|
+
self,
|
|
443
|
+
agent_name: str,
|
|
444
|
+
sub_idx: int,
|
|
445
|
+
subscription: "Subscription", # noqa: F821
|
|
446
|
+
artifact_map: Mapping[str, GraphArtifact],
|
|
447
|
+
start_counter: int,
|
|
448
|
+
) -> list[GraphEdge]:
|
|
449
|
+
"""Build pending edges for JoinSpec correlation groups."""
|
|
450
|
+
from flock.dashboard.service import _get_correlation_groups
|
|
451
|
+
|
|
452
|
+
edges: list[GraphEdge] = []
|
|
453
|
+
correlation_groups = _get_correlation_groups(
|
|
454
|
+
self._orchestrator._correlation_engine,
|
|
455
|
+
agent_name,
|
|
456
|
+
sub_idx
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if not correlation_groups:
|
|
460
|
+
return edges
|
|
461
|
+
|
|
462
|
+
edge_counter = start_counter
|
|
463
|
+
|
|
464
|
+
# For each correlation group, create edges for waiting artifacts
|
|
465
|
+
for group in correlation_groups:
|
|
466
|
+
# Get artifacts in this correlation group's waiting pool
|
|
467
|
+
correlation_key = group.get("correlation_key")
|
|
468
|
+
if not correlation_key:
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
# Get the actual correlation group from engine
|
|
472
|
+
pool_key = (agent_name, sub_idx)
|
|
473
|
+
groups_dict = self._orchestrator._correlation_engine.correlation_groups.get(pool_key, {})
|
|
474
|
+
corr_group = groups_dict.get(correlation_key)
|
|
475
|
+
|
|
476
|
+
if not corr_group:
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
# Build edges for each artifact type in the waiting pool
|
|
480
|
+
for artifact_type, artifact_list in corr_group.waiting_artifacts.items():
|
|
481
|
+
for artifact in artifact_list:
|
|
482
|
+
artifact_id = str(artifact.id)
|
|
483
|
+
graph_artifact = artifact_map.get(artifact_id)
|
|
484
|
+
|
|
485
|
+
if not graph_artifact:
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
producer = graph_artifact.produced_by or "external"
|
|
489
|
+
|
|
490
|
+
# Create pending join edge
|
|
491
|
+
edge_id = f"pending_join__{producer}__{agent_name}__{artifact_id}__{edge_counter}"
|
|
492
|
+
edge_counter += 1
|
|
493
|
+
|
|
494
|
+
edges.append(
|
|
495
|
+
GraphEdge(
|
|
496
|
+
id=edge_id,
|
|
497
|
+
source=producer,
|
|
498
|
+
target=agent_name,
|
|
499
|
+
type="pending_join",
|
|
500
|
+
label=f"⋈ {correlation_key}",
|
|
501
|
+
data={
|
|
502
|
+
"artifactId": artifact_id,
|
|
503
|
+
"artifactType": artifact_type,
|
|
504
|
+
"correlationKey": correlation_key,
|
|
505
|
+
"subscriptionIndex": sub_idx,
|
|
506
|
+
"waitingFor": group.get("waiting_for", []),
|
|
507
|
+
"labelOffset": 0.0,
|
|
508
|
+
},
|
|
509
|
+
marker_end=GraphMarker(),
|
|
510
|
+
hidden=False,
|
|
511
|
+
)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return edges
|
|
515
|
+
|
|
516
|
+
def _build_pending_batch_edges(
|
|
517
|
+
self,
|
|
518
|
+
agent_name: str,
|
|
519
|
+
sub_idx: int,
|
|
520
|
+
subscription: "Subscription", # noqa: F821
|
|
521
|
+
artifact_map: Mapping[str, GraphArtifact],
|
|
522
|
+
start_counter: int,
|
|
523
|
+
) -> list[GraphEdge]:
|
|
524
|
+
"""Build pending edges for BatchSpec accumulation."""
|
|
525
|
+
from flock.dashboard.service import _get_batch_state
|
|
526
|
+
|
|
527
|
+
edges: list[GraphEdge] = []
|
|
528
|
+
batch_state = _get_batch_state(
|
|
529
|
+
self._orchestrator._batch_engine,
|
|
530
|
+
agent_name,
|
|
531
|
+
sub_idx,
|
|
532
|
+
subscription.batch
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if not batch_state:
|
|
536
|
+
return edges
|
|
537
|
+
|
|
538
|
+
# Get the batch accumulator
|
|
539
|
+
batch_key = (agent_name, sub_idx)
|
|
540
|
+
accumulator = self._orchestrator._batch_engine.batches.get(batch_key)
|
|
541
|
+
|
|
542
|
+
if not accumulator or not accumulator.artifacts:
|
|
543
|
+
return edges
|
|
544
|
+
|
|
545
|
+
edge_counter = start_counter
|
|
546
|
+
|
|
547
|
+
# Create edges for each artifact in the batch
|
|
548
|
+
for artifact in accumulator.artifacts:
|
|
549
|
+
artifact_id = str(artifact.id)
|
|
550
|
+
graph_artifact = artifact_map.get(artifact_id)
|
|
551
|
+
|
|
552
|
+
if not graph_artifact:
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
producer = graph_artifact.produced_by or "external"
|
|
556
|
+
artifact_type = graph_artifact.artifact_type
|
|
557
|
+
|
|
558
|
+
# Create pending batch edge
|
|
559
|
+
edge_id = f"pending_batch__{producer}__{agent_name}__{artifact_id}__{edge_counter}"
|
|
560
|
+
edge_counter += 1
|
|
561
|
+
|
|
562
|
+
edges.append(
|
|
563
|
+
GraphEdge(
|
|
564
|
+
id=edge_id,
|
|
565
|
+
source=producer,
|
|
566
|
+
target=agent_name,
|
|
567
|
+
type="pending_batch",
|
|
568
|
+
label=f"⊞ {batch_state['items_collected']}/{batch_state['items_target'] or '∞'}",
|
|
569
|
+
data={
|
|
570
|
+
"artifactId": artifact_id,
|
|
571
|
+
"artifactType": artifact_type,
|
|
572
|
+
"subscriptionIndex": sub_idx,
|
|
573
|
+
"itemsCollected": batch_state["items_collected"],
|
|
574
|
+
"itemsTarget": batch_state["items_target"],
|
|
575
|
+
"labelOffset": 0.0,
|
|
576
|
+
},
|
|
577
|
+
marker_end=GraphMarker(),
|
|
578
|
+
hidden=False,
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
return edges
|
|
583
|
+
|
|
381
584
|
def _derive_blackboard_edges(
|
|
382
585
|
self,
|
|
383
586
|
artifacts: Mapping[str, GraphArtifact],
|
|
@@ -561,3 +764,72 @@ class GraphAssembler(metaclass=AutoTracedMeta):
|
|
|
561
764
|
for index, edge_id in enumerate(edge_ids):
|
|
562
765
|
offsets[edge_id] = index * step - offset_range / 2
|
|
563
766
|
return offsets
|
|
767
|
+
|
|
768
|
+
def _build_logic_config_for_subscription(self, agent, subscription, idx):
|
|
769
|
+
"""Build logic operations config for a subscription (Phase 1.2 + 1.2.1: with waiting_state)."""
|
|
770
|
+
if not subscription.join and not subscription.batch:
|
|
771
|
+
return None
|
|
772
|
+
|
|
773
|
+
config = {
|
|
774
|
+
"subscription_index": idx,
|
|
775
|
+
"subscription_types": list(subscription.type_names),
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
# JoinSpec configuration
|
|
779
|
+
if subscription.join:
|
|
780
|
+
join_spec = subscription.join
|
|
781
|
+
window_type = "time" if isinstance(join_spec.within, timedelta) else "count"
|
|
782
|
+
window_value = (
|
|
783
|
+
int(join_spec.within.total_seconds())
|
|
784
|
+
if isinstance(join_spec.within, timedelta)
|
|
785
|
+
else join_spec.within
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
config["join"] = {
|
|
789
|
+
"correlation_strategy": "by_key",
|
|
790
|
+
"window_type": window_type,
|
|
791
|
+
"window_value": window_value,
|
|
792
|
+
"window_unit": "seconds" if window_type == "time" else "artifacts",
|
|
793
|
+
"required_types": list(subscription.type_names),
|
|
794
|
+
"type_counts": dict(subscription.type_counts),
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
# Phase 1.2.1: Get waiting state from CorrelationEngine
|
|
798
|
+
from flock.dashboard.service import _get_correlation_groups
|
|
799
|
+
correlation_groups = _get_correlation_groups(
|
|
800
|
+
self._orchestrator._correlation_engine, agent.name, idx
|
|
801
|
+
)
|
|
802
|
+
if correlation_groups:
|
|
803
|
+
config["waiting_state"] = {
|
|
804
|
+
"is_waiting": True,
|
|
805
|
+
"correlation_groups": correlation_groups,
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# BatchSpec configuration
|
|
809
|
+
if subscription.batch:
|
|
810
|
+
batch_spec = subscription.batch
|
|
811
|
+
strategy = (
|
|
812
|
+
"hybrid"
|
|
813
|
+
if batch_spec.size and batch_spec.timeout
|
|
814
|
+
else "size"
|
|
815
|
+
if batch_spec.size
|
|
816
|
+
else "timeout"
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
config["batch"] = {
|
|
820
|
+
"strategy": strategy,
|
|
821
|
+
}
|
|
822
|
+
if batch_spec.size:
|
|
823
|
+
config["batch"]["size"] = batch_spec.size
|
|
824
|
+
if batch_spec.timeout:
|
|
825
|
+
config["batch"]["timeout_seconds"] = int(batch_spec.timeout.total_seconds())
|
|
826
|
+
|
|
827
|
+
# Phase 1.2.1: Get waiting state from BatchEngine
|
|
828
|
+
from flock.dashboard.service import _get_batch_state
|
|
829
|
+
batch_state = _get_batch_state(self._orchestrator._batch_engine, agent.name, idx, batch_spec)
|
|
830
|
+
if batch_state:
|
|
831
|
+
if "waiting_state" not in config:
|
|
832
|
+
config["waiting_state"] = {"is_waiting": True}
|
|
833
|
+
config["waiting_state"]["batch_state"] = batch_state
|
|
834
|
+
|
|
835
|
+
return config
|
flock/dashboard/models/graph.py
CHANGED
|
@@ -79,7 +79,9 @@ class GraphEdge(BaseModel):
|
|
|
79
79
|
id: str
|
|
80
80
|
source: str
|
|
81
81
|
target: str
|
|
82
|
-
type: Literal[
|
|
82
|
+
type: Literal[
|
|
83
|
+
"message_flow", "transformation", "pending_join", "pending_batch"
|
|
84
|
+
] # Phase 1.5: Added pending edge types
|
|
83
85
|
label: str | None = None
|
|
84
86
|
data: dict[str, Any] = Field(default_factory=dict)
|
|
85
87
|
marker_end: GraphMarker | None = Field(default=None, alias="markerEnd")
|