flock-core 0.5.6__py3-none-any.whl → 0.5.8__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 +344 -81
- flock/artifacts.py +2 -2
- flock/components.py +38 -30
- flock/correlation_engine.py +3 -6
- flock/dashboard/collector.py +9 -9
- flock/dashboard/events.py +8 -8
- flock/dashboard/service.py +7 -7
- flock/engines/dspy_engine.py +560 -64
- flock/engines/examples/simple_batch_engine.py +36 -20
- flock/examples.py +2 -2
- flock/helper/cli_helper.py +2 -2
- flock/logging/formatters/themed_formatter.py +3 -1
- flock/mcp/config.py +1 -2
- flock/mcp/tool.py +1 -2
- flock/orchestrator.py +2 -2
- flock/store.py +2 -2
- flock/utilities.py +1 -1
- flock/visibility.py +3 -3
- {flock_core-0.5.6.dist-info → flock_core-0.5.8.dist-info}/METADATA +99 -4
- {flock_core-0.5.6.dist-info → flock_core-0.5.8.dist-info}/RECORD +23 -23
- {flock_core-0.5.6.dist-info → flock_core-0.5.8.dist-info}/WHEEL +0 -0
- {flock_core-0.5.6.dist-info → flock_core-0.5.8.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.6.dist-info → flock_core-0.5.8.dist-info}/licenses/LICENSE +0 -0
flock/agent.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import os
|
|
7
|
-
from collections.abc import Sequence
|
|
7
|
+
from collections.abc import Callable, Sequence
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import TYPE_CHECKING, Any, TypedDict
|
|
10
10
|
|
|
@@ -64,6 +64,21 @@ class MCPServerConfig(TypedDict, total=False):
|
|
|
64
64
|
class AgentOutput:
|
|
65
65
|
spec: ArtifactSpec
|
|
66
66
|
default_visibility: Visibility
|
|
67
|
+
count: int = 1 # Number of artifacts to generate (fan-out)
|
|
68
|
+
filter_predicate: Callable[[BaseModel], bool] | None = None # Where clause
|
|
69
|
+
validate_predicate: Callable[[BaseModel], bool] | list[tuple[Callable, str]] | None = (
|
|
70
|
+
None # Validation logic
|
|
71
|
+
)
|
|
72
|
+
group_description: str | None = None # Group description override
|
|
73
|
+
|
|
74
|
+
def __post_init__(self):
|
|
75
|
+
"""Validate field constraints."""
|
|
76
|
+
if self.count < 1:
|
|
77
|
+
raise ValueError(f"count must be >= 1, got {self.count}")
|
|
78
|
+
|
|
79
|
+
def is_many(self) -> bool:
|
|
80
|
+
"""Return True if this output generates multiple artifacts (count > 1)."""
|
|
81
|
+
return self.count > 1
|
|
67
82
|
|
|
68
83
|
def apply(
|
|
69
84
|
self,
|
|
@@ -85,6 +100,27 @@ class AgentOutput:
|
|
|
85
100
|
)
|
|
86
101
|
|
|
87
102
|
|
|
103
|
+
@dataclass
|
|
104
|
+
class OutputGroup:
|
|
105
|
+
"""Represents one .publishes() call.
|
|
106
|
+
|
|
107
|
+
Each OutputGroup triggers one engine execution that generates
|
|
108
|
+
all artifacts in the group together.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
outputs: list[AgentOutput]
|
|
112
|
+
shared_visibility: Visibility | None = None
|
|
113
|
+
group_description: str | None = None # Group-level description override
|
|
114
|
+
|
|
115
|
+
def is_single_call(self) -> bool:
|
|
116
|
+
"""True if this is one engine call generating multiple artifacts.
|
|
117
|
+
|
|
118
|
+
Currently always returns True as each group = one engine call.
|
|
119
|
+
Future: Could return False for parallel sub-groups.
|
|
120
|
+
"""
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
88
124
|
class Agent(metaclass=AutoTracedMeta):
|
|
89
125
|
"""Executable agent constructed via `AgentBuilder`.
|
|
90
126
|
|
|
@@ -96,7 +132,7 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
96
132
|
self.description: str | None = None
|
|
97
133
|
self._orchestrator = orchestrator
|
|
98
134
|
self.subscriptions: list[Subscription] = []
|
|
99
|
-
self.
|
|
135
|
+
self.output_groups: list[OutputGroup] = []
|
|
100
136
|
self.utilities: list[AgentComponent] = []
|
|
101
137
|
self.engines: list[EngineComponent] = []
|
|
102
138
|
self.best_of_n: int = 1
|
|
@@ -115,6 +151,11 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
115
151
|
self.mcp_server_mounts: dict[str, list[str]] = {} # Server-specific mount points
|
|
116
152
|
self.tool_whitelist: list[str] | None = None
|
|
117
153
|
|
|
154
|
+
@property
|
|
155
|
+
def outputs(self) -> list[AgentOutput]:
|
|
156
|
+
"""Backwards compatibility: return flat list of all outputs from all groups."""
|
|
157
|
+
return [output for group in self.output_groups for output in group.outputs]
|
|
158
|
+
|
|
118
159
|
@property
|
|
119
160
|
def identity(self) -> AgentIdentity:
|
|
120
161
|
return AgentIdentity(name=self.name, labels=self.labels, tenant_id=self.tenant_id)
|
|
@@ -160,9 +201,40 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
160
201
|
processed_inputs = await self._run_pre_consume(ctx, artifacts)
|
|
161
202
|
eval_inputs = EvalInputs(artifacts=processed_inputs, state=dict(ctx.state))
|
|
162
203
|
eval_inputs = await self._run_pre_evaluate(ctx, eval_inputs)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
204
|
+
|
|
205
|
+
# Phase 3: Call engine ONCE PER OutputGroup
|
|
206
|
+
all_outputs: list[Artifact] = []
|
|
207
|
+
|
|
208
|
+
if not self.output_groups:
|
|
209
|
+
# No output groups: Utility agents that don't publish
|
|
210
|
+
# Create empty OutputGroup for engines that may have side effects
|
|
211
|
+
empty_group = OutputGroup(outputs=[], group_description=None)
|
|
212
|
+
result = await self._run_engines(ctx, eval_inputs, empty_group)
|
|
213
|
+
# Run post_evaluate hooks for utility components (e.g., metrics)
|
|
214
|
+
result = await self._run_post_evaluate(ctx, eval_inputs, result)
|
|
215
|
+
# Utility agents return empty list (no outputs declared)
|
|
216
|
+
outputs = []
|
|
217
|
+
else:
|
|
218
|
+
# Loop over each output group
|
|
219
|
+
for group_idx, output_group in enumerate(self.output_groups):
|
|
220
|
+
# Prepare group-specific context
|
|
221
|
+
group_ctx = self._prepare_group_context(ctx, group_idx, output_group)
|
|
222
|
+
|
|
223
|
+
# Phase 7: Single evaluation path with auto-detection
|
|
224
|
+
# Engine's evaluate() auto-detects batch/fan-out from ctx and output_group
|
|
225
|
+
result = await self._run_engines(group_ctx, eval_inputs, output_group)
|
|
226
|
+
|
|
227
|
+
result = await self._run_post_evaluate(group_ctx, eval_inputs, result)
|
|
228
|
+
|
|
229
|
+
# Extract outputs for THIS group only
|
|
230
|
+
group_outputs = await self._make_outputs_for_group(
|
|
231
|
+
group_ctx, result, output_group
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
all_outputs.extend(group_outputs)
|
|
235
|
+
|
|
236
|
+
outputs = all_outputs
|
|
237
|
+
|
|
166
238
|
await self._run_post_publish(ctx, outputs)
|
|
167
239
|
if self.calls_func:
|
|
168
240
|
await self._invoke_call(ctx, outputs or processed_inputs)
|
|
@@ -302,7 +374,19 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
302
374
|
raise
|
|
303
375
|
return current
|
|
304
376
|
|
|
305
|
-
async def _run_engines(
|
|
377
|
+
async def _run_engines(
|
|
378
|
+
self, ctx: Context, inputs: EvalInputs, output_group: OutputGroup
|
|
379
|
+
) -> EvalResult:
|
|
380
|
+
"""Execute engines for a specific OutputGroup.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
ctx: Execution context
|
|
384
|
+
inputs: EvalInputs with input artifacts
|
|
385
|
+
output_group: The OutputGroup defining what artifacts to produce
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
EvalResult with artifacts matching output_group specifications
|
|
389
|
+
"""
|
|
306
390
|
engines = self._resolve_engines()
|
|
307
391
|
if not engines:
|
|
308
392
|
return EvalResult(artifacts=inputs.artifacts, state=inputs.state)
|
|
@@ -313,26 +397,10 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
313
397
|
accumulated_metrics: dict[str, float] = {}
|
|
314
398
|
for engine in engines:
|
|
315
399
|
current_inputs = await engine.on_pre_evaluate(self, ctx, current_inputs)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
"Agent %s: routing %d artifacts to %s.evaluate_batch",
|
|
321
|
-
self.name,
|
|
322
|
-
len(current_inputs.artifacts),
|
|
323
|
-
engine.__class__.__name__,
|
|
324
|
-
)
|
|
325
|
-
result = await engine.evaluate_batch(self, ctx, current_inputs)
|
|
326
|
-
else:
|
|
327
|
-
result = await engine.evaluate(self, ctx, current_inputs)
|
|
328
|
-
except NotImplementedError:
|
|
329
|
-
if use_batch_mode:
|
|
330
|
-
logger.exception(
|
|
331
|
-
"Agent %s: engine %s does not implement evaluate_batch()",
|
|
332
|
-
self.name,
|
|
333
|
-
engine.__class__.__name__,
|
|
334
|
-
)
|
|
335
|
-
raise
|
|
400
|
+
|
|
401
|
+
# Phase 7: Single evaluation path with auto-detection
|
|
402
|
+
# Engine's evaluate() auto-detects batching via ctx.is_batch
|
|
403
|
+
result = await engine.evaluate(self, ctx, current_inputs, output_group)
|
|
336
404
|
|
|
337
405
|
# AUTO-WRAP: If engine returns BaseModel instead of EvalResult, wrap it
|
|
338
406
|
from flock.runtime import EvalResult as ER
|
|
@@ -392,29 +460,177 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
392
460
|
return current
|
|
393
461
|
|
|
394
462
|
async def _make_outputs(self, ctx: Context, result: EvalResult) -> list[Artifact]:
|
|
395
|
-
if not self.
|
|
463
|
+
if not self.output_groups:
|
|
396
464
|
# Utility agents may not publish anything
|
|
397
465
|
return list(result.artifacts)
|
|
398
466
|
|
|
399
467
|
produced: list[Artifact] = []
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
468
|
+
|
|
469
|
+
# For Phase 2: Iterate ALL output_groups (even though we only have 1 engine call)
|
|
470
|
+
# Phase 3 will modify this to call engine once PER group
|
|
471
|
+
for output_group in self.output_groups:
|
|
472
|
+
for output_decl in output_group.outputs:
|
|
473
|
+
# Phase 6: Find the matching artifact from engine result to preserve its ID
|
|
474
|
+
matching_artifact = self._find_matching_artifact(output_decl, result)
|
|
475
|
+
|
|
476
|
+
payload = self._select_payload(output_decl, result)
|
|
477
|
+
if payload is None:
|
|
478
|
+
continue
|
|
479
|
+
metadata = {
|
|
480
|
+
"correlation_id": ctx.correlation_id,
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# Phase 6: Preserve artifact ID from engine (for streaming message preview)
|
|
484
|
+
if matching_artifact:
|
|
485
|
+
metadata["artifact_id"] = matching_artifact.id
|
|
486
|
+
|
|
487
|
+
artifact = output_decl.apply(payload, produced_by=self.name, metadata=metadata)
|
|
488
|
+
produced.append(artifact)
|
|
489
|
+
await ctx.board.publish(artifact)
|
|
490
|
+
|
|
491
|
+
return produced
|
|
492
|
+
|
|
493
|
+
def _prepare_group_context(
|
|
494
|
+
self, ctx: Context, group_idx: int, output_group: OutputGroup
|
|
495
|
+
) -> Context:
|
|
496
|
+
"""Phase 3: Prepare context specific to this OutputGroup.
|
|
497
|
+
|
|
498
|
+
Creates a modified context for this group's engine call, potentially
|
|
499
|
+
with group-specific instructions or metadata.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
ctx: Base context
|
|
503
|
+
group_idx: Index of this group (0-based)
|
|
504
|
+
output_group: The OutputGroup being processed
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Context for this group (may be the same instance or modified)
|
|
508
|
+
"""
|
|
509
|
+
# For now, return the same context
|
|
510
|
+
# Phase 4 will add group-specific system prompts here
|
|
511
|
+
# Future: ctx.clone() and add group_description to system prompt
|
|
512
|
+
return ctx
|
|
513
|
+
|
|
514
|
+
async def _make_outputs_for_group(
|
|
515
|
+
self, ctx: Context, result: EvalResult, output_group: OutputGroup
|
|
516
|
+
) -> list[Artifact]:
|
|
517
|
+
"""Phase 3/5: Validate, filter, and publish artifacts for specific OutputGroup.
|
|
518
|
+
|
|
519
|
+
This function:
|
|
520
|
+
1. Validates that the engine fulfilled its contract (produced expected count)
|
|
521
|
+
2. Applies WHERE filtering (reduces artifacts, no error)
|
|
522
|
+
3. Applies VALIDATE checks (raises ValueError if validation fails)
|
|
523
|
+
4. Applies visibility (static or dynamic)
|
|
524
|
+
5. Publishes artifacts to the board
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
ctx: Context for this group
|
|
528
|
+
result: EvalResult from engine for THIS group
|
|
529
|
+
output_group: OutputGroup defining expected outputs
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
List of artifacts matching this group's outputs
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
ValueError: If engine violated contract or validation failed
|
|
536
|
+
"""
|
|
537
|
+
produced: list[Artifact] = []
|
|
538
|
+
|
|
539
|
+
for output_decl in output_group.outputs:
|
|
540
|
+
# 1. Find ALL matching artifacts for this type
|
|
541
|
+
from flock.registry import type_registry
|
|
542
|
+
|
|
543
|
+
expected_canonical = type_registry.resolve_name(output_decl.spec.type_name)
|
|
544
|
+
|
|
545
|
+
matching_artifacts: list[Artifact] = []
|
|
546
|
+
for artifact in result.artifacts:
|
|
547
|
+
try:
|
|
548
|
+
artifact_canonical = type_registry.resolve_name(artifact.type)
|
|
549
|
+
if artifact_canonical == expected_canonical:
|
|
550
|
+
matching_artifacts.append(artifact)
|
|
551
|
+
except Exception:
|
|
552
|
+
if artifact.type == output_decl.spec.type_name:
|
|
553
|
+
matching_artifacts.append(artifact)
|
|
554
|
+
|
|
555
|
+
# 2. STRICT VALIDATION: Engine must produce exactly what was promised
|
|
556
|
+
# (This happens BEFORE filtering so engine contract is validated first)
|
|
557
|
+
expected_count = output_decl.count
|
|
558
|
+
actual_count = len(matching_artifacts)
|
|
559
|
+
|
|
560
|
+
if actual_count != expected_count:
|
|
561
|
+
raise ValueError(
|
|
562
|
+
f"Engine contract violation in agent '{self.name}': "
|
|
563
|
+
f"Expected {expected_count} artifact(s) of type '{output_decl.spec.type_name}', "
|
|
564
|
+
f"but engine produced {actual_count}. "
|
|
565
|
+
f"Check your engine implementation to ensure it generates the correct number of outputs."
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# 3. Apply WHERE filtering (Phase 5)
|
|
569
|
+
# Filtering reduces the number of published artifacts (this is intentional)
|
|
570
|
+
# NOTE: Predicates expect Pydantic model instances, not dicts
|
|
571
|
+
model_cls = type_registry.resolve(output_decl.spec.type_name)
|
|
572
|
+
|
|
573
|
+
if output_decl.filter_predicate:
|
|
574
|
+
original_count = len(matching_artifacts)
|
|
575
|
+
filtered = []
|
|
576
|
+
for a in matching_artifacts:
|
|
577
|
+
# Reconstruct Pydantic model from payload dict
|
|
578
|
+
model_instance = model_cls(**a.payload)
|
|
579
|
+
if output_decl.filter_predicate(model_instance):
|
|
580
|
+
filtered.append(a)
|
|
581
|
+
matching_artifacts = filtered
|
|
582
|
+
logger.debug(
|
|
583
|
+
f"Agent {self.name}: WHERE filter reduced artifacts from "
|
|
584
|
+
f"{original_count} to {len(matching_artifacts)} for type {output_decl.spec.type_name}"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# 4. Apply VALIDATE checks (Phase 5)
|
|
588
|
+
# Validation failures raise errors (fail-fast)
|
|
589
|
+
if output_decl.validate_predicate:
|
|
590
|
+
if callable(output_decl.validate_predicate):
|
|
591
|
+
# Single predicate
|
|
592
|
+
for artifact in matching_artifacts:
|
|
593
|
+
# Reconstruct Pydantic model from payload dict
|
|
594
|
+
model_instance = model_cls(**artifact.payload)
|
|
595
|
+
if not output_decl.validate_predicate(model_instance):
|
|
596
|
+
raise ValueError(
|
|
597
|
+
f"Validation failed for {output_decl.spec.type_name} "
|
|
598
|
+
f"in agent '{self.name}'"
|
|
599
|
+
)
|
|
600
|
+
elif isinstance(output_decl.validate_predicate, list):
|
|
601
|
+
# List of (callable, error_msg) tuples
|
|
602
|
+
for artifact in matching_artifacts:
|
|
603
|
+
# Reconstruct Pydantic model from payload dict
|
|
604
|
+
model_instance = model_cls(**artifact.payload)
|
|
605
|
+
for check, error_msg in output_decl.validate_predicate:
|
|
606
|
+
if not check(model_instance):
|
|
607
|
+
raise ValueError(f"{error_msg}: {output_decl.spec.type_name}")
|
|
608
|
+
|
|
609
|
+
# 5. Apply visibility and publish artifacts (Phase 5)
|
|
610
|
+
for artifact_from_engine in matching_artifacts:
|
|
611
|
+
metadata = {
|
|
612
|
+
"correlation_id": ctx.correlation_id,
|
|
613
|
+
"artifact_id": artifact_from_engine.id, # Preserve engine's ID
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
# Determine visibility (static or dynamic)
|
|
617
|
+
visibility = output_decl.default_visibility
|
|
618
|
+
if callable(visibility):
|
|
619
|
+
# Dynamic visibility based on artifact content
|
|
620
|
+
# Reconstruct Pydantic model from payload dict
|
|
621
|
+
model_instance = model_cls(**artifact_from_engine.payload)
|
|
622
|
+
visibility = visibility(model_instance)
|
|
623
|
+
|
|
624
|
+
# Override metadata visibility
|
|
625
|
+
metadata["visibility"] = visibility
|
|
626
|
+
|
|
627
|
+
# Re-wrap the artifact with agent metadata
|
|
628
|
+
artifact = output_decl.apply(
|
|
629
|
+
artifact_from_engine.payload, produced_by=self.name, metadata=metadata
|
|
630
|
+
)
|
|
631
|
+
produced.append(artifact)
|
|
632
|
+
await ctx.board.publish(artifact)
|
|
633
|
+
|
|
418
634
|
return produced
|
|
419
635
|
|
|
420
636
|
async def _run_post_publish(self, ctx: Context, artifacts: Sequence[Artifact]) -> None:
|
|
@@ -453,9 +669,16 @@ class Agent(metaclass=AutoTracedMeta):
|
|
|
453
669
|
for component in self._sorted_utilities():
|
|
454
670
|
comp_name = self._component_display_name(component)
|
|
455
671
|
priority = getattr(component, "priority", 0)
|
|
672
|
+
|
|
673
|
+
# Python 3.12+ TaskGroup raises BaseExceptionGroup - extract sub-exceptions
|
|
674
|
+
error_detail = str(error)
|
|
675
|
+
if isinstance(error, BaseExceptionGroup):
|
|
676
|
+
sub_exceptions = [f"{type(e).__name__}: {e}" for e in error.exceptions]
|
|
677
|
+
error_detail = f"{error!s} - Sub-exceptions: {sub_exceptions}"
|
|
678
|
+
|
|
456
679
|
logger.debug(
|
|
457
680
|
f"Agent error hook: agent={self.name}, component={comp_name}, "
|
|
458
|
-
f"priority={priority}, error={
|
|
681
|
+
f"priority={priority}, error={error_detail}"
|
|
459
682
|
)
|
|
460
683
|
try:
|
|
461
684
|
await component.on_error(self, ctx, error)
|
|
@@ -699,43 +922,34 @@ class AgentBuilder:
|
|
|
699
922
|
return self
|
|
700
923
|
|
|
701
924
|
def publishes(
|
|
702
|
-
self,
|
|
925
|
+
self,
|
|
926
|
+
*types: type[BaseModel],
|
|
927
|
+
visibility: Visibility | Callable[[BaseModel], Visibility] | None = None,
|
|
928
|
+
fan_out: int | None = None,
|
|
929
|
+
where: Callable[[BaseModel], bool] | None = None,
|
|
930
|
+
validate: Callable[[BaseModel], bool] | list[tuple[Callable, str]] | None = None,
|
|
931
|
+
description: str | None = None,
|
|
703
932
|
) -> PublishBuilder:
|
|
704
933
|
"""Declare which artifact types this agent produces.
|
|
705
934
|
|
|
706
|
-
Configures the output types and default visibility controls for artifacts
|
|
707
|
-
published by this agent. Can chain with .where() for conditional publishing.
|
|
708
|
-
|
|
709
935
|
Args:
|
|
710
936
|
*types: Artifact types (Pydantic models) to publish
|
|
711
|
-
visibility: Default visibility control
|
|
712
|
-
|
|
937
|
+
visibility: Default visibility control OR callable for dynamic visibility
|
|
938
|
+
fan_out: Number of artifacts to publish (applies to ALL types)
|
|
939
|
+
where: Filter predicate for output artifacts
|
|
940
|
+
validate: Validation predicate(s) - callable or list of (callable, error_msg) tuples
|
|
941
|
+
description: Group-level description override
|
|
713
942
|
|
|
714
943
|
Returns:
|
|
715
944
|
PublishBuilder for conditional publishing configuration
|
|
716
945
|
|
|
717
946
|
Examples:
|
|
718
|
-
>>> #
|
|
719
|
-
>>> agent.publishes(
|
|
720
|
-
|
|
721
|
-
>>> #
|
|
722
|
-
>>> agent.publishes(
|
|
723
|
-
|
|
724
|
-
>>> # Private outputs (only specific agents can see)
|
|
725
|
-
>>> agent.publishes(
|
|
726
|
-
... SecretData,
|
|
727
|
-
... visibility=PrivateVisibility(agents={"admin", "auditor"})
|
|
728
|
-
... )
|
|
729
|
-
|
|
730
|
-
>>> # Tenant-isolated outputs
|
|
731
|
-
>>> agent.publishes(
|
|
732
|
-
... Invoice,
|
|
733
|
-
... visibility=TenantVisibility()
|
|
734
|
-
... )
|
|
735
|
-
|
|
736
|
-
>>> # Conditional publishing with chaining
|
|
737
|
-
>>> (agent.publishes(Alert)
|
|
738
|
-
... .where(lambda result: result.severity == "critical"))
|
|
947
|
+
>>> agent.publishes(Report) # Publish 1 Report
|
|
948
|
+
>>> agent.publishes(Task, Task, Task) # Publish 3 Tasks (duplicate counting)
|
|
949
|
+
>>> agent.publishes(Task, fan_out=3) # Same as above (sugar syntax)
|
|
950
|
+
>>> agent.publishes(Task, where=lambda t: t.priority > 5) # With filtering
|
|
951
|
+
>>> agent.publishes(Report, validate=lambda r: r.score > 0) # With validation
|
|
952
|
+
>>> agent.publishes(Task, description="Special instructions") # With description
|
|
739
953
|
|
|
740
954
|
See Also:
|
|
741
955
|
- PublicVisibility: Default, visible to all agents
|
|
@@ -743,14 +957,59 @@ class AgentBuilder:
|
|
|
743
957
|
- TenantVisibility: Multi-tenant isolation
|
|
744
958
|
- LabelledVisibility: Role-based access control
|
|
745
959
|
"""
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
960
|
+
# Validate fan_out if provided
|
|
961
|
+
if fan_out is not None and fan_out < 1:
|
|
962
|
+
raise ValueError(f"fan_out must be >= 1, got {fan_out}")
|
|
963
|
+
|
|
964
|
+
# Resolve visibility
|
|
965
|
+
resolved_visibility = (
|
|
966
|
+
ensure_visibility(visibility) if not callable(visibility) else visibility
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
# Create AgentOutput objects for this group
|
|
970
|
+
outputs: list[AgentOutput] = []
|
|
971
|
+
|
|
972
|
+
if fan_out is not None:
|
|
973
|
+
# Apply fan_out to ALL types
|
|
974
|
+
for model in types:
|
|
975
|
+
spec = ArtifactSpec.from_model(model)
|
|
976
|
+
output = AgentOutput(
|
|
977
|
+
spec=spec,
|
|
978
|
+
default_visibility=resolved_visibility,
|
|
979
|
+
count=fan_out,
|
|
980
|
+
filter_predicate=where,
|
|
981
|
+
validate_predicate=validate,
|
|
982
|
+
group_description=description,
|
|
983
|
+
)
|
|
984
|
+
outputs.append(output)
|
|
985
|
+
else:
|
|
986
|
+
# Create separate AgentOutput for each type (including duplicates)
|
|
987
|
+
# This preserves order: .publishes(A, B, A) → [A, B, A] (3 outputs)
|
|
988
|
+
for model in types:
|
|
989
|
+
spec = ArtifactSpec.from_model(model)
|
|
990
|
+
output = AgentOutput(
|
|
991
|
+
spec=spec,
|
|
992
|
+
default_visibility=resolved_visibility,
|
|
993
|
+
count=1,
|
|
994
|
+
filter_predicate=where,
|
|
995
|
+
validate_predicate=validate,
|
|
996
|
+
group_description=description,
|
|
997
|
+
)
|
|
998
|
+
outputs.append(output)
|
|
999
|
+
|
|
1000
|
+
# Create OutputGroup from outputs
|
|
1001
|
+
group = OutputGroup(
|
|
1002
|
+
outputs=outputs,
|
|
1003
|
+
shared_visibility=resolved_visibility if not callable(resolved_visibility) else None,
|
|
1004
|
+
group_description=description,
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
# Append to agent's output_groups
|
|
1008
|
+
self._agent.output_groups.append(group)
|
|
1009
|
+
|
|
1010
|
+
# Validate configuration
|
|
753
1011
|
self._validate_self_trigger_risk()
|
|
1012
|
+
|
|
754
1013
|
return PublishBuilder(self, outputs)
|
|
755
1014
|
|
|
756
1015
|
def with_utilities(self, *components: AgentComponent) -> AgentBuilder:
|
|
@@ -1081,7 +1340,9 @@ class AgentBuilder:
|
|
|
1081
1340
|
consuming_types.update(sub.type_names)
|
|
1082
1341
|
|
|
1083
1342
|
# Get types agent publishes
|
|
1084
|
-
publishing_types = {
|
|
1343
|
+
publishing_types = {
|
|
1344
|
+
output.spec.type_name for group in self._agent.output_groups for output in group.outputs
|
|
1345
|
+
}
|
|
1085
1346
|
|
|
1086
1347
|
# Check for overlap
|
|
1087
1348
|
overlap = consuming_types.intersection(publishing_types)
|
|
@@ -1225,4 +1486,6 @@ class Pipeline:
|
|
|
1225
1486
|
__all__ = [
|
|
1226
1487
|
"Agent",
|
|
1227
1488
|
"AgentBuilder",
|
|
1489
|
+
"AgentOutput",
|
|
1490
|
+
"OutputGroup",
|
|
1228
1491
|
]
|
flock/artifacts.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from typing import Any
|
|
7
7
|
from uuid import UUID, uuid4
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ class Artifact(BaseModel):
|
|
|
23
23
|
partition_key: str | None = None
|
|
24
24
|
tags: set[str] = Field(default_factory=set)
|
|
25
25
|
visibility: Visibility = Field(default_factory=lambda: ensure_visibility(None))
|
|
26
|
-
created_at: datetime = Field(default_factory=lambda: datetime.now(
|
|
26
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
27
27
|
version: int = 1
|
|
28
28
|
|
|
29
29
|
def model_dump_payload(self) -> dict[str, Any]: # pragma: no cover - convenience
|
flock/components.py
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
6
6
|
|
|
7
7
|
from pydantic import BaseModel, Field, create_model
|
|
8
8
|
from pydantic._internal._model_construction import ModelMetaclass
|
|
9
|
-
from typing_extensions import
|
|
9
|
+
from typing_extensions import TypeVar
|
|
10
10
|
|
|
11
11
|
from flock.logging.auto_trace import AutoTracedMeta
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ from flock.logging.auto_trace import AutoTracedMeta
|
|
|
14
14
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
15
15
|
from uuid import UUID
|
|
16
16
|
|
|
17
|
-
from flock.agent import Agent
|
|
17
|
+
from flock.agent import Agent, OutputGroup
|
|
18
18
|
from flock.artifacts import Artifact
|
|
19
19
|
from flock.runtime import Context, EvalInputs, EvalResult
|
|
20
20
|
|
|
@@ -109,41 +109,49 @@ class EngineComponent(AgentComponent):
|
|
|
109
109
|
default_factory=set, description="Artifact types to exclude from context"
|
|
110
110
|
)
|
|
111
111
|
|
|
112
|
-
async def evaluate(
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
async def evaluate(
|
|
113
|
+
self, agent: Agent, ctx: Context, inputs: EvalInputs, output_group: OutputGroup
|
|
114
|
+
) -> EvalResult:
|
|
115
|
+
"""Universal evaluation method with auto-detection of batch and fan-out modes.
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
This single method handles ALL evaluation scenarios:
|
|
118
|
+
- Single artifact → single output
|
|
119
|
+
- Batch processing (ctx.is_batch=True) → list[Type] signatures
|
|
120
|
+
- Fan-out (output_group.outputs[*].count > 1) → multiple artifacts
|
|
121
|
+
- Multi-output (len(output_group.outputs) > 1) → multiple types
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
Auto-detection happens automatically:
|
|
124
|
+
- Batching: Detected via ctx.is_batch flag
|
|
125
|
+
- Fan-out: Detected via output_group.outputs[*].count
|
|
126
|
+
- Multi-input: Detected via len(inputs.artifacts)
|
|
127
|
+
- Multi-output: Detected via len(output_group.outputs)
|
|
120
128
|
|
|
121
129
|
Args:
|
|
122
130
|
agent: Agent instance executing this engine
|
|
123
|
-
ctx: Execution context (ctx.is_batch
|
|
124
|
-
inputs: EvalInputs with
|
|
131
|
+
ctx: Execution context (check ctx.is_batch for batch mode)
|
|
132
|
+
inputs: EvalInputs with input artifacts
|
|
133
|
+
output_group: OutputGroup defining what artifacts to produce
|
|
134
|
+
(inspect outputs[*].count for fan-out detection)
|
|
125
135
|
|
|
126
136
|
Returns:
|
|
127
|
-
EvalResult with
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...
|
|
135
|
-
...
|
|
136
|
-
...
|
|
137
|
+
EvalResult with artifacts matching output_group specifications
|
|
138
|
+
|
|
139
|
+
Implementation Guide:
|
|
140
|
+
>>> async def evaluate(self, agent, ctx, inputs, output_group):
|
|
141
|
+
... # Auto-detect batching from context
|
|
142
|
+
... batched = bool(getattr(ctx, "is_batch", False))
|
|
143
|
+
...
|
|
144
|
+
... # Fan-out is auto-detected from output_group
|
|
145
|
+
... # Your signature building should check:
|
|
146
|
+
... # - output_group.outputs[i].count > 1 for fan-out
|
|
147
|
+
... # - len(output_group.outputs) > 1 for multi-output
|
|
148
|
+
...
|
|
149
|
+
... # Build signature adapting to all modes
|
|
150
|
+
... signature = self._build_signature(inputs, output_group, batched)
|
|
151
|
+
... result = await self._execute(signature, inputs)
|
|
152
|
+
... return EvalResult.from_objects(*result, agent=agent)
|
|
137
153
|
"""
|
|
138
|
-
raise NotImplementedError
|
|
139
|
-
f"{self.__class__.__name__} does not support batch processing.\n\n"
|
|
140
|
-
f"To fix this:\n"
|
|
141
|
-
f"1. Remove BatchSpec from agent subscription, OR\n"
|
|
142
|
-
f"2. Implement evaluate_batch() in {self.__class__.__name__}, OR\n"
|
|
143
|
-
f"3. Use a batch-aware engine (e.g., CustomBatchEngine)\n\n"
|
|
144
|
-
f"Agent: {agent.name}\n"
|
|
145
|
-
f"Engine: {self.__class__.__name__}"
|
|
146
|
-
)
|
|
154
|
+
raise NotImplementedError
|
|
147
155
|
|
|
148
156
|
async def fetch_conversation_context(
|
|
149
157
|
self,
|