puffinflow 2.dev0__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.
Files changed (55) hide show
  1. puffinflow/__init__.py +132 -0
  2. puffinflow/core/__init__.py +110 -0
  3. puffinflow/core/agent/__init__.py +320 -0
  4. puffinflow/core/agent/base.py +1635 -0
  5. puffinflow/core/agent/checkpoint.py +50 -0
  6. puffinflow/core/agent/context.py +521 -0
  7. puffinflow/core/agent/decorators/__init__.py +90 -0
  8. puffinflow/core/agent/decorators/builder.py +454 -0
  9. puffinflow/core/agent/decorators/flexible.py +714 -0
  10. puffinflow/core/agent/decorators/inspection.py +144 -0
  11. puffinflow/core/agent/dependencies.py +57 -0
  12. puffinflow/core/agent/scheduling/__init__.py +21 -0
  13. puffinflow/core/agent/scheduling/builder.py +160 -0
  14. puffinflow/core/agent/scheduling/exceptions.py +35 -0
  15. puffinflow/core/agent/scheduling/inputs.py +137 -0
  16. puffinflow/core/agent/scheduling/parser.py +209 -0
  17. puffinflow/core/agent/scheduling/scheduler.py +413 -0
  18. puffinflow/core/agent/state.py +141 -0
  19. puffinflow/core/config.py +62 -0
  20. puffinflow/core/coordination/__init__.py +137 -0
  21. puffinflow/core/coordination/agent_group.py +359 -0
  22. puffinflow/core/coordination/agent_pool.py +629 -0
  23. puffinflow/core/coordination/agent_team.py +577 -0
  24. puffinflow/core/coordination/coordinator.py +720 -0
  25. puffinflow/core/coordination/deadlock.py +1759 -0
  26. puffinflow/core/coordination/fluent_api.py +421 -0
  27. puffinflow/core/coordination/primitives.py +478 -0
  28. puffinflow/core/coordination/rate_limiter.py +520 -0
  29. puffinflow/core/observability/__init__.py +47 -0
  30. puffinflow/core/observability/agent.py +139 -0
  31. puffinflow/core/observability/alerting.py +73 -0
  32. puffinflow/core/observability/config.py +127 -0
  33. puffinflow/core/observability/context.py +88 -0
  34. puffinflow/core/observability/core.py +147 -0
  35. puffinflow/core/observability/decorators.py +105 -0
  36. puffinflow/core/observability/events.py +71 -0
  37. puffinflow/core/observability/interfaces.py +196 -0
  38. puffinflow/core/observability/metrics.py +137 -0
  39. puffinflow/core/observability/tracing.py +209 -0
  40. puffinflow/core/reliability/__init__.py +27 -0
  41. puffinflow/core/reliability/bulkhead.py +96 -0
  42. puffinflow/core/reliability/circuit_breaker.py +149 -0
  43. puffinflow/core/reliability/leak_detector.py +122 -0
  44. puffinflow/core/resources/__init__.py +77 -0
  45. puffinflow/core/resources/allocation.py +790 -0
  46. puffinflow/core/resources/pool.py +645 -0
  47. puffinflow/core/resources/quotas.py +567 -0
  48. puffinflow/core/resources/requirements.py +217 -0
  49. puffinflow/version.py +21 -0
  50. puffinflow-2.dev0.dist-info/METADATA +334 -0
  51. puffinflow-2.dev0.dist-info/RECORD +55 -0
  52. puffinflow-2.dev0.dist-info/WHEEL +5 -0
  53. puffinflow-2.dev0.dist-info/entry_points.txt +3 -0
  54. puffinflow-2.dev0.dist-info/licenses/LICENSE +21 -0
  55. puffinflow-2.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,577 @@
