flock-core 0.5.10__py3-none-any.whl → 0.5.20__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/__init__.py +1 -1
- flock/agent/__init__.py +30 -0
- flock/agent/builder_helpers.py +192 -0
- flock/agent/builder_validator.py +169 -0
- flock/agent/component_lifecycle.py +325 -0
- flock/agent/context_resolver.py +141 -0
- flock/agent/mcp_integration.py +212 -0
- flock/agent/output_processor.py +304 -0
- flock/api/__init__.py +20 -0
- flock/api/models.py +283 -0
- flock/{service.py → api/service.py} +121 -63
- flock/cli.py +2 -2
- flock/components/__init__.py +41 -0
- flock/components/agent/__init__.py +22 -0
- flock/{components.py → components/agent/base.py} +4 -3
- flock/{utility/output_utility_component.py → components/agent/output_utility.py} +12 -7
- flock/components/orchestrator/__init__.py +22 -0
- flock/{orchestrator_component.py → components/orchestrator/base.py} +5 -293
- flock/components/orchestrator/circuit_breaker.py +95 -0
- flock/components/orchestrator/collection.py +143 -0
- flock/components/orchestrator/deduplication.py +78 -0
- flock/core/__init__.py +30 -0
- flock/core/agent.py +953 -0
- flock/{artifacts.py → core/artifacts.py} +1 -1
- flock/{context_provider.py → core/context_provider.py} +3 -3
- flock/core/orchestrator.py +1102 -0
- flock/{store.py → core/store.py} +99 -454
- flock/{subscription.py → core/subscription.py} +1 -1
- flock/dashboard/collector.py +5 -5
- flock/dashboard/graph_builder.py +7 -7
- flock/dashboard/routes/__init__.py +21 -0
- flock/dashboard/routes/control.py +327 -0
- flock/dashboard/routes/helpers.py +340 -0
- flock/dashboard/routes/themes.py +76 -0
- flock/dashboard/routes/traces.py +521 -0
- flock/dashboard/routes/websocket.py +108 -0
- flock/dashboard/service.py +44 -1294
- flock/engines/dspy/__init__.py +20 -0
- flock/engines/dspy/artifact_materializer.py +216 -0
- flock/engines/dspy/signature_builder.py +474 -0
- flock/engines/dspy/streaming_executor.py +858 -0
- flock/engines/dspy_engine.py +45 -1330
- flock/engines/examples/simple_batch_engine.py +2 -2
- flock/examples.py +7 -7
- flock/logging/logging.py +1 -16
- flock/models/__init__.py +10 -0
- flock/models/system_artifacts.py +33 -0
- flock/orchestrator/__init__.py +45 -0
- flock/{artifact_collector.py → orchestrator/artifact_collector.py} +3 -3
- flock/orchestrator/artifact_manager.py +168 -0
- flock/{batch_accumulator.py → orchestrator/batch_accumulator.py} +2 -2
- flock/orchestrator/component_runner.py +389 -0
- flock/orchestrator/context_builder.py +167 -0
- flock/{correlation_engine.py → orchestrator/correlation_engine.py} +2 -2
- flock/orchestrator/event_emitter.py +167 -0
- flock/orchestrator/initialization.py +184 -0
- flock/orchestrator/lifecycle_manager.py +226 -0
- flock/orchestrator/mcp_manager.py +202 -0
- flock/orchestrator/scheduler.py +189 -0
- flock/orchestrator/server_manager.py +234 -0
- flock/orchestrator/tracing.py +147 -0
- flock/storage/__init__.py +10 -0
- flock/storage/artifact_aggregator.py +158 -0
- flock/storage/in_memory/__init__.py +6 -0
- flock/storage/in_memory/artifact_filter.py +114 -0
- flock/storage/in_memory/history_aggregator.py +115 -0
- flock/storage/sqlite/__init__.py +10 -0
- flock/storage/sqlite/agent_history_queries.py +154 -0
- flock/storage/sqlite/consumption_loader.py +100 -0
- flock/storage/sqlite/query_builder.py +112 -0
- flock/storage/sqlite/query_params_builder.py +91 -0
- flock/storage/sqlite/schema_manager.py +168 -0
- flock/storage/sqlite/summary_queries.py +194 -0
- flock/utils/__init__.py +14 -0
- flock/utils/async_utils.py +67 -0
- flock/{runtime.py → utils/runtime.py} +3 -3
- flock/utils/time_utils.py +53 -0
- flock/utils/type_resolution.py +38 -0
- flock/{utilities.py → utils/utilities.py} +2 -2
- flock/utils/validation.py +57 -0
- flock/utils/visibility.py +79 -0
- flock/utils/visibility_utils.py +134 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/METADATA +69 -61
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/RECORD +89 -31
- flock/agent.py +0 -1578
- flock/orchestrator.py +0 -1746
- /flock/{visibility.py → core/visibility.py} +0 -0
- /flock/{helper → utils}/cli_helper.py +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/WHEEL +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.10.dist-info → flock_core-0.5.20.dist-info}/licenses/LICENSE +0 -0
flock/core/agent.py
ADDED
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
"""Agent definitions and fluent builder APIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Callable, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, Any, TypedDict
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
# Phase 5B: Import builder modules
|
|
14
|
+
from flock.agent.builder_helpers import Pipeline, PublishBuilder, RunHandle
|
|
15
|
+
from flock.agent.builder_validator import BuilderValidator
|
|
16
|
+
from flock.agent.component_lifecycle import ComponentLifecycle
|
|
17
|
+
from flock.agent.mcp_integration import MCPIntegration
|
|
18
|
+
|
|
19
|
+
# Phase 4: Import extracted modules
|
|
20
|
+
from flock.agent.output_processor import OutputProcessor
|
|
21
|
+
from flock.core.artifacts import Artifact, ArtifactSpec
|
|
22
|
+
from flock.core.subscription import BatchSpec, JoinSpec, Subscription, TextPredicate
|
|
23
|
+
from flock.core.visibility import AgentIdentity, Visibility, ensure_visibility
|
|
24
|
+
from flock.logging.auto_trace import AutoTracedMeta
|
|
25
|
+
from flock.logging.logging import get_logger
|
|
26
|
+
from flock.registry import function_registry, type_registry
|
|
27
|
+
from flock.utils.runtime import Context, EvalInputs, EvalResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING: # pragma: no cover - type hints only
|
|
33
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
34
|
+
|
|
35
|
+
from flock.components.agent import AgentComponent, EngineComponent
|
|
36
|
+
from flock.core import Flock
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MCPServerConfig(TypedDict, total=False):
|
|
40
|
+
"""Configuration for MCP server assignment to an agent.
|
|
41
|
+
|
|
42
|
+
All fields are optional. If omitted, no restrictions apply.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
roots: Filesystem paths this server can access.
|
|
46
|
+
Empty list or omitted = no mount restrictions.
|
|
47
|
+
tool_whitelist: Tool names the agent can use from this server.
|
|
48
|
+
Empty list or omitted = all tools available.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> # No restrictions
|
|
52
|
+
>>> config: MCPServerConfig = {}
|
|
53
|
+
|
|
54
|
+
>>> # Mount restrictions only
|
|
55
|
+
>>> config: MCPServerConfig = {"roots": ["/workspace/data"]}
|
|
56
|
+
|
|
57
|
+
>>> # Tool whitelist only
|
|
58
|
+
>>> config: MCPServerConfig = {
|
|
59
|
+
... "tool_whitelist": ["read_file", "write_file"]
|
|
60
|
+
... }
|
|
61
|
+
|
|
62
|
+
>>> # Both restrictions
|
|
63
|
+
>>> config: MCPServerConfig = {
|
|
64
|
+
... "roots": ["/workspace/data"],
|
|
65
|
+
... "tool_whitelist": ["read_file"],
|
|
66
|
+
... }
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
roots: list[str]
|
|
70
|
+
tool_whitelist: list[str]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class AgentOutput:
|
|
75
|
+
spec: ArtifactSpec
|
|
76
|
+
default_visibility: Visibility
|
|
77
|
+
count: int = 1 # Number of artifacts to generate (fan-out)
|
|
78
|
+
filter_predicate: Callable[[BaseModel], bool] | None = None # Where clause
|
|
79
|
+
validate_predicate: (
|
|
80
|
+
Callable[[BaseModel], bool] | list[tuple[Callable, str]] | None
|
|
81
|
+
) = None # Validation logic
|
|
82
|
+
group_description: str | None = None # Group description override
|
|
83
|
+
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
"""Validate field constraints."""
|
|
86
|
+
if self.count < 1:
|
|
87
|
+
raise ValueError(f"count must be >= 1, got {self.count}")
|
|
88
|
+
|
|
89
|
+
def is_many(self) -> bool:
|
|
90
|
+
"""Return True if this output generates multiple artifacts (count > 1)."""
|
|
91
|
+
return self.count > 1
|
|
92
|
+
|
|
93
|
+
def apply(
|
|
94
|
+
self,
|
|
95
|
+
data: dict[str, Any],
|
|
96
|
+
*,
|
|
97
|
+
produced_by: str,
|
|
98
|
+
metadata: dict[str, Any] | None = None,
|
|
99
|
+
) -> Artifact:
|
|
100
|
+
metadata = metadata or {}
|
|
101
|
+
return self.spec.build(
|
|
102
|
+
produced_by=produced_by,
|
|
103
|
+
data=data,
|
|
104
|
+
visibility=metadata.get("visibility", self.default_visibility),
|
|
105
|
+
correlation_id=metadata.get("correlation_id"),
|
|
106
|
+
partition_key=metadata.get("partition_key"),
|
|
107
|
+
tags=metadata.get("tags"),
|
|
108
|
+
version=metadata.get("version", 1),
|
|
109
|
+
artifact_id=metadata.get("artifact_id"), # Phase 6: Preserve engine's ID
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class OutputGroup:
|
|
115
|
+
"""Represents one .publishes() call.
|
|
116
|
+
|
|
117
|
+
Each OutputGroup triggers one engine execution that generates
|
|
118
|
+
all artifacts in the group together.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
outputs: list[AgentOutput]
|
|
122
|
+
shared_visibility: Visibility | None = None
|
|
123
|
+
group_description: str | None = None # Group-level description override
|
|
124
|
+
|
|
125
|
+
def is_single_call(self) -> bool:
|
|
126
|
+
"""True if this is one engine call generating multiple artifacts.
|
|
127
|
+
|
|
128
|
+
Currently always returns True as each group = one engine call.
|
|
129
|
+
Future: Could return False for parallel sub-groups.
|
|
130
|
+
"""
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Agent(metaclass=AutoTracedMeta):
|
|
135
|
+
"""Executable agent constructed via `AgentBuilder`.
|
|
136
|
+
|
|
137
|
+
All public methods are automatically traced via OpenTelemetry.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
# Phase 6+7: Class-level streaming coordination (SHARED across ALL agent instances)
|
|
141
|
+
# These class variables enable all agents to coordinate CLI streaming behavior
|
|
142
|
+
_streaming_counter: int = 0 # Global count of agents currently streaming to CLI
|
|
143
|
+
_websocket_broadcast_global: Any = (
|
|
144
|
+
None # WebSocket broadcast wrapper (dashboard mode)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def __init__(self, name: str, *, orchestrator: Flock) -> None:
|
|
148
|
+
self.name = name
|
|
149
|
+
self.description: str | None = None
|
|
150
|
+
self._orchestrator = orchestrator
|
|
151
|
+
self.subscriptions: list[Subscription] = []
|
|
152
|
+
self.output_groups: list[OutputGroup] = []
|
|
153
|
+
self.utilities: list[AgentComponent] = []
|
|
154
|
+
self.engines: list[EngineComponent] = []
|
|
155
|
+
self.best_of_n: int = 1
|
|
156
|
+
self.best_of_score: Callable[[EvalResult], float] | None = None
|
|
157
|
+
self.max_concurrency: int = 2
|
|
158
|
+
self._semaphore = asyncio.Semaphore(self.max_concurrency)
|
|
159
|
+
self.calls_func: Callable[..., Any] | None = None
|
|
160
|
+
self.tools: set[Callable[..., Any]] = set()
|
|
161
|
+
self.labels: set[str] = set()
|
|
162
|
+
self.tenant_id: str | None = None
|
|
163
|
+
self.model: str | None = None
|
|
164
|
+
self.prevent_self_trigger: bool = True # T065: Prevent infinite feedback loops
|
|
165
|
+
# Phase 3: Per-agent context provider (security fix)
|
|
166
|
+
self.context_provider: Any = None
|
|
167
|
+
|
|
168
|
+
# Phase 4: Initialize extracted modules
|
|
169
|
+
self._output_processor = OutputProcessor(name)
|
|
170
|
+
self._mcp_integration = MCPIntegration(name, orchestrator)
|
|
171
|
+
self._component_lifecycle = ComponentLifecycle(name)
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def outputs(self) -> list[AgentOutput]:
|
|
175
|
+
"""Return flat list of all outputs from all groups."""
|
|
176
|
+
return [output for group in self.output_groups for output in group.outputs]
|
|
177
|
+
|
|
178
|
+
# Phase 4: MCP properties - delegate to MCPIntegration
|
|
179
|
+
@property
|
|
180
|
+
def mcp_server_names(self) -> set[str]:
|
|
181
|
+
"""MCP server names assigned to this agent."""
|
|
182
|
+
return self._mcp_integration.mcp_server_names
|
|
183
|
+
|
|
184
|
+
@mcp_server_names.setter
|
|
185
|
+
def mcp_server_names(self, value: set[str]) -> None:
|
|
186
|
+
self._mcp_integration.mcp_server_names = value
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def mcp_server_mounts(self) -> dict[str, list[str]]:
|
|
190
|
+
"""Server-specific mount points."""
|
|
191
|
+
return self._mcp_integration.mcp_server_mounts
|
|
192
|
+
|
|
193
|
+
@mcp_server_mounts.setter
|
|
194
|
+
def mcp_server_mounts(self, value: dict[str, list[str]]) -> None:
|
|
195
|
+
self._mcp_integration.mcp_server_mounts = value
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def tool_whitelist(self) -> list[str] | None:
|
|
199
|
+
"""Tool whitelist for MCP servers."""
|
|
200
|
+
return self._mcp_integration.tool_whitelist
|
|
201
|
+
|
|
202
|
+
@tool_whitelist.setter
|
|
203
|
+
def tool_whitelist(self, value: list[str] | None) -> None:
|
|
204
|
+
self._mcp_integration.tool_whitelist = value
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def identity(self) -> AgentIdentity:
|
|
208
|
+
return AgentIdentity(
|
|
209
|
+
name=self.name, labels=self.labels, tenant_id=self.tenant_id
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _component_display_name(component: AgentComponent) -> str:
|
|
214
|
+
return component.name or component.__class__.__name__
|
|
215
|
+
|
|
216
|
+
def _sorted_utilities(self) -> list[AgentComponent]:
|
|
217
|
+
if not self.utilities:
|
|
218
|
+
return []
|
|
219
|
+
return sorted(self.utilities, key=lambda comp: getattr(comp, "priority", 0))
|
|
220
|
+
|
|
221
|
+
def _add_utilities(self, components: Sequence[AgentComponent]) -> None:
|
|
222
|
+
if not components:
|
|
223
|
+
return
|
|
224
|
+
for component in components:
|
|
225
|
+
self.utilities.append(component)
|
|
226
|
+
comp_name = self._component_display_name(component)
|
|
227
|
+
priority = getattr(component, "priority", 0)
|
|
228
|
+
logger.info(
|
|
229
|
+
"Agent %s: utility added: component=%s, priority=%s, total_utilities=%s",
|
|
230
|
+
self.name,
|
|
231
|
+
comp_name,
|
|
232
|
+
priority,
|
|
233
|
+
len(self.utilities),
|
|
234
|
+
)
|
|
235
|
+
self.utilities.sort(key=lambda comp: getattr(comp, "priority", 0))
|
|
236
|
+
|
|
237
|
+
def set_max_concurrency(self, value: int) -> None:
|
|
238
|
+
self.max_concurrency = max(1, value)
|
|
239
|
+
self._semaphore = asyncio.Semaphore(self.max_concurrency)
|
|
240
|
+
|
|
241
|
+
async def run_direct(self, *inputs: BaseModel) -> list[Artifact]:
|
|
242
|
+
return await self._orchestrator.direct_invoke(self, list(inputs))
|
|
243
|
+
|
|
244
|
+
async def execute(self, ctx: Context, artifacts: list[Artifact]) -> list[Artifact]:
|
|
245
|
+
async with self._semaphore:
|
|
246
|
+
try:
|
|
247
|
+
self._resolve_engines()
|
|
248
|
+
self._resolve_utilities()
|
|
249
|
+
await self._run_initialize(ctx)
|
|
250
|
+
processed_inputs = await self._run_pre_consume(ctx, artifacts)
|
|
251
|
+
eval_inputs = EvalInputs(
|
|
252
|
+
artifacts=processed_inputs, state=dict(ctx.state)
|
|
253
|
+
)
|
|
254
|
+
eval_inputs = await self._run_pre_evaluate(ctx, eval_inputs)
|
|
255
|
+
|
|
256
|
+
# Phase 3: Call engine ONCE PER OutputGroup
|
|
257
|
+
all_outputs: list[Artifact] = []
|
|
258
|
+
|
|
259
|
+
if not self.output_groups:
|
|
260
|
+
# No output groups: Utility agents that don't publish
|
|
261
|
+
# Create empty OutputGroup for engines that may have side effects
|
|
262
|
+
empty_group = OutputGroup(outputs=[], group_description=None)
|
|
263
|
+
result = await self._run_engines(ctx, eval_inputs, empty_group)
|
|
264
|
+
# Run post_evaluate hooks for utility components (e.g., metrics)
|
|
265
|
+
result = await self._run_post_evaluate(ctx, eval_inputs, result)
|
|
266
|
+
# Utility agents return empty list (no outputs declared)
|
|
267
|
+
outputs = []
|
|
268
|
+
else:
|
|
269
|
+
# Loop over each output group
|
|
270
|
+
for group_idx, output_group in enumerate(self.output_groups):
|
|
271
|
+
# Prepare group-specific context
|
|
272
|
+
group_ctx = self._prepare_group_context(
|
|
273
|
+
ctx, group_idx, output_group
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Phase 7: Single evaluation path with auto-detection
|
|
277
|
+
# Engine's evaluate() auto-detects batch/fan-out from ctx and output_group
|
|
278
|
+
result = await self._run_engines(
|
|
279
|
+
group_ctx, eval_inputs, output_group
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
result = await self._run_post_evaluate(
|
|
283
|
+
group_ctx, eval_inputs, result
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Extract outputs for THIS group only
|
|
287
|
+
group_outputs = await self._make_outputs_for_group(
|
|
288
|
+
group_ctx, result, output_group
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
all_outputs.extend(group_outputs)
|
|
292
|
+
|
|
293
|
+
outputs = all_outputs
|
|
294
|
+
|
|
295
|
+
await self._run_post_publish(ctx, outputs)
|
|
296
|
+
if self.calls_func:
|
|
297
|
+
await self._invoke_call(ctx, outputs or processed_inputs)
|
|
298
|
+
return outputs
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
await self._run_error(ctx, exc)
|
|
301
|
+
raise
|
|
302
|
+
finally:
|
|
303
|
+
await self._run_terminate(ctx)
|
|
304
|
+
|
|
305
|
+
async def _get_mcp_tools(self, ctx: Context) -> list[Callable]:
|
|
306
|
+
"""Delegate to MCPIntegration module."""
|
|
307
|
+
return await self._mcp_integration.get_mcp_tools(ctx)
|
|
308
|
+
|
|
309
|
+
async def _run_initialize(self, ctx: Context) -> None:
|
|
310
|
+
"""Delegate to ComponentLifecycle module."""
|
|
311
|
+
await self._component_lifecycle.run_initialize(
|
|
312
|
+
self, ctx, self._sorted_utilities(), self.engines
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def _run_pre_consume(
|
|
316
|
+
self, ctx: Context, inputs: list[Artifact]
|
|
317
|
+
) -> list[Artifact]:
|
|
318
|
+
"""Delegate to ComponentLifecycle module."""
|
|
319
|
+
return await self._component_lifecycle.run_pre_consume(
|
|
320
|
+
self, ctx, inputs, self._sorted_utilities()
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async def _run_pre_evaluate(self, ctx: Context, inputs: EvalInputs) -> EvalInputs:
|
|
324
|
+
"""Delegate to ComponentLifecycle module."""
|
|
325
|
+
return await self._component_lifecycle.run_pre_evaluate(
|
|
326
|
+
self, ctx, inputs, self._sorted_utilities()
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def _run_engines(
|
|
330
|
+
self, ctx: Context, inputs: EvalInputs, output_group: OutputGroup
|
|
331
|
+
) -> EvalResult:
|
|
332
|
+
"""Execute engines for a specific OutputGroup.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
ctx: Execution context
|
|
336
|
+
inputs: EvalInputs with input artifacts
|
|
337
|
+
output_group: The OutputGroup defining what artifacts to produce
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
EvalResult with artifacts matching output_group specifications
|
|
341
|
+
"""
|
|
342
|
+
engines = self._resolve_engines()
|
|
343
|
+
if not engines:
|
|
344
|
+
return EvalResult(artifacts=inputs.artifacts, state=inputs.state)
|
|
345
|
+
|
|
346
|
+
async def run_chain() -> EvalResult:
|
|
347
|
+
current_inputs = inputs
|
|
348
|
+
accumulated_logs: list[str] = []
|
|
349
|
+
accumulated_metrics: dict[str, float] = {}
|
|
350
|
+
for engine in engines:
|
|
351
|
+
current_inputs = await engine.on_pre_evaluate(self, ctx, current_inputs)
|
|
352
|
+
|
|
353
|
+
# Phase 7: Single evaluation path with auto-detection
|
|
354
|
+
# Engine's evaluate() auto-detects batching via ctx.is_batch
|
|
355
|
+
result = await engine.evaluate(self, ctx, current_inputs, output_group)
|
|
356
|
+
|
|
357
|
+
# AUTO-WRAP: If engine returns BaseModel instead of EvalResult, wrap it
|
|
358
|
+
from flock.utils.runtime import EvalResult as ER
|
|
359
|
+
|
|
360
|
+
if isinstance(result, BaseModel) and not isinstance(result, ER):
|
|
361
|
+
result = ER.from_object(result, agent=self)
|
|
362
|
+
|
|
363
|
+
artifacts = result.artifacts
|
|
364
|
+
for artifact in artifacts:
|
|
365
|
+
artifact.correlation_id = ctx.correlation_id
|
|
366
|
+
|
|
367
|
+
result = await engine.on_post_evaluate(
|
|
368
|
+
self, ctx, current_inputs, result
|
|
369
|
+
)
|
|
370
|
+
accumulated_logs.extend(result.logs)
|
|
371
|
+
accumulated_metrics.update(result.metrics)
|
|
372
|
+
merged_state = dict(current_inputs.state)
|
|
373
|
+
merged_state.update(result.state)
|
|
374
|
+
current_inputs = EvalInputs(
|
|
375
|
+
artifacts=result.artifacts or current_inputs.artifacts,
|
|
376
|
+
state=merged_state,
|
|
377
|
+
)
|
|
378
|
+
return EvalResult(
|
|
379
|
+
artifacts=current_inputs.artifacts,
|
|
380
|
+
state=current_inputs.state,
|
|
381
|
+
metrics=accumulated_metrics,
|
|
382
|
+
logs=accumulated_logs,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if self.best_of_n <= 1:
|
|
386
|
+
return await run_chain()
|
|
387
|
+
|
|
388
|
+
async with asyncio.TaskGroup() as tg: # Python 3.12
|
|
389
|
+
tasks: list[asyncio.Task[EvalResult]] = []
|
|
390
|
+
for _ in range(self.best_of_n):
|
|
391
|
+
tasks.append(tg.create_task(run_chain()))
|
|
392
|
+
results = [task.result() for task in tasks]
|
|
393
|
+
if not results:
|
|
394
|
+
return EvalResult(artifacts=[], state={})
|
|
395
|
+
if self.best_of_score is None:
|
|
396
|
+
return results[0]
|
|
397
|
+
return max(results, key=self.best_of_score)
|
|
398
|
+
|
|
399
|
+
async def _run_post_evaluate(
|
|
400
|
+
self, ctx: Context, inputs: EvalInputs, result: EvalResult
|
|
401
|
+
) -> EvalResult:
|
|
402
|
+
"""Delegate to ComponentLifecycle module."""
|
|
403
|
+
return await self._component_lifecycle.run_post_evaluate(
|
|
404
|
+
self, ctx, inputs, result, self._sorted_utilities()
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
async def _make_outputs(self, ctx: Context, result: EvalResult) -> list[Artifact]:
|
|
408
|
+
"""Delegate to OutputProcessor module."""
|
|
409
|
+
return await self._output_processor.make_outputs(
|
|
410
|
+
ctx, result, self.output_groups
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def _prepare_group_context(
|
|
414
|
+
self, ctx: Context, group_idx: int, output_group: OutputGroup
|
|
415
|
+
) -> Context:
|
|
416
|
+
"""Delegate to OutputProcessor module."""
|
|
417
|
+
return self._output_processor.prepare_group_context(
|
|
418
|
+
ctx, group_idx, output_group
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
async def _make_outputs_for_group(
|
|
422
|
+
self, ctx: Context, result: EvalResult, output_group: OutputGroup
|
|
423
|
+
) -> list[Artifact]:
|
|
424
|
+
"""Delegate to OutputProcessor module."""
|
|
425
|
+
return await self._output_processor.make_outputs_for_group(
|
|
426
|
+
ctx, result, output_group
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
async def _run_post_publish(
|
|
430
|
+
self, ctx: Context, artifacts: Sequence[Artifact]
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Delegate to ComponentLifecycle module."""
|
|
433
|
+
await self._component_lifecycle.run_post_publish(
|
|
434
|
+
self, ctx, artifacts, self._sorted_utilities()
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
async def _invoke_call(self, ctx: Context, artifacts: Sequence[Artifact]) -> None:
|
|
438
|
+
func = self.calls_func
|
|
439
|
+
if func is None:
|
|
440
|
+
return
|
|
441
|
+
if not artifacts:
|
|
442
|
+
return
|
|
443
|
+
first = artifacts[0]
|
|
444
|
+
model_cls = type_registry.resolve(first.type)
|
|
445
|
+
payload = model_cls(**first.payload)
|
|
446
|
+
maybe_coro = func(payload)
|
|
447
|
+
if asyncio.iscoroutine(maybe_coro): # pragma: no cover - optional async support
|
|
448
|
+
await maybe_coro
|
|
449
|
+
|
|
450
|
+
async def _run_error(self, ctx: Context, error: Exception) -> None:
|
|
451
|
+
"""Delegate to ComponentLifecycle module."""
|
|
452
|
+
await self._component_lifecycle.run_error(
|
|
453
|
+
self, ctx, error, self._sorted_utilities(), self.engines
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
async def _run_terminate(self, ctx: Context) -> None:
|
|
457
|
+
"""Delegate to ComponentLifecycle module."""
|
|
458
|
+
await self._component_lifecycle.run_terminate(
|
|
459
|
+
self, ctx, self._sorted_utilities(), self.engines
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def _resolve_engines(self) -> list[EngineComponent]:
|
|
463
|
+
if self.engines:
|
|
464
|
+
return self.engines
|
|
465
|
+
try:
|
|
466
|
+
from flock.engines import DSPyEngine
|
|
467
|
+
except Exception: # pragma: no cover - optional dependency issues
|
|
468
|
+
return []
|
|
469
|
+
|
|
470
|
+
default_engine = DSPyEngine(
|
|
471
|
+
model=self._orchestrator.model
|
|
472
|
+
or os.getenv("DEFAULT_MODEL", "openai/gpt-4.1"),
|
|
473
|
+
instructions=self.description,
|
|
474
|
+
)
|
|
475
|
+
self.engines = [default_engine]
|
|
476
|
+
return self.engines
|
|
477
|
+
|
|
478
|
+
def _resolve_utilities(self) -> list[AgentComponent]:
|
|
479
|
+
if self.utilities:
|
|
480
|
+
return self.utilities
|
|
481
|
+
try:
|
|
482
|
+
from flock.components.agent import (
|
|
483
|
+
OutputUtilityComponent,
|
|
484
|
+
)
|
|
485
|
+
except Exception: # pragma: no cover - optional dependency issues
|
|
486
|
+
return []
|
|
487
|
+
|
|
488
|
+
default_component = OutputUtilityComponent()
|
|
489
|
+
self._add_utilities([default_component])
|
|
490
|
+
return self.utilities
|
|
491
|
+
|
|
492
|
+
def _find_matching_artifact(
|
|
493
|
+
self, output_decl: AgentOutput, result: EvalResult
|
|
494
|
+
) -> Artifact | None:
|
|
495
|
+
"""Delegate to OutputProcessor module."""
|
|
496
|
+
return self._output_processor.find_matching_artifact(output_decl, result)
|
|
497
|
+
|
|
498
|
+
def _select_payload(
|
|
499
|
+
self, output_decl: AgentOutput, result: EvalResult
|
|
500
|
+
) -> dict[str, Any] | None:
|
|
501
|
+
"""Delegate to OutputProcessor module."""
|
|
502
|
+
return self._output_processor.select_payload(output_decl, result)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class AgentBuilder:
|
|
506
|
+
"""Fluent builder that also acts as the runtime agent handle."""
|
|
507
|
+
|
|
508
|
+
def __init__(self, orchestrator: Flock, name: str) -> None:
|
|
509
|
+
self._orchestrator = orchestrator
|
|
510
|
+
self._agent = Agent(name, orchestrator=orchestrator)
|
|
511
|
+
self._agent.model = orchestrator.model
|
|
512
|
+
orchestrator.register_agent(self._agent)
|
|
513
|
+
|
|
514
|
+
# Fluent configuration -------------------------------------------------
|
|
515
|
+
|
|
516
|
+
def description(self, text: str) -> AgentBuilder:
|
|
517
|
+
"""Set the agent's description for documentation and tracing.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
text: Human-readable description of what the agent does
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
self for method chaining
|
|
524
|
+
|
|
525
|
+
Example:
|
|
526
|
+
>>> agent = (
|
|
527
|
+
... flock.agent("pizza_chef")
|
|
528
|
+
... .description("Creates authentic Italian pizza recipes")
|
|
529
|
+
... .consumes(Idea)
|
|
530
|
+
... .publishes(Recipe)
|
|
531
|
+
... )
|
|
532
|
+
"""
|
|
533
|
+
self._agent.description = text
|
|
534
|
+
return self
|
|
535
|
+
|
|
536
|
+
def consumes(
|
|
537
|
+
self,
|
|
538
|
+
*types: type[BaseModel],
|
|
539
|
+
where: Callable[[BaseModel], bool]
|
|
540
|
+
| Sequence[Callable[[BaseModel], bool]]
|
|
541
|
+
| None = None,
|
|
542
|
+
text: str | None = None,
|
|
543
|
+
min_p: float = 0.0,
|
|
544
|
+
from_agents: Iterable[str] | None = None,
|
|
545
|
+
tags: Iterable[str] | None = None,
|
|
546
|
+
join: dict | JoinSpec | None = None,
|
|
547
|
+
batch: dict | BatchSpec | None = None,
|
|
548
|
+
delivery: str = "exclusive",
|
|
549
|
+
mode: str = "both",
|
|
550
|
+
priority: int = 0,
|
|
551
|
+
) -> AgentBuilder:
|
|
552
|
+
"""Declare which artifact types this agent processes.
|
|
553
|
+
|
|
554
|
+
Sets up subscription rules that determine when the agent executes.
|
|
555
|
+
Supports type-based matching, conditional filters, batching, and joins.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
*types: Artifact types (Pydantic models) to consume
|
|
559
|
+
where: Optional filter predicate(s). Agent only executes if predicate returns True.
|
|
560
|
+
Can be a single callable or sequence of callables (all must pass).
|
|
561
|
+
text: Optional semantic text filter using embedding similarity
|
|
562
|
+
min_p: Minimum probability threshold for text similarity (0.0-1.0)
|
|
563
|
+
from_agents: Only consume artifacts from specific agents
|
|
564
|
+
tags: Only consume artifacts with matching tags
|
|
565
|
+
join: Join specification for coordinating multiple artifact types
|
|
566
|
+
batch: Batch specification for processing multiple artifacts together
|
|
567
|
+
delivery: Delivery mode - "exclusive" (one agent) or "broadcast" (all matching)
|
|
568
|
+
mode: Processing mode - "both", "streaming", or "batch"
|
|
569
|
+
priority: Execution priority (higher = executes first)
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
self for method chaining
|
|
573
|
+
|
|
574
|
+
Examples:
|
|
575
|
+
>>> # Basic type subscription
|
|
576
|
+
>>> agent.consumes(Task)
|
|
577
|
+
|
|
578
|
+
>>> # Multiple types
|
|
579
|
+
>>> agent.consumes(Task, Event, Command)
|
|
580
|
+
|
|
581
|
+
>>> # Conditional consumption (filtering)
|
|
582
|
+
>>> agent.consumes(Review, where=lambda r: r.score >= 8)
|
|
583
|
+
|
|
584
|
+
>>> # Multiple predicates (all must pass)
|
|
585
|
+
>>> agent.consumes(
|
|
586
|
+
... Order,
|
|
587
|
+
... where=[lambda o: o.total > 100, lambda o: o.status == "pending"],
|
|
588
|
+
... )
|
|
589
|
+
|
|
590
|
+
>>> # Consume from specific agents
|
|
591
|
+
>>> agent.consumes(Report, from_agents=["analyzer", "validator"])
|
|
592
|
+
|
|
593
|
+
>>> # Channel-based routing
|
|
594
|
+
>>> agent.consumes(Alert, tags={"critical", "security"})
|
|
595
|
+
|
|
596
|
+
>>> # Batch processing
|
|
597
|
+
>>> agent.consumes(Email, batch={"size": 10, "timeout": 5.0})
|
|
598
|
+
"""
|
|
599
|
+
predicates: Sequence[Callable[[BaseModel], bool]] | None
|
|
600
|
+
if where is None:
|
|
601
|
+
predicates = None
|
|
602
|
+
elif callable(where):
|
|
603
|
+
predicates = [where]
|
|
604
|
+
else:
|
|
605
|
+
predicates = list(where)
|
|
606
|
+
|
|
607
|
+
# Phase 5B: Use BuilderValidator for normalization
|
|
608
|
+
join_spec = BuilderValidator.normalize_join(join)
|
|
609
|
+
batch_spec = BuilderValidator.normalize_batch(batch)
|
|
610
|
+
text_predicates = [TextPredicate(text=text, min_p=min_p)] if text else []
|
|
611
|
+
subscription = Subscription(
|
|
612
|
+
agent_name=self._agent.name,
|
|
613
|
+
types=types,
|
|
614
|
+
where=predicates,
|
|
615
|
+
text_predicates=text_predicates,
|
|
616
|
+
from_agents=from_agents,
|
|
617
|
+
tags=tags,
|
|
618
|
+
join=join_spec,
|
|
619
|
+
batch=batch_spec,
|
|
620
|
+
delivery=delivery,
|
|
621
|
+
mode=mode,
|
|
622
|
+
priority=priority,
|
|
623
|
+
)
|
|
624
|
+
self._agent.subscriptions.append(subscription)
|
|
625
|
+
return self
|
|
626
|
+
|
|
627
|
+
def publishes(
|
|
628
|
+
self,
|
|
629
|
+
*types: type[BaseModel],
|
|
630
|
+
visibility: Visibility | Callable[[BaseModel], Visibility] | None = None,
|
|
631
|
+
fan_out: int | None = None,
|
|
632
|
+
where: Callable[[BaseModel], bool] | None = None,
|
|
633
|
+
validate: Callable[[BaseModel], bool]
|
|
634
|
+
| list[tuple[Callable, str]]
|
|
635
|
+
| None = None,
|
|
636
|
+
description: str | None = None,
|
|
637
|
+
) -> PublishBuilder:
|
|
638
|
+
"""Declare which artifact types this agent produces.
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
*types: Artifact types (Pydantic models) to publish
|
|
642
|
+
visibility: Default visibility control OR callable for dynamic visibility
|
|
643
|
+
fan_out: Number of artifacts to publish (applies to ALL types)
|
|
644
|
+
where: Filter predicate for output artifacts
|
|
645
|
+
validate: Validation predicate(s) - callable or list of (callable, error_msg) tuples
|
|
646
|
+
description: Group-level description override
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
PublishBuilder for conditional publishing configuration
|
|
650
|
+
|
|
651
|
+
Examples:
|
|
652
|
+
>>> agent.publishes(Report) # Publish 1 Report
|
|
653
|
+
>>> agent.publishes(
|
|
654
|
+
... Task, Task, Task
|
|
655
|
+
... ) # Publish 3 Tasks (duplicate counting)
|
|
656
|
+
>>> agent.publishes(Task, fan_out=3) # Same as above (sugar syntax)
|
|
657
|
+
>>> agent.publishes(Task, where=lambda t: t.priority > 5) # With filtering
|
|
658
|
+
>>> agent.publishes(
|
|
659
|
+
... Report, validate=lambda r: r.score > 0
|
|
660
|
+
... ) # With validation
|
|
661
|
+
>>> agent.publishes(
|
|
662
|
+
... Task, description="Special instructions"
|
|
663
|
+
... ) # With description
|
|
664
|
+
|
|
665
|
+
See Also:
|
|
666
|
+
- PublicVisibility: Default, visible to all agents
|
|
667
|
+
- PrivateVisibility: Allowlist-based access control
|
|
668
|
+
- TenantVisibility: Multi-tenant isolation
|
|
669
|
+
- LabelledVisibility: Role-based access control
|
|
670
|
+
"""
|
|
671
|
+
# Validate fan_out if provided
|
|
672
|
+
if fan_out is not None and fan_out < 1:
|
|
673
|
+
raise ValueError(f"fan_out must be >= 1, got {fan_out}")
|
|
674
|
+
|
|
675
|
+
# Resolve visibility
|
|
676
|
+
resolved_visibility = (
|
|
677
|
+
ensure_visibility(visibility) if not callable(visibility) else visibility
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Create AgentOutput objects for this group
|
|
681
|
+
outputs: list[AgentOutput] = []
|
|
682
|
+
|
|
683
|
+
if fan_out is not None:
|
|
684
|
+
# Apply fan_out to ALL types
|
|
685
|
+
for model in types:
|
|
686
|
+
spec = ArtifactSpec.from_model(model)
|
|
687
|
+
output = AgentOutput(
|
|
688
|
+
spec=spec,
|
|
689
|
+
default_visibility=resolved_visibility,
|
|
690
|
+
count=fan_out,
|
|
691
|
+
filter_predicate=where,
|
|
692
|
+
validate_predicate=validate,
|
|
693
|
+
group_description=description,
|
|
694
|
+
)
|
|
695
|
+
outputs.append(output)
|
|
696
|
+
else:
|
|
697
|
+
# Create separate AgentOutput for each type (including duplicates)
|
|
698
|
+
# This preserves order: .publishes(A, B, A) → [A, B, A] (3 outputs)
|
|
699
|
+
for model in types:
|
|
700
|
+
spec = ArtifactSpec.from_model(model)
|
|
701
|
+
output = AgentOutput(
|
|
702
|
+
spec=spec,
|
|
703
|
+
default_visibility=resolved_visibility,
|
|
704
|
+
count=1,
|
|
705
|
+
filter_predicate=where,
|
|
706
|
+
validate_predicate=validate,
|
|
707
|
+
group_description=description,
|
|
708
|
+
)
|
|
709
|
+
outputs.append(output)
|
|
710
|
+
|
|
711
|
+
# Create OutputGroup from outputs
|
|
712
|
+
group = OutputGroup(
|
|
713
|
+
outputs=outputs,
|
|
714
|
+
shared_visibility=resolved_visibility
|
|
715
|
+
if not callable(resolved_visibility)
|
|
716
|
+
else None,
|
|
717
|
+
group_description=description,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
# Append to agent's output_groups
|
|
721
|
+
self._agent.output_groups.append(group)
|
|
722
|
+
|
|
723
|
+
# Phase 5B: Use BuilderValidator for validation
|
|
724
|
+
BuilderValidator.validate_self_trigger_risk(self._agent)
|
|
725
|
+
|
|
726
|
+
return PublishBuilder(self, outputs)
|
|
727
|
+
|
|
728
|
+
def with_utilities(self, *components: AgentComponent) -> AgentBuilder:
|
|
729
|
+
"""Add utility components to customize agent lifecycle and behavior.
|
|
730
|
+
|
|
731
|
+
Components are hooks that run at specific points in the agent execution
|
|
732
|
+
lifecycle. Common uses include rate limiting, budgets, metrics, caching,
|
|
733
|
+
and custom preprocessing/postprocessing.
|
|
734
|
+
|
|
735
|
+
Args:
|
|
736
|
+
*components: AgentComponent instances with lifecycle hooks
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
self for method chaining
|
|
740
|
+
|
|
741
|
+
Examples:
|
|
742
|
+
>>> # Rate limiting
|
|
743
|
+
>>> agent.with_utilities(RateLimiter(max_calls=10, window=60))
|
|
744
|
+
|
|
745
|
+
>>> # Budget control
|
|
746
|
+
>>> agent.with_utilities(TokenBudget(max_tokens=10000))
|
|
747
|
+
|
|
748
|
+
>>> # Multiple components (executed in order)
|
|
749
|
+
>>> agent.with_utilities(
|
|
750
|
+
... RateLimiter(max_calls=5), MetricsCollector(), CacheLayer(ttl=3600)
|
|
751
|
+
... )
|
|
752
|
+
|
|
753
|
+
See Also:
|
|
754
|
+
- AgentComponent: Base class for custom components
|
|
755
|
+
- Lifecycle hooks: on_initialize, on_pre_consume, on_post_publish, etc.
|
|
756
|
+
"""
|
|
757
|
+
if components:
|
|
758
|
+
self._agent._add_utilities(list(components))
|
|
759
|
+
return self
|
|
760
|
+
|
|
761
|
+
def with_engines(self, *engines: EngineComponent) -> AgentBuilder:
|
|
762
|
+
"""Configure LLM engines for agent evaluation.
|
|
763
|
+
|
|
764
|
+
Engines determine how agents process inputs. Default is DSPy with the
|
|
765
|
+
orchestrator's model. Custom engines enable different LLM backends,
|
|
766
|
+
non-LLM logic, or hybrid approaches.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
*engines: EngineComponent instances for evaluation
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
self for method chaining
|
|
773
|
+
|
|
774
|
+
Examples:
|
|
775
|
+
>>> # DSPy engine with specific model
|
|
776
|
+
>>> agent.with_engines(DSPyEngine(model="openai/gpt-4o"))
|
|
777
|
+
|
|
778
|
+
>>> # Custom non-LLM engine
|
|
779
|
+
>>> agent.with_engines(RuleBasedEngine(rules=my_rules))
|
|
780
|
+
|
|
781
|
+
>>> # Hybrid approach (multiple engines)
|
|
782
|
+
>>> agent.with_engines(
|
|
783
|
+
... DSPyEngine(model="openai/gpt-4o-mini"), FallbackEngine()
|
|
784
|
+
... )
|
|
785
|
+
|
|
786
|
+
Note:
|
|
787
|
+
If no engines specified, agent uses DSPy with the orchestrator's default model.
|
|
788
|
+
|
|
789
|
+
See Also:
|
|
790
|
+
- DSPyEngine: Default LLM-based evaluation
|
|
791
|
+
- EngineComponent: Base class for custom engines
|
|
792
|
+
"""
|
|
793
|
+
self._agent.engines.extend(engines)
|
|
794
|
+
return self
|
|
795
|
+
|
|
796
|
+
def best_of(self, n: int, score: Callable[[EvalResult], float]) -> AgentBuilder:
|
|
797
|
+
self._agent.best_of_n = max(1, n)
|
|
798
|
+
self._agent.best_of_score = score
|
|
799
|
+
# Phase 5B: Use BuilderValidator for validation
|
|
800
|
+
BuilderValidator.validate_best_of(self._agent.name, n)
|
|
801
|
+
return self
|
|
802
|
+
|
|
803
|
+
def max_concurrency(self, n: int) -> AgentBuilder:
|
|
804
|
+
self._agent.set_max_concurrency(n)
|
|
805
|
+
# Phase 5B: Use BuilderValidator for validation
|
|
806
|
+
BuilderValidator.validate_concurrency(self._agent.name, n)
|
|
807
|
+
return self
|
|
808
|
+
|
|
809
|
+
def calls(self, func: Callable[..., Any]) -> AgentBuilder:
|
|
810
|
+
function_registry.register(func)
|
|
811
|
+
self._agent.calls_func = func
|
|
812
|
+
return self
|
|
813
|
+
|
|
814
|
+
def with_tools(self, funcs: Iterable[Callable[..., Any]]) -> AgentBuilder:
|
|
815
|
+
self._agent.tools.update(funcs)
|
|
816
|
+
return self
|
|
817
|
+
|
|
818
|
+
def with_context(self, provider: Any) -> AgentBuilder:
|
|
819
|
+
"""Configure a custom context provider for this agent (Phase 3 security fix).
|
|
820
|
+
|
|
821
|
+
Context providers control what artifacts an agent can see, enforcing
|
|
822
|
+
visibility filtering at the security boundary layer.
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
provider: ContextProvider instance for this agent
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
self for method chaining
|
|
829
|
+
|
|
830
|
+
Examples:
|
|
831
|
+
>>> # Use custom provider for this agent
|
|
832
|
+
>>> agent.with_context(MyCustomProvider())
|
|
833
|
+
|
|
834
|
+
>>> # Use FilteredContextProvider for declarative filtering
|
|
835
|
+
>>> agent.with_context(
|
|
836
|
+
... FilteredContextProvider(FilterConfig(tags={"important"}))
|
|
837
|
+
... )
|
|
838
|
+
|
|
839
|
+
Note:
|
|
840
|
+
Per-agent provider takes precedence over global provider configured
|
|
841
|
+
on Flock(context_provider=...). If neither is set, DefaultContextProvider
|
|
842
|
+
is used automatically.
|
|
843
|
+
|
|
844
|
+
See Also:
|
|
845
|
+
- DefaultContextProvider: Default security boundary with visibility enforcement
|
|
846
|
+
- FilteredContextProvider: Declarative filtering with FilterConfig
|
|
847
|
+
"""
|
|
848
|
+
self._agent.context_provider = provider
|
|
849
|
+
return self
|
|
850
|
+
|
|
851
|
+
def with_mcps(
|
|
852
|
+
self,
|
|
853
|
+
servers: (Iterable[str] | dict[str, MCPServerConfig]),
|
|
854
|
+
) -> AgentBuilder:
|
|
855
|
+
"""Assign MCP servers to this agent with optional server-specific mount points.
|
|
856
|
+
|
|
857
|
+
Architecture Decision: AD001 - Two-Level Architecture
|
|
858
|
+
Agents reference servers registered at orchestrator level.
|
|
859
|
+
|
|
860
|
+
Args:
|
|
861
|
+
servers: One of:
|
|
862
|
+
- List of server names (strings) - no specific mounts
|
|
863
|
+
- Dict mapping server names to MCPServerConfig
|
|
864
|
+
|
|
865
|
+
Returns:
|
|
866
|
+
self for method chaining
|
|
867
|
+
|
|
868
|
+
Raises:
|
|
869
|
+
ValueError: If any server name is not registered with orchestrator
|
|
870
|
+
|
|
871
|
+
Examples:
|
|
872
|
+
>>> # Simple: no mount restrictions
|
|
873
|
+
>>> agent.with_mcps(["filesystem", "github"])
|
|
874
|
+
|
|
875
|
+
>>> # Server-specific config with roots and tool whitelist
|
|
876
|
+
>>> agent.with_mcps({
|
|
877
|
+
... "filesystem": {
|
|
878
|
+
... "roots": ["/workspace/dir/data"],
|
|
879
|
+
... "tool_whitelist": ["read_file"],
|
|
880
|
+
... },
|
|
881
|
+
... "github": {}, # No restrictions for github
|
|
882
|
+
... })
|
|
883
|
+
"""
|
|
884
|
+
# Delegate to MCPIntegration module
|
|
885
|
+
registered_servers = set(self._orchestrator._mcp_configs.keys())
|
|
886
|
+
self._agent._mcp_integration.configure_servers(servers, registered_servers)
|
|
887
|
+
return self
|
|
888
|
+
|
|
889
|
+
def labels(self, *labels: str) -> AgentBuilder:
|
|
890
|
+
self._agent.labels.update(labels)
|
|
891
|
+
return self
|
|
892
|
+
|
|
893
|
+
def tenant(self, tenant_id: str) -> AgentBuilder:
|
|
894
|
+
self._agent.tenant_id = tenant_id
|
|
895
|
+
return self
|
|
896
|
+
|
|
897
|
+
def prevent_self_trigger(self, enabled: bool = True) -> AgentBuilder:
|
|
898
|
+
"""Prevent agent from being triggered by its own outputs.
|
|
899
|
+
|
|
900
|
+
When enabled (default), the orchestrator will skip scheduling this agent
|
|
901
|
+
for artifacts it produced itself. This prevents infinite feedback loops
|
|
902
|
+
when an agent consumes and publishes the same type.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
enabled: True to prevent self-triggering (safe default),
|
|
906
|
+
False to allow feedback loops (advanced use case)
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
AgentBuilder for method chaining
|
|
910
|
+
|
|
911
|
+
Example:
|
|
912
|
+
# Safe by default (recommended)
|
|
913
|
+
agent.consumes(Document).publishes(Document)
|
|
914
|
+
# Won't trigger on own outputs ✅
|
|
915
|
+
|
|
916
|
+
# Explicit feedback loop (use with caution!)
|
|
917
|
+
agent.consumes(Data, where=lambda d: d.depth < 10)
|
|
918
|
+
.publishes(Data)
|
|
919
|
+
.prevent_self_trigger(False) # Acknowledge risk
|
|
920
|
+
"""
|
|
921
|
+
self._agent.prevent_self_trigger = enabled
|
|
922
|
+
return self
|
|
923
|
+
|
|
924
|
+
# Runtime helpers ------------------------------------------------------
|
|
925
|
+
|
|
926
|
+
def run(self, *inputs: BaseModel) -> RunHandle:
|
|
927
|
+
return RunHandle(self._agent, list(inputs))
|
|
928
|
+
|
|
929
|
+
def then(self, other: AgentBuilder) -> Pipeline:
|
|
930
|
+
return Pipeline([self, other])
|
|
931
|
+
|
|
932
|
+
# Phase 5B: Validation and normalization moved to BuilderValidator module
|
|
933
|
+
|
|
934
|
+
# Properties -----------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
@property
|
|
937
|
+
def name(self) -> str:
|
|
938
|
+
return self._agent.name
|
|
939
|
+
|
|
940
|
+
@property
|
|
941
|
+
def agent(self) -> Agent:
|
|
942
|
+
return self._agent
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
# Phase 5B: Helper classes moved to builder_helpers module
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
__all__ = [
|
|
949
|
+
"Agent",
|
|
950
|
+
"AgentBuilder",
|
|
951
|
+
"AgentOutput",
|
|
952
|
+
"OutputGroup",
|
|
953
|
+
]
|