evolvingmachines-evolve 0.0.55.dev1355__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.
@@ -0,0 +1,512 @@
1
+ """Pipeline - Fluent API for Swarm Operations.
2
+
3
+ Thin wrapper over Swarm providing method chaining, timing, and events.
4
+
5
+ Example:
6
+ ```python
7
+ pipeline = (
8
+ Pipeline(swarm)
9
+ .map(MapConfig(prompt="Analyze..."))
10
+ .filter(FilterConfig(
11
+ prompt="Rate quality",
12
+ schema=QualitySchema,
13
+ condition=lambda d: d.score > 7,
14
+ ))
15
+ .reduce(ReduceConfig(prompt="Synthesize..."))
16
+ )
17
+
18
+ # Run with items
19
+ result = await pipeline.run(documents)
20
+
21
+ # Reusable - run with different data
22
+ await pipeline.run(batch1)
23
+ await pipeline.run(batch2)
24
+ ```
25
+ """
26
+
27
+ import secrets
28
+ import time
29
+ from dataclasses import dataclass, field, replace
30
+ from typing import Any, Callable, Generic, List, Optional, TypeVar, Union, overload
31
+
32
+ from ..swarm import Swarm
33
+ from ..swarm.types import FileMap, ItemInput, BestOfConfig, VerifyConfig, PipelineContext
34
+ from ..swarm.results import SwarmResult, SwarmResultList, ReduceResult
35
+ from ..retry import RetryConfig
36
+ from .types import (
37
+ Step,
38
+ StepType,
39
+ MapConfig,
40
+ FilterConfig,
41
+ ReduceConfig,
42
+ StepResult,
43
+ PipelineResult,
44
+ PipelineEvents,
45
+ PipelineEventMap,
46
+ StepStartEvent,
47
+ StepCompleteEvent,
48
+ StepErrorEvent,
49
+ ItemRetryEvent,
50
+ WorkerCompleteEvent,
51
+ VerifierCompleteEvent,
52
+ CandidateCompleteEvent,
53
+ JudgeCompleteEvent,
54
+ )
55
+
56
+
57
+ T = TypeVar('T')
58
+
59
+
60
+ # =============================================================================
61
+ # PIPELINE
62
+ # =============================================================================
63
+
64
+ @dataclass
65
+ class Pipeline(Generic[T]):
66
+ """Pipeline for chaining Swarm operations.
67
+
68
+ Swarm is bound at construction (infrastructure).
69
+ Items are passed at execution (data).
70
+ Pipeline is immutable - each method returns a new instance.
71
+ """
72
+ _swarm: Swarm
73
+ _steps: List[Step] = field(default_factory=list)
74
+ _events: PipelineEvents = field(default_factory=PipelineEvents)
75
+
76
+ # ===========================================================================
77
+ # STEP METHODS
78
+ # ===========================================================================
79
+
80
+ def map(self, config: MapConfig[T]) -> 'Pipeline[T]':
81
+ """Add a map step to transform items in parallel."""
82
+ new_steps = self._steps + [Step(type="map", config=config)]
83
+ return Pipeline(
84
+ _swarm=self._swarm,
85
+ _steps=new_steps,
86
+ _events=self._events,
87
+ )
88
+
89
+ def filter(self, config: FilterConfig[T]) -> 'Pipeline[T]':
90
+ """Add a filter step to evaluate and filter items."""
91
+ new_steps = self._steps + [Step(type="filter", config=config)]
92
+ return Pipeline(
93
+ _swarm=self._swarm,
94
+ _steps=new_steps,
95
+ _events=self._events,
96
+ )
97
+
98
+ def reduce(self, config: ReduceConfig[T]) -> 'TerminalPipeline[T]':
99
+ """Add a reduce step (terminal - no steps can follow)."""
100
+ new_steps = self._steps + [Step(type="reduce", config=config)]
101
+ return TerminalPipeline(
102
+ _swarm=self._swarm,
103
+ _steps=new_steps,
104
+ _events=self._events,
105
+ )
106
+
107
+ # ===========================================================================
108
+ # EVENTS
109
+ # ===========================================================================
110
+
111
+ @overload
112
+ def on(self, handlers: PipelineEvents) -> 'Pipeline[T]': ...
113
+
114
+ @overload
115
+ def on(self, event: str, handler: Callable) -> 'Pipeline[T]': ...
116
+
117
+ def on(self, event_or_handlers: Union[PipelineEvents, str], handler: Optional[Callable] = None) -> 'Pipeline[T]':
118
+ """Register event handlers for step lifecycle.
119
+
120
+ Supports two styles:
121
+ - Object: .on(PipelineEvents(on_step_complete=fn))
122
+ - Chainable: .on("step_complete", fn)
123
+ """
124
+ if isinstance(event_or_handlers, str):
125
+ # Chainable style: .on("step_complete", fn)
126
+ key = PipelineEventMap.get(event_or_handlers)
127
+ if key is None:
128
+ raise ValueError(f"Unknown event: {event_or_handlers}")
129
+ new_events = replace(self._events, **{key: handler})
130
+ else:
131
+ # Object style: .on(PipelineEvents(...))
132
+ new_events = replace(
133
+ self._events,
134
+ on_step_start=event_or_handlers.on_step_start or self._events.on_step_start,
135
+ on_step_complete=event_or_handlers.on_step_complete or self._events.on_step_complete,
136
+ on_step_error=event_or_handlers.on_step_error or self._events.on_step_error,
137
+ on_item_retry=event_or_handlers.on_item_retry or self._events.on_item_retry,
138
+ on_worker_complete=event_or_handlers.on_worker_complete or self._events.on_worker_complete,
139
+ on_verifier_complete=event_or_handlers.on_verifier_complete or self._events.on_verifier_complete,
140
+ on_candidate_complete=event_or_handlers.on_candidate_complete or self._events.on_candidate_complete,
141
+ on_judge_complete=event_or_handlers.on_judge_complete or self._events.on_judge_complete,
142
+ )
143
+ return Pipeline(
144
+ _swarm=self._swarm,
145
+ _steps=self._steps,
146
+ _events=new_events,
147
+ )
148
+
149
+ # ===========================================================================
150
+ # EXECUTION
151
+ # ===========================================================================
152
+
153
+ async def run(self, items: List[ItemInput]) -> PipelineResult[T]:
154
+ """Execute the pipeline with the given items."""
155
+ pipeline_run_id = secrets.token_hex(8)
156
+ step_results: List[StepResult] = []
157
+ current_items: List[ItemInput] = list(items)
158
+ start_time = time.time()
159
+
160
+ for i, step in enumerate(self._steps):
161
+ step_name = getattr(step.config, 'name', None)
162
+ step_start = time.time()
163
+
164
+ if self._events.on_step_start:
165
+ self._events.on_step_start(StepStartEvent(
166
+ type=step.type,
167
+ index=i,
168
+ name=step_name,
169
+ item_count=len(current_items),
170
+ ))
171
+
172
+ # Create PipelineContext for observability
173
+ pipeline_context = PipelineContext(
174
+ pipeline_run_id=pipeline_run_id,
175
+ pipeline_step_index=i,
176
+ )
177
+
178
+ try:
179
+ result = await self._execute_step(step, current_items, i, step_name, pipeline_context)
180
+ duration_ms = int((time.time() - step_start) * 1000)
181
+
182
+ step_results.append(StepResult(
183
+ type=step.type,
184
+ index=i,
185
+ duration_ms=duration_ms,
186
+ results=result["output"],
187
+ ))
188
+
189
+ if self._events.on_step_complete:
190
+ self._events.on_step_complete(StepCompleteEvent(
191
+ type=step.type,
192
+ index=i,
193
+ name=step_name,
194
+ duration_ms=duration_ms,
195
+ success_count=result["success_count"],
196
+ error_count=result["error_count"],
197
+ filtered_count=result["filtered_count"],
198
+ ))
199
+
200
+ # Reduce is terminal
201
+ if step.type == "reduce":
202
+ return PipelineResult(
203
+ pipeline_run_id=pipeline_run_id,
204
+ steps=step_results,
205
+ output=result["output"],
206
+ total_duration_ms=int((time.time() - start_time) * 1000),
207
+ )
208
+
209
+ current_items = result["next_items"]
210
+
211
+ except Exception as e:
212
+ if self._events.on_step_error:
213
+ self._events.on_step_error(StepErrorEvent(
214
+ type=step.type,
215
+ index=i,
216
+ name=step_name,
217
+ error=e,
218
+ ))
219
+ raise
220
+
221
+ last_result = step_results[-1] if step_results else None
222
+ return PipelineResult(
223
+ pipeline_run_id=pipeline_run_id,
224
+ steps=step_results,
225
+ output=last_result.results if last_result else [],
226
+ total_duration_ms=int((time.time() - start_time) * 1000),
227
+ )
228
+
229
+ # ===========================================================================
230
+ # PRIVATE
231
+ # ===========================================================================
232
+
233
+ async def _execute_step(
234
+ self,
235
+ step: Step,
236
+ items: List[ItemInput],
237
+ step_index: int,
238
+ step_name: Optional[str],
239
+ pipeline_context: PipelineContext,
240
+ ) -> dict:
241
+ """Execute a single step and return results."""
242
+ if step.type == "map":
243
+ config = step.config
244
+ results = await self._swarm.map(
245
+ items=items,
246
+ prompt=config.prompt,
247
+ system_prompt=config.system_prompt,
248
+ schema=config.schema,
249
+ schema_options=config.schema_options,
250
+ agent=config.agent,
251
+ mcp_servers=config.mcp_servers,
252
+ skills=config.skills,
253
+ integrations=config.integrations,
254
+ best_of=self._wrap_best_of(config.best_of, step_index, step_name),
255
+ verify=self._wrap_verify(config.verify, step_index, step_name),
256
+ retry=self._wrap_retry(config.retry, step_index, step_name),
257
+ timeout_ms=config.timeout_ms,
258
+ name=step_name,
259
+ _pipeline_context=pipeline_context,
260
+ )
261
+ return {
262
+ "output": list(results),
263
+ "next_items": results.success,
264
+ "success_count": len(results.success),
265
+ "error_count": len(results.error),
266
+ "filtered_count": 0,
267
+ }
268
+
269
+ if step.type == "filter":
270
+ config = step.config
271
+ results = await self._swarm.filter(
272
+ items=items,
273
+ prompt=config.prompt,
274
+ schema=config.schema,
275
+ condition=config.condition,
276
+ schema_options=config.schema_options,
277
+ system_prompt=config.system_prompt,
278
+ agent=config.agent,
279
+ mcp_servers=config.mcp_servers,
280
+ skills=config.skills,
281
+ integrations=config.integrations,
282
+ verify=self._wrap_verify(config.verify, step_index, step_name),
283
+ retry=self._wrap_retry(config.retry, step_index, step_name),
284
+ timeout_ms=config.timeout_ms,
285
+ name=step_name,
286
+ _pipeline_context=pipeline_context,
287
+ )
288
+ emit = getattr(config, 'emit', 'success')
289
+ if emit == "success":
290
+ next_items = results.success
291
+ elif emit == "filtered":
292
+ next_items = results.filtered
293
+ else: # "all"
294
+ next_items = results.success + results.filtered
295
+ return {
296
+ "output": list(results),
297
+ "next_items": next_items,
298
+ "success_count": len(results.success),
299
+ "error_count": len(results.error),
300
+ "filtered_count": len(results.filtered),
301
+ }
302
+
303
+ # reduce
304
+ config = step.config
305
+ result = await self._swarm.reduce(
306
+ items=items,
307
+ prompt=config.prompt,
308
+ system_prompt=config.system_prompt,
309
+ schema=config.schema,
310
+ schema_options=config.schema_options,
311
+ agent=config.agent,
312
+ mcp_servers=config.mcp_servers,
313
+ skills=config.skills,
314
+ integrations=config.integrations,
315
+ verify=self._wrap_verify(config.verify, step_index, step_name),
316
+ retry=self._wrap_retry(config.retry, step_index, step_name),
317
+ timeout_ms=config.timeout_ms,
318
+ name=step_name,
319
+ _pipeline_context=pipeline_context,
320
+ )
321
+ return {
322
+ "output": result,
323
+ "next_items": [],
324
+ "success_count": 1 if result.status == "success" else 0,
325
+ "error_count": 1 if result.status == "error" else 0,
326
+ "filtered_count": 0,
327
+ }
328
+
329
+ def _wrap_retry(
330
+ self,
331
+ config: Optional[RetryConfig],
332
+ step_index: int,
333
+ step_name: Optional[str],
334
+ ) -> Optional[RetryConfig]:
335
+ """Wrap retry config to inject pipeline-level callback."""
336
+ if config is None:
337
+ return None
338
+
339
+ original_callback = config.on_item_retry
340
+
341
+ def wrapped_callback(item_index: int, attempt: int, error: str):
342
+ if original_callback:
343
+ original_callback(item_index, attempt, error)
344
+ if self._events.on_item_retry:
345
+ self._events.on_item_retry(ItemRetryEvent(
346
+ step_index=step_index,
347
+ step_name=step_name,
348
+ item_index=item_index,
349
+ attempt=attempt,
350
+ error=error,
351
+ ))
352
+
353
+ return RetryConfig(
354
+ max_attempts=config.max_attempts,
355
+ backoff_ms=config.backoff_ms,
356
+ backoff_multiplier=config.backoff_multiplier,
357
+ retry_on=config.retry_on,
358
+ on_item_retry=wrapped_callback,
359
+ )
360
+
361
+ def _wrap_verify(
362
+ self,
363
+ config: Optional[VerifyConfig],
364
+ step_index: int,
365
+ step_name: Optional[str],
366
+ ) -> Optional[VerifyConfig]:
367
+ """Wrap verify config to inject pipeline-level callbacks."""
368
+ if config is None:
369
+ return None
370
+
371
+ original_worker = config.on_worker_complete
372
+ original_verifier = config.on_verifier_complete
373
+
374
+ def wrapped_worker(item_index: int, attempt: int, status: str):
375
+ if original_worker:
376
+ original_worker(item_index, attempt, status)
377
+ if self._events.on_worker_complete:
378
+ self._events.on_worker_complete(WorkerCompleteEvent(
379
+ step_index=step_index,
380
+ step_name=step_name,
381
+ item_index=item_index,
382
+ attempt=attempt,
383
+ status=status,
384
+ ))
385
+
386
+ def wrapped_verifier(item_index: int, attempt: int, passed: bool, feedback: Optional[str]):
387
+ if original_verifier:
388
+ original_verifier(item_index, attempt, passed, feedback)
389
+ if self._events.on_verifier_complete:
390
+ self._events.on_verifier_complete(VerifierCompleteEvent(
391
+ step_index=step_index,
392
+ step_name=step_name,
393
+ item_index=item_index,
394
+ attempt=attempt,
395
+ passed=passed,
396
+ feedback=feedback,
397
+ ))
398
+
399
+ return VerifyConfig(
400
+ criteria=config.criteria,
401
+ max_attempts=config.max_attempts,
402
+ verifier_agent=config.verifier_agent,
403
+ verifier_mcp_servers=config.verifier_mcp_servers,
404
+ verifier_skills=config.verifier_skills,
405
+ verifier_integrations=config.verifier_integrations,
406
+ on_worker_complete=wrapped_worker,
407
+ on_verifier_complete=wrapped_verifier,
408
+ )
409
+
410
+ def _wrap_best_of(
411
+ self,
412
+ config: Optional[BestOfConfig],
413
+ step_index: int,
414
+ step_name: Optional[str],
415
+ ) -> Optional[BestOfConfig]:
416
+ """Wrap bestOf config to inject pipeline-level callbacks."""
417
+ if config is None:
418
+ return None
419
+
420
+ original_candidate = config.on_candidate_complete
421
+ original_judge = config.on_judge_complete
422
+
423
+ def wrapped_candidate(item_index: int, candidate_index: int, status: str):
424
+ if original_candidate:
425
+ original_candidate(item_index, candidate_index, status)
426
+ if self._events.on_candidate_complete:
427
+ self._events.on_candidate_complete(CandidateCompleteEvent(
428
+ step_index=step_index,
429
+ step_name=step_name,
430
+ item_index=item_index,
431
+ candidate_index=candidate_index,
432
+ status=status,
433
+ ))
434
+
435
+ def wrapped_judge(item_index: int, winner_index: int, reasoning: str):
436
+ if original_judge:
437
+ original_judge(item_index, winner_index, reasoning)
438
+ if self._events.on_judge_complete:
439
+ self._events.on_judge_complete(JudgeCompleteEvent(
440
+ step_index=step_index,
441
+ step_name=step_name,
442
+ item_index=item_index,
443
+ winner_index=winner_index,
444
+ reasoning=reasoning,
445
+ ))
446
+
447
+ return BestOfConfig(
448
+ judge_criteria=config.judge_criteria,
449
+ n=config.n,
450
+ task_agents=config.task_agents,
451
+ judge_agent=config.judge_agent,
452
+ mcp_servers=config.mcp_servers,
453
+ judge_mcp_servers=config.judge_mcp_servers,
454
+ skills=config.skills,
455
+ judge_skills=config.judge_skills,
456
+ integrations=config.integrations,
457
+ judge_integrations=config.judge_integrations,
458
+ on_candidate_complete=wrapped_candidate,
459
+ on_judge_complete=wrapped_judge,
460
+ )
461
+
462
+
463
+ # =============================================================================
464
+ # TERMINAL PIPELINE
465
+ # =============================================================================
466
+
467
+ @dataclass
468
+ class TerminalPipeline(Pipeline[T]):
469
+ """Pipeline after reduce - no more steps can be added."""
470
+
471
+ def map(self, config: MapConfig) -> 'Pipeline':
472
+ """Cannot add steps after reduce."""
473
+ raise RuntimeError("Cannot add steps after reduce")
474
+
475
+ def filter(self, config: FilterConfig) -> 'Pipeline':
476
+ """Cannot add steps after reduce."""
477
+ raise RuntimeError("Cannot add steps after reduce")
478
+
479
+ def reduce(self, config: ReduceConfig) -> 'TerminalPipeline':
480
+ """Cannot add steps after reduce."""
481
+ raise RuntimeError("Cannot add steps after reduce")
482
+
483
+ @overload
484
+ def on(self, handlers: PipelineEvents) -> 'TerminalPipeline[T]': ...
485
+
486
+ @overload
487
+ def on(self, event: str, handler: Callable) -> 'TerminalPipeline[T]': ...
488
+
489
+ def on(self, event_or_handlers: Union[PipelineEvents, str], handler: Optional[Callable] = None) -> 'TerminalPipeline[T]':
490
+ """Register event handlers for step lifecycle."""
491
+ if isinstance(event_or_handlers, str):
492
+ key = PipelineEventMap.get(event_or_handlers)
493
+ if key is None:
494
+ raise ValueError(f"Unknown event: {event_or_handlers}")
495
+ new_events = replace(self._events, **{key: handler})
496
+ else:
497
+ new_events = replace(
498
+ self._events,
499
+ on_step_start=event_or_handlers.on_step_start or self._events.on_step_start,
500
+ on_step_complete=event_or_handlers.on_step_complete or self._events.on_step_complete,
501
+ on_step_error=event_or_handlers.on_step_error or self._events.on_step_error,
502
+ on_item_retry=event_or_handlers.on_item_retry or self._events.on_item_retry,
503
+ on_worker_complete=event_or_handlers.on_worker_complete or self._events.on_worker_complete,
504
+ on_verifier_complete=event_or_handlers.on_verifier_complete or self._events.on_verifier_complete,
505
+ on_candidate_complete=event_or_handlers.on_candidate_complete or self._events.on_candidate_complete,
506
+ on_judge_complete=event_or_handlers.on_judge_complete or self._events.on_judge_complete,
507
+ )
508
+ return TerminalPipeline(
509
+ _swarm=self._swarm,
510
+ _steps=self._steps,
511
+ _events=new_events,
512
+ )