1
+ """Agent team coordination with messaging and event systems."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import logging
6
+ import time
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Callable, Optional
10
+
11
+ from ..agent.base import Agent, AgentResult
12
+ from ..agent.state import AgentStatus
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class TeamResult:
19
+ """Result container for team execution."""
20
+
21
+ team_name: str
22
+ status: str
23
+ agent_results: dict[str, AgentResult] = field(default_factory=dict)
24
+ start_time: Optional[float] = None
25
+ end_time: Optional[float] = None
26
+ total_duration: Optional[float] = None
27
+ error: Optional[Exception] = None
28
+
29
+ def get_agent_result(self, agent_name: str) -> Optional[AgentResult]:
30
+ """Get result for specific agent."""
31
+ return self.agent_results.get(agent_name)
32
+
33
+ def get_all_outputs(self, key: str) -> list[Any]:
34
+ """Get specific output from all agents."""
35
+ return [
36
+ result.get_output(key)
37
+ for result in self.agent_results.values()
38
+ if result.get_output(key) is not None
39
+ ]
40
+
41
+ def get_all_variables(self, key: str) -> list[Any]:
42
+ """Get specific variable from all agents."""
43
+ return [
44
+ result.get_variable(key)
45
+ for result in self.agent_results.values()
46
+ if result.get_variable(key) is not None
47
+ ]
48
+
49
+ def get_best_by(self, metric: str, maximize: bool = True) -> Optional[AgentResult]:
50
+ """Get agent with best metric value."""
51
+ valid_results = [
52
+ result
53
+ for result in self.agent_results.values()
54
+ if result.get_output(metric) is not None
55
+ or result.get_metric(metric) is not None
56
+ ]
57
+
58
+ if not valid_results:
59
+ return None
60
+
61
+ def get_value(result: Any) -> Any:
62
+ return result.get_output(metric) or result.get_metric(metric) or 0
63
+
64
+ return (
65
+ max(valid_results, key=get_value)
66
+ if maximize
67
+ else min(valid_results, key=get_value)
68
+ )
69
+
70
+ def average(self, key: str) -> float:
71
+ """Get average of a numeric output/metric across agents."""
72
+ values = []
73
+ for result in self.agent_results.values():
74
+ value = result.get_output(key) or result.get_metric(key)
75
+ if isinstance(value, (int, float)):
76
+ values.append(value)
77
+
78
+ return sum(values) / len(values) if values else 0.0
79
+
80
+ def sum(self, key: str) -> float:
81
+ """Get sum of a numeric output/metric across agents."""
82
+ total = 0.0
83
+ for result in self.agent_results.values():
84
+ value = result.get_output(key) or result.get_metric(key)
85
+ if isinstance(value, (int, float)):
86
+ total += value
87
+ return total
88
+
89
+ def count_successful(self) -> int:
90
+ """Count successful agent executions."""
91
+ return sum(1 for result in self.agent_results.values() if result.is_success)
92
+
93
+ def count_failed(self) -> int:
94
+ """Count failed agent executions."""
95
+ return sum(1 for result in self.agent_results.values() if result.is_failed)
96
+
97
+ @property
98
+ def success_rate(self) -> float:
99
+ """Get success rate as percentage."""
100
+ total = len(self.agent_results)
101
+ return (self.count_successful() / total * 100) if total > 0 else 0.0
102
+
103
+
104
+ @dataclass
105
+ class Message:
106
+ """Message between agents."""
107
+
108
+ sender: str
109
+ recipient: str
110
+ message_type: str
111
+ data: dict[str, Any]
112
+ timestamp: float = field(default_factory=time.time)
113
+ correlation_id: Optional[str] = None
114
+
115
+
116
+ @dataclass
117
+ class Event:
118
+ """Event emitted by agents."""
119
+
120
+ source: str
121
+ event_type: str
122
+ data: dict[str, Any]
123
+ timestamp: float = field(default_factory=time.time)
124
+ event_id: str = field(default_factory=lambda: f"event_{int(time.time() * 1000)}")
125
+
126
+
127
+ class EventBus:
128
+ """Event bus for agent communication."""
129
+
130
+ def __init__(self) -> None:
131
+ self._handlers: dict[str, list[Callable]] = defaultdict(list)
132
+ self._event_history: list[Event] = []
133
+ self._max_history = 1000
134
+
135
+ def subscribe(self, event_type: str, handler: Callable) -> None:
136
+ """Subscribe to event type."""
137
+ self._handlers[event_type].append(handler)
138
+
139
+ def unsubscribe(self, event_type: str, handler: Callable) -> None:
140
+ """Unsubscribe from event type."""
141
+ if event_type in self._handlers:
142
+ with contextlib.suppress(ValueError):
143
+ self._handlers[event_type].remove(handler)
144
+
145
+ async def emit(self, event: Event) -> None:
146
+ """Emit an event to all subscribers."""
147
+ self._event_history.append(event)
148
+
149
+ # Trim history if needed
150
+ if len(self._event_history) > self._max_history:
151
+ self._event_history = self._event_history[-self._max_history :]
152
+
153
+ # Notify handlers
154
+ handlers = self._handlers.get(event.event_type, [])
155
+ for handler in handlers:
156
+ try:
157
+ if asyncio.iscoroutinefunction(handler):
158
+ await handler(event)
159
+ else:
160
+ handler(event)
161
+ except Exception as e:
162
+ logger.error(f"Error in event handler for {event.event_type}: {e}")
163
+
164
+ def get_events(
165
+ self, event_type: Optional[str] = None, source: Optional[str] = None
166
+ ) -> list[Event]:
167
+ """Get events by type and/or source."""
168
+ events = self._event_history
169
+
170
+ if event_type:
171
+ events = [e for e in events if e.event_type == event_type]
172
+
173
+ if source:
174
+ events = [e for e in events if e.source == source]
175
+
176
+ return events
177
+
178
+
179
+ class AgentTeam:
180
+ """Enhanced agent team with coordination features."""
181
+
182
+ def __init__(self, name: str):
183
+ self.name = name
184
+ self._agents: dict[str, Agent] = {}
185
+ self._shared_context: dict[str, Any] = {}
186
+ self._message_queue: asyncio.Queue = asyncio.Queue()
187
+ self._event_bus = EventBus()
188
+ self._running = False
189
+ self._results: dict[str, AgentResult] = {}
190
+ self._execution_order: list[str] = []
191
+ self._parallel_groups: list[list[str]] = []
192
+ self._dependencies: dict[str, set[str]] = defaultdict(set)
193
+
194
+ def add_agent(self, agent: Agent) -> "AgentTeam":
195
+ """Add agent to team."""
196
+ self._agents[agent.name] = agent
197
+ agent.set_team(self)
198
+
199
+ # Share context
200
+ agent.shared_state.update(self._shared_context)
201
+
202
+ return self
203
+
204
+ def add_agents(self, agents: list[Agent]) -> "AgentTeam":
205
+ """Add multiple agents to team."""
206
+ for agent in agents:
207
+ self.add_agent(agent)
208
+ return self
209
+
210
+ def get_agent(self, name: str) -> Optional[Agent]:
211
+ """Get agent by name."""
212
+ return self._agents.get(name)
213
+
214
+ def remove_agent(self, name: str) -> bool:
215
+ """Remove agent from team."""
216
+ if name in self._agents:
217
+ del self._agents[name]
218
+ return True
219
+ return False
220
+
221
+ def with_shared_context(
222
+ self, context: Optional[dict[str, Any]] = None
223
+ ) -> "AgentTeam":
224
+ """Set shared context for all agents."""
225
+ if context:
226
+ self._shared_context.update(context)
227
+
228
+ # Update all agent contexts
229
+ for agent in self._agents.values():
230
+ agent.shared_state.update(context)
231
+
232
+ return self
233
+
234
+ def set_global_variable(self, key: str, value: Any) -> None:
235
+ """Set variable for all agents."""
236
+ self._shared_context[key] = value
237
+ for agent in self._agents.values():
238
+ agent.set_shared_variable(key, value)
239
+
240
+ def get_global_variable(self, key: str, default: Any = None) -> Any:
241
+ """Get global variable."""
242
+ return self._shared_context.get(key, default)
243
+
244
+ def set_variable_for_all(self, key: str, value: Any) -> "AgentTeam":
245
+ """Set variable for all agents (fluent)."""
246
+ for agent in self._agents.values():
247
+ agent.set_variable(key, value)
248
+ return self
249
+
250
+ # Messaging system
251
+ async def send_message(
252
+ self, sender: str, recipient: str, data: dict[str, Any]
253
+ ) -> dict[str, Any]:
254
+ """Send message between agents."""
255
+ message = Message(
256
+ sender=sender,
257
+ recipient=recipient,
258
+ message_type=data.get("message_type", "generic"),
259
+ data=data,
260
+ )
261
+
262
+ recipient_agent = self._agents.get(recipient)
263
+ if recipient_agent:
264
+ return await recipient_agent.handle_message(
265
+ message.message_type, message.data, sender
266
+ )
267
+
268
+ return {}
269
+
270
+ async def broadcast_message(
271
+ self, sender: str, message_type: str, data: dict[str, Any]
272
+ ) -> None:
273
+ """Broadcast message to all agents except sender."""
274
+ for agent_name, agent in self._agents.items():
275
+ if agent_name != sender:
276
+ try:
277
+ await agent.handle_message(message_type, data, sender)
278
+ except Exception as e:
279
+ logger.error(f"Error broadcasting to {agent_name}: {e}")
280
+
281
+ # Event system
282
+ async def emit_event(
283
+ self, source: str, event_type: str, data: dict[str, Any]
284
+ ) -> None:
285
+ """Emit event to event bus."""
286
+ event = Event(source=source, event_type=event_type, data=data)
287
+ await self._event_bus.emit(event)
288
+
289
+ def subscribe_to_events(self, event_type: str, handler: Callable) -> None:
290
+ """Subscribe to events."""
291
+ self._event_bus.subscribe(event_type, handler)
292
+
293
+ # Execution methods
294
+ async def run(
295
+ self, mode: str = "parallel", timeout: Optional[float] = None
296
+ ) -> TeamResult:
297
+ """Run the team with specified mode.
298
+
299
+ Args:
300
+ mode: Execution mode - "parallel", "sequential", or "dependencies"
301
+ timeout: Optional timeout for execution
302
+
303
+ Returns:
304
+ TeamResult with execution results
305
+ """
306
+ if mode == "parallel":
307
+ return await self.run_parallel(timeout)
308
+ elif mode == "sequential":
309
+ return await self.run_sequential()
310
+ elif mode == "dependencies":
311
+ return await self.run_with_dependencies()
312
+ else:
313
+ raise ValueError(f"Unknown execution mode: {mode}")
314
+
315
+ async def run_parallel(self, timeout: Optional[float] = None) -> TeamResult:
316
+ """Run all agents in parallel."""
317
+ start_time = time.time()
318
+
319
+ try:
320
+ # Create tasks for all agents
321
+ tasks = {
322
+ agent.name: asyncio.create_task(agent.run())
323
+ for agent in self._agents.values()
324
+ }
325
+
326
+ # Wait for completion with optional timeout
327
+ if timeout:
328
+ done, pending = await asyncio.wait(
329
+ tasks.values(), timeout=timeout, return_when=asyncio.ALL_COMPLETED
330
+ )
331
+
332
+ # Cancel pending tasks
333
+ for task in pending:
334
+ task.cancel()
335
+ else:
336
+ await asyncio.gather(*tasks.values(), return_exceptions=True)
337
+
338
+ # Collect results
339
+ results = {}
340
+ for agent_name, task in tasks.items():
341
+ if task.done():
342
+ try:
343
+ result = task.result()
344
+ results[agent_name] = result
345
+ except Exception as e:
346
+ # Create error result
347
+ results[agent_name] = AgentResult(
348
+ agent_name=agent_name,
349
+ status=AgentStatus.FAILED,
350
+ error=e,
351
+ start_time=start_time,
352
+ end_time=time.time(),
353
+ )
354
+
355
+ end_time = time.time()
356
+
357
+ return TeamResult(
358
+ team_name=self.name,
359
+ status="completed",
360
+ agent_results=results,
361
+ start_time=start_time,
362
+ end_time=end_time,
363
+ total_duration=end_time - start_time,
364
+ )
365
+
366
+ except Exception as e:
367
+ end_time = time.time()
368
+ return TeamResult(
369
+ team_name=self.name,
370
+ status="failed",
371
+ error=e,
372
+ start_time=start_time,
373
+ end_time=end_time,
374
+ total_duration=end_time - start_time,
375
+ )
376
+
377
+ async def run_sequential(
378
+ self, agent_order: Optional[list[str]] = None
379
+ ) -> TeamResult:
380
+ """Run agents sequentially."""
381
+ start_time = time.time()
382
+ results = {}
383
+
384
+ try:
385
+ order = agent_order or list(self._agents.keys())
386
+
387
+ for agent_name in order:
388
+ agent = self._agents.get(agent_name)
389
+ if agent:
390
+ result = await agent.run()
391
+ results[agent_name] = result
392
+
393
+ # Stop on first failure if needed
394
+ if not result.is_success:
395
+ logger.warning(f"Agent {agent_name} failed, continuing...")
396
+
397
+ end_time = time.time()
398
+
399
+ return TeamResult(
400
+ team_name=self.name,
401
+ status="completed",
402
+ agent_results=results,
403
+ start_time=start_time,
404
+ end_time=end_time,
405
+ total_duration=end_time - start_time,
406
+ )
407
+
408
+ except Exception as e:
409
+ end_time = time.time()
410
+ return TeamResult(
411
+ team_name=self.name,
412
+ status="failed",
413
+ agent_results=results,
414
+ error=e,
415
+ start_time=start_time,
416
+ end_time=end_time,
417
+ total_duration=end_time - start_time,
418
+ )
419
+
420
+ async def run_parallel_and_collect(self) -> TeamResult:
421
+ """Run in parallel and collect all results."""
422
+ return await self.run_parallel()
423
+
424
+ async def run_with_dependencies(self) -> TeamResult:
425
+ """Run agents respecting dependencies."""
426
+ start_time = time.time()
427
+ results = {}
428
+ completed: set[str] = set()
429
+ running: dict[str, Any] = {}
430
+
431
+ try:
432
+ while len(completed) < len(self._agents):
433
+ # Find agents ready to run
434
+ ready = []
435
+ for agent_name, _agent in self._agents.items():
436
+ if (
437
+ agent_name not in completed
438
+ and agent_name not in running
439
+ and self._dependencies[agent_name].issubset(completed)
440
+ ):
441
+ ready.append(agent_name)
442
+
443
+ if not ready:
444
+ # Wait for running agents
445
+ if running:
446
+ done_tasks = await asyncio.wait(
447
+ running.values(), return_when=asyncio.FIRST_COMPLETED
448
+ )
449
+
450
+ # Process completed agents
451
+ for task in done_tasks[0]:
452
+ for agent_name, agent_task in list(running.items()):
453
+ if agent_task == task:
454
+ result = await task
455
+ results[agent_name] = result
456
+ completed.add(agent_name)
457
+ del running[agent_name]
458
+ break
459
+ else:
460
+ break # No agents ready and none running
461
+ else:
462
+ # Start ready agents
463
+ for agent_name in ready:
464
+ agent = self._agents[agent_name]
465
+ task = asyncio.create_task(agent.run())
466
+ running[agent_name] = task
467
+
468
+ # Wait for remaining agents
469
+ if running:
470
+ remaining_results = await asyncio.gather(*running.values())
471
+ for i, (agent_name, _) in enumerate(running.items()):
472
+ results[agent_name] = remaining_results[i]
473
+ completed.add(agent_name)
474
+
475
+ end_time = time.time()
476
+
477
+ return TeamResult(
478
+ team_name=self.name,
479
+ status="completed",
480
+ agent_results=results,
481
+ start_time=start_time,
482
+ end_time=end_time,
483
+ total_duration=end_time - start_time,
484
+ )
485
+
486
+ except Exception as e:
487
+ end_time = time.time()
488
+ return TeamResult(
489
+ team_name=self.name,
490
+ status="failed",
491
+ agent_results=results,
492
+ error=e,
493
+ start_time=start_time,
494
+ end_time=end_time,
495
+ total_duration=end_time - start_time,
496
+ )
497
+
498
+ # Dependency management
499
+ def add_dependency(self, agent: str, depends_on: str) -> "AgentTeam":
500
+ """Add dependency between agents."""
501
+ self._dependencies[agent].add(depends_on)
502
+ return self
503
+
504
+ def set_execution_order(self, order: list[str]) -> "AgentTeam":
505
+ """Set execution order for sequential runs."""
506
+ self._execution_order = order
507
+ return self
508
+
509
+ # Monitoring and control
510
+ def get_status(self) -> dict[str, Any]:
511
+ """Get team status."""
512
+ agent_statuses = {
513
+ name: (
514
+ agent.status.value
515
+ if hasattr(agent.status, "value")
516
+ else str(agent.status)
517
+ )
518
+ for name, agent in self._agents.items()
519
+ }
520
+
521
+ return {
522
+ "team_name": self.name,
523
+ "agent_count": len(self._agents),
524
+ "agent_statuses": agent_statuses,
525
+ "shared_variables": len(self._shared_context),
526
+ "running": self._running,
527
+ }
528
+
529
+ async def pause_all(self) -> None:
530
+ """Pause all agents."""
531
+ for agent in self._agents.values():
532
+ await agent.pause()
533
+
534
+ async def resume_all(self) -> None:
535
+ """Resume all agents."""
536
+ for agent in self._agents.values():
537
+ await agent.resume()
538
+
539
+ async def cancel_all(self) -> None:
540
+ """Cancel all agents."""
541
+ for agent in self._agents.values():
542
+ await agent.cancel_all()
543
+
544
+ # Context manager support
545
+ async def __aenter__(self) -> "AgentTeam":
546
+ """Async context manager entry."""
547
+ self._running = True
548
+ return self
549
+
550
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
551
+ """Async context manager exit."""
552
+ self._running = False
553
+ await self.cancel_all()
554
+
555
+
556
+ # Helper functions for easy team creation
557
+ def create_team(name: str, agents: list[Agent]) -> AgentTeam:
558
+ """Create a team with agents."""
559
+ team = AgentTeam(name)
560
+ team.add_agents(agents)
561
+ return team
562
+
563
+
564
+ async def run_agents_parallel(
565
+ agents: list[Agent], timeout: Optional[float] = None
566
+ ) -> dict[str, AgentResult]:
567
+ """Run agents in parallel and return results."""
568
+ team = create_team("parallel_execution", agents)
569
+ result = await team.run_parallel(timeout)
570
+ return result.agent_results
571
+
572
+
573
+ async def run_agents_sequential(agents: list[Agent]) -> dict[str, AgentResult]:
574
+ """Run agents sequentially and return results."""
575
+ team = create_team("sequential_execution", agents)
576
+ result = await team.run_sequential()
577
+ return result.agent_results