pyagent-patterns 0.1.0__tar.gz → 0.2.0__tar.gz

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 (43) hide show
  1. pyagent_patterns-0.2.0/.gitignore +45 -0
  2. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/PKG-INFO +7 -1
  3. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/pyproject.toml +11 -1
  4. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/__init__.py +1 -1
  5. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/human_in_the_loop.py +1 -1
  6. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/react.py +2 -2
  7. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/swarm.py +8 -8
  8. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advisor.py +21 -14
  9. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/base.py +6 -3
  10. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/composite.py +8 -7
  11. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/guardrails.py +2 -1
  12. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/__init__.py +1 -1
  13. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/fan_out_fan_in.py +5 -3
  14. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/hierarchical.py +1 -3
  15. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/orchestrator_workers.py +1 -1
  16. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/pipeline.py +6 -3
  17. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/recovery.py +13 -10
  18. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/registry.py +3 -2
  19. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/__init__.py +1 -1
  20. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/cross_reflection.py +1 -0
  21. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/debate.py +9 -6
  22. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/evaluator_optimizer.py +1 -0
  23. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/self_reflection.py +1 -0
  24. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/resolution/voting.py +7 -7
  25. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/streaming.py +15 -12
  26. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/__init__.py +1 -1
  27. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/blackboard.py +1 -1
  28. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/layered.py +5 -8
  29. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/role_based.py +1 -1
  30. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/structural/topology.py +16 -12
  31. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_advanced.py +1 -2
  32. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_composite.py +6 -5
  33. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_orchestration.py +13 -10
  34. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_resolution.py +17 -7
  35. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_structural.py +7 -6
  36. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/test_valueadd.py +10 -6
  37. pyagent_patterns-0.1.0/.gitignore +0 -18
  38. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/README.md +0 -0
  39. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/__init__.py +1 -1
  40. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/advanced/talker_reasoner.py +0 -0
  41. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/orchestration/supervisor.py +0 -0
  42. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/src/pyagent_patterns/py.typed +0 -0
  43. {pyagent_patterns-0.1.0 → pyagent_patterns-0.2.0}/tests/__init__.py +0 -0
@@ -0,0 +1,45 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .eggs/
11
+ *.whl
12
+
13
+ # uv
14
+ .uv/
15
+ .venv/
16
+ uv.lock
17
+
18
+ # Environments
19
+ .env
20
+ .envrc
21
+ *.env
22
+
23
+ # Testing
24
+ .pytest_cache/
25
+ .coverage
26
+ htmlcov/
27
+ .tox/
28
+
29
+ # Type checking
30
+ .mypy_cache/
31
+ .dmypy.json
32
+
33
+ # Docs
34
+ site/
35
+ .mkdocs_temp/
36
+
37
+ # Editors
38
+ .vscode/
39
+ .idea/
40
+ *.swp
41
+ *.swo
42
+
43
+ # OS
44
+ .DS_Store
45
+ Thumbs.db
@@ -1,7 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagent-patterns
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: 18 reusable multi-agent orchestration patterns for LLMs
5
+ Project-URL: Homepage, https://github.com/pyagent-core/pyagent
6
+ Project-URL: Source, https://github.com/pyagent-core/pyagent/tree/main/packages/pyagent-patterns
7
+ Project-URL: Documentation, https://pyagent-core.github.io/pyagent
8
+ Project-URL: Issues, https://github.com/pyagent-core/pyagent/issues
9
+ Project-URL: Changelog, https://github.com/pyagent-core/pyagent/blob/main/CHANGELOG.md
10
+ Author: PyAgent Contributors
5
11
  License: MIT
6
12
  Keywords: LLM,agents,multi-agent,orchestration,patterns
7
13
  Classifier: Development Status :: 3 - Alpha
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyagent-patterns"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "18 reusable multi-agent orchestration patterns for LLMs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,6 +20,16 @@ classifiers = [
20
20
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
21
  "Typing :: Typed",
22
22
  ]
23
+ authors = [
24
+ {name = "PyAgent Contributors"},
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/pyagent-core/pyagent"
29
+ Source = "https://github.com/pyagent-core/pyagent/tree/main/packages/pyagent-patterns"
30
+ Documentation = "https://pyagent-core.github.io/pyagent"
31
+ Issues = "https://github.com/pyagent-core/pyagent/issues"
32
+ Changelog = "https://github.com/pyagent-core/pyagent/blob/main/CHANGELOG.md"
23
33
 
24
34
  [project.optional-dependencies]
25
35
  dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff>=0.5", "mypy>=1.10"]
@@ -5,4 +5,4 @@ from pyagent_patterns.advanced.react import ReAct
5
5
  from pyagent_patterns.advanced.swarm import Swarm
6
6
  from pyagent_patterns.advanced.talker_reasoner import TalkerReasoner
7
7
 
8
- __all__ = ["TalkerReasoner", "Swarm", "HumanInTheLoop", "ReAct"]
8
+ __all__ = ["HumanInTheLoop", "ReAct", "Swarm", "TalkerReasoner"]
@@ -8,7 +8,7 @@ LLM calls: 1 agent + optional revision calls
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Callable
11
+ from collections.abc import Callable
12
12
 
13
13
  from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
14
14
 
@@ -9,11 +9,11 @@ LLM calls: 1 per step × max_steps
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from typing import Any, Callable
12
+ from collections.abc import Callable
13
+ from typing import Any
13
14
 
14
15
  from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
15
16
 
16
-
17
17
  # Tool function type: (action_input: str) -> str
18
18
  ToolFn = Callable[[str], str]
19
19
 
@@ -51,32 +51,32 @@ class Swarm(Pattern):
51
51
  # Round 0: each agent independently responds to the task
52
52
  init_tasks = [agent.run([Message.user(ctx.task)]) for agent in self._agents]
53
53
  init_results = await asyncio.gather(*init_tasks)
54
- for agent, result in zip(self._agents, init_results):
54
+ for agent, result in zip(self._agents, init_results, strict=False):
55
55
  states[agent.name] = result.content
56
56
  messages.append(result)
57
57
 
58
58
  # Subsequent rounds: agents interact with random neighbors
59
- for round_num in range(1, self._rounds + 1):
59
+ for _round_num in range(1, self._rounds + 1):
60
60
  new_states: dict[str, str] = {}
61
61
 
62
- async def _update_agent(agent: Agent) -> tuple[str, str]:
62
+ async def _update_agent(
63
+ agent: Agent, _states: dict[str, str] = states
64
+ ) -> tuple[str, str]:
63
65
  # Select random neighbors
64
66
  others = [a for a in self._agents if a.name != agent.name]
65
67
  neighbors = random.sample(others, min(self._neighbor_count, len(others)))
66
68
 
67
- neighbor_views = "\n".join(
68
- f"- {n.name}: {states[n.name]}" for n in neighbors
69
- )
69
+ neighbor_views = "\n".join(f"- {n.name}: {_states[n.name]}" for n in neighbors)
70
70
  prompt = Message.user(
71
71
  f"Task: {ctx.task}\n\n"
72
- f"Your current response: {states[agent.name]}\n\n"
72
+ f"Your current response: {_states[agent.name]}\n\n"
73
73
  f"Neighbor responses:\n{neighbor_views}\n\n"
74
74
  f"Update your response considering your neighbors' views."
75
75
  )
76
76
  result = await agent.run([prompt])
77
77
  return agent.name, result.content
78
78
 
79
- update_tasks = [_update_agent(agent) for agent in self._agents]
79
+ update_tasks = [_update_agent(agent, states) for agent in self._agents]
80
80
  updates = await asyncio.gather(*update_tasks)
81
81
  for name, content in updates:
82
82
  new_states[name] = content
@@ -13,17 +13,17 @@ Decision factors:
13
13
  from __future__ import annotations
14
14
 
15
15
  from dataclasses import dataclass
16
- from enum import Enum
16
+ from enum import StrEnum
17
17
 
18
18
 
19
- class Quality(str, Enum):
19
+ class Quality(StrEnum):
20
20
  DRAFT = "draft"
21
21
  STANDARD = "standard"
22
22
  HIGH = "high"
23
23
  CRITICAL = "critical"
24
24
 
25
25
 
26
- class Latency(str, Enum):
26
+ class Latency(StrEnum):
27
27
  REALTIME = "realtime" # < 2s
28
28
  INTERACTIVE = "interactive" # < 10s
29
29
  BATCH = "batch" # minutes OK
@@ -68,15 +68,18 @@ class PatternAdvisor:
68
68
  # Decision tree based on Augment 2026 "Five Decision Rules"
69
69
 
70
70
  # Rule 1: Simple single-step tasks → Pipeline or single agent
71
- if not c.multi_step and c.quality in (Quality.DRAFT, Quality.STANDARD):
72
- if c.latency == Latency.REALTIME:
73
- return Recommendation(
74
- pattern="pipeline",
75
- reason="Simple task with real-time latency → minimal sequential processing",
76
- estimated_calls=1,
77
- estimated_cost_range="$0.001-0.003",
78
- alternatives=["talker_reasoner"],
79
- )
71
+ if (
72
+ not c.multi_step
73
+ and c.quality in (Quality.DRAFT, Quality.STANDARD)
74
+ and c.latency == Latency.REALTIME
75
+ ):
76
+ return Recommendation(
77
+ pattern="pipeline",
78
+ reason="Simple task with real-time latency → minimal sequential processing",
79
+ estimated_calls=1,
80
+ estimated_cost_range="$0.001-0.003",
81
+ alternatives=["talker_reasoner"],
82
+ )
80
83
 
81
84
  # Rule 2: Cost-sensitive → Route to cheapest viable model
82
85
  if c.max_cost_usd < 0.01:
@@ -101,7 +104,9 @@ class PatternAdvisor:
101
104
  # Rule 4: High quality → Reflection, Debate, or Evaluator
102
105
  if c.quality in (Quality.HIGH, Quality.CRITICAL):
103
106
  # Check for adversarial/debate keywords
104
- if any(w in task_lower for w in ["compare", "pros and cons", "debate", "argue", "versus"]):
107
+ if any(
108
+ w in task_lower for w in ["compare", "pros and cons", "debate", "argue", "versus"]
109
+ ):
105
110
  return Recommendation(
106
111
  pattern="debate",
107
112
  reason="High quality + adversarial task → structured debate with judge",
@@ -129,7 +134,9 @@ class PatternAdvisor:
129
134
  )
130
135
 
131
136
  # Rule 5: Multi-step/complex → Supervisor, Hierarchical, or Pipeline
132
- if c.multi_step or any(w in task_lower for w in ["steps", "process", "workflow", "pipeline"]):
137
+ if c.multi_step or any(
138
+ w in task_lower for w in ["steps", "process", "workflow", "pipeline"]
139
+ ):
133
140
  if any(w in task_lower for w in ["team", "delegate", "manage", "coordinate"]):
134
141
  return Recommendation(
135
142
  pattern="hierarchical",
@@ -7,11 +7,14 @@ import time
7
7
  import uuid
8
8
  from abc import ABC, abstractmethod
9
9
  from dataclasses import dataclass, field
10
- from enum import Enum
11
- from typing import Any, AsyncIterator, Callable, Protocol, runtime_checkable
10
+ from enum import StrEnum
11
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
12
12
 
13
+ if TYPE_CHECKING:
14
+ from collections.abc import AsyncIterator
13
15
 
14
- class Role(str, Enum):
16
+
17
+ class Role(StrEnum):
15
18
  """Well-known agent roles."""
16
19
 
17
20
  SYSTEM = "system"
@@ -8,11 +8,10 @@ EscalationChain is a pre-built composite: Reflection → Debate → Voting → H
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Callable
11
+ from collections.abc import Callable
12
12
 
13
13
  from pyagent_patterns.base import Context, Pattern, Result
14
14
 
15
-
16
15
  # Quality check: returns True if the result is acceptable
17
16
  QualityCheckFn = Callable[[Result], bool]
18
17
 
@@ -68,11 +67,13 @@ class CompositePattern(Pattern):
68
67
 
69
68
  result = await pattern._execute(child_ctx)
70
69
  all_messages.extend(result.messages)
71
- escalation_log.append({
72
- "pattern": pattern.pattern_type,
73
- "output_length": len(result.output),
74
- "metadata": result.metadata,
75
- })
70
+ escalation_log.append(
71
+ {
72
+ "pattern": pattern.pattern_type,
73
+ "output_length": len(result.output),
74
+ "metadata": result.metadata,
75
+ }
76
+ )
76
77
 
77
78
  if self._quality_check(result):
78
79
  return Result(
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
  import re
16
16
  from abc import ABC, abstractmethod
17
17
  from dataclasses import dataclass
18
+ from typing import ClassVar
18
19
 
19
20
 
20
21
  @dataclass
@@ -71,7 +72,7 @@ class PIIGuard(Guardrail):
71
72
  redact: If True, redact PII and pass. If False, reject on detection.
72
73
  """
73
74
 
74
- _PATTERNS = {
75
+ _PATTERNS: ClassVar[dict[str, str]] = {
75
76
  "email": r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
76
77
  "phone": r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b",
77
78
  "ssn": r"\b\d{3}[-]?\d{2}[-]?\d{4}\b",
@@ -6,4 +6,4 @@ from pyagent_patterns.orchestration.orchestrator_workers import OrchestratorWork
6
6
  from pyagent_patterns.orchestration.pipeline import Pipeline
7
7
  from pyagent_patterns.orchestration.supervisor import Supervisor
8
8
 
9
- __all__ = ["Supervisor", "Pipeline", "FanOutFanIn", "Hierarchical", "OrchestratorWorkers"]
9
+ __all__ = ["FanOutFanIn", "Hierarchical", "OrchestratorWorkers", "Pipeline", "Supervisor"]
@@ -10,10 +10,13 @@ Wall-clock latency: max(agent latencies) + aggregator
10
10
  from __future__ import annotations
11
11
 
12
12
  import asyncio
13
- from typing import AsyncIterator
13
+ from typing import TYPE_CHECKING
14
14
 
15
15
  from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
16
16
 
17
+ if TYPE_CHECKING:
18
+ from collections.abc import AsyncIterator
19
+
17
20
 
18
21
  class FanOutFanIn(Pattern):
19
22
  """Parallel execution with result aggregation.
@@ -43,8 +46,7 @@ class FanOutFanIn(Pattern):
43
46
 
44
47
  # Fan-in: aggregate results
45
48
  combined = "\n\n".join(
46
- f"--- {self._agents[i].name} ---\n{r.content}"
47
- for i, r in enumerate(parallel_results)
49
+ f"--- {self._agents[i].name} ---\n{r.content}" for i, r in enumerate(parallel_results)
48
50
  )
49
51
  agg_prompt = Message.user(
50
52
  f"Combine the following analyses into a unified response:\n\n{combined}"
@@ -75,9 +75,7 @@ class Hierarchical(Pattern):
75
75
  worker_summary = "\n".join(
76
76
  f"- {team.workers[i].name}: {r.content}" for i, r in enumerate(worker_results)
77
77
  )
78
- lead_msg = Message.user(
79
- f"Synthesize your team's work:\n{worker_summary}"
80
- )
78
+ lead_msg = Message.user(f"Synthesize your team's work:\n{worker_summary}")
81
79
  lead_result = await team.lead.run([lead_msg])
82
80
  team_msgs.append(lead_result)
83
81
  return lead_result.content, team_msgs
@@ -38,7 +38,7 @@ class OrchestratorWorkers(Pattern):
38
38
  plan_prompt = Message.user(
39
39
  f"You have these workers available: {', '.join(worker_names)}.\n"
40
40
  f"Plan how to accomplish this task by assigning subtasks to workers.\n"
41
- f"Respond as JSON: {{\"assignments\": [{{\"worker\": \"name\", \"subtask\": \"description\"}}]}}\n\n"
41
+ f'Respond as JSON: {{"assignments": [{{"worker": "name", "subtask": "description"}}]}}\n\n'
42
42
  f"Task: {ctx.task}"
43
43
  )
44
44
  plan_msg = await self._orchestrator.run([plan_prompt])
@@ -8,10 +8,13 @@ LLM calls: N (one per stage)
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import AsyncIterator
11
+ from typing import TYPE_CHECKING
12
12
 
13
13
  from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
14
14
 
15
+ if TYPE_CHECKING:
16
+ from collections.abc import AsyncIterator
17
+
15
18
 
16
19
  class Pipeline(Pattern):
17
20
  """Sequential stage chain — output of stage N becomes input of stage N+1.
@@ -33,7 +36,7 @@ class Pipeline(Pattern):
33
36
  messages: list[Message] = []
34
37
  current_input = ctx.task
35
38
 
36
- for i, stage in enumerate(self._stages):
39
+ for _i, stage in enumerate(self._stages):
37
40
  stage_msg = Message.user(current_input)
38
41
  response = await stage.run([stage_msg])
39
42
  messages.append(response)
@@ -47,7 +50,7 @@ class Pipeline(Pattern):
47
50
 
48
51
  async def stream(self, task: str, context: Context | None = None) -> AsyncIterator[str]:
49
52
  """Stream stage completions as they finish."""
50
- ctx = context or Context(task=task)
53
+ context or Context(task=task)
51
54
  current_input = task
52
55
 
53
56
  for i, stage in enumerate(self._stages):
@@ -11,14 +11,13 @@ from __future__ import annotations
11
11
 
12
12
  import asyncio
13
13
  import time
14
- from dataclasses import dataclass, field
15
- from enum import Enum
16
- from typing import Any
14
+ from dataclasses import dataclass
15
+ from enum import StrEnum
17
16
 
18
17
  from pyagent_patterns.base import Context, Pattern, Result
19
18
 
20
19
 
21
- class CircuitState(str, Enum):
20
+ class CircuitState(StrEnum):
22
21
  CLOSED = "closed" # Normal operation
23
22
  OPEN = "open" # Failing, rejecting requests
24
23
  HALF_OPEN = "half_open" # Testing if recovered
@@ -69,10 +68,12 @@ class BoundedExecution:
69
68
  result.metadata["attempts"] = attempt
70
69
  return result
71
70
  # Token limit exceeded — try next level
72
- last_error = Exception(f"Token limit exceeded: {result.token_estimate}/{self.max_tokens}")
71
+ last_error = Exception(
72
+ f"Token limit exceeded: {result.token_estimate}/{self.max_tokens}"
73
+ )
73
74
  break
74
- except asyncio.TimeoutError:
75
- last_error = asyncio.TimeoutError(f"Timeout after {self.timeout_seconds}s")
75
+ except TimeoutError:
76
+ last_error = TimeoutError(f"Timeout after {self.timeout_seconds}s")
76
77
  break
77
78
  except Exception as e:
78
79
  last_error = e
@@ -131,9 +132,11 @@ class CircuitBreaker:
131
132
 
132
133
  @property
133
134
  def state(self) -> CircuitState:
134
- if self._state == CircuitState.OPEN:
135
- if time.time() - self._last_failure_time > self._reset_timeout:
136
- self._state = CircuitState.HALF_OPEN
135
+ if (
136
+ self._state == CircuitState.OPEN
137
+ and time.time() - self._last_failure_time > self._reset_timeout
138
+ ):
139
+ self._state = CircuitState.HALF_OPEN
137
140
  return self._state
138
141
 
139
142
  async def execute(self, pattern: Pattern, task: str, context: Context | None = None) -> Result:
@@ -2,9 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
5
+ from typing import TYPE_CHECKING
6
6
 
7
- from pyagent_patterns.base import Pattern
7
+ if TYPE_CHECKING:
8
+ from pyagent_patterns.base import Pattern
8
9
 
9
10
  _REGISTRY: dict[str, type[Pattern]] = {}
10
11
 
@@ -6,4 +6,4 @@ from pyagent_patterns.resolution.evaluator_optimizer import EvaluatorOptimizer
6
6
  from pyagent_patterns.resolution.self_reflection import SelfReflection
7
7
  from pyagent_patterns.resolution.voting import Voting
8
8
 
9
- __all__ = ["SelfReflection", "CrossReflection", "Debate", "Voting", "EvaluatorOptimizer"]
9
+ __all__ = ["CrossReflection", "Debate", "EvaluatorOptimizer", "SelfReflection", "Voting"]
@@ -40,6 +40,7 @@ class CrossReflection(Pattern):
40
40
  async def _execute(self, ctx: Context) -> Result:
41
41
  messages: list[Message] = []
42
42
  current_output = ""
43
+ review_text = ""
43
44
 
44
45
  for round_num in range(1, self._max_rounds + 1):
45
46
  # Generate or revise
@@ -43,6 +43,7 @@ class Debate(Pattern):
43
43
  async def _execute(self, ctx: Context) -> Result:
44
44
  messages: list[Message] = []
45
45
  debate_log: list[dict[str, str]] = []
46
+ round_args_prev: list[str] = []
46
47
 
47
48
  for round_num in range(1, self._rounds + 1):
48
49
  round_args: list[str] = []
@@ -69,12 +70,14 @@ class Debate(Pattern):
69
70
  arg_result = await debater.run([prompt])
70
71
  messages.append(arg_result)
71
72
  round_args.append(arg_result.content)
72
- debate_log.append({
73
- "round": round_num,
74
- "debater": debater.name,
75
- "position": position,
76
- "argument": arg_result.content,
77
- })
73
+ debate_log.append(
74
+ {
75
+ "round": round_num,
76
+ "debater": debater.name,
77
+ "position": position,
78
+ "argument": arg_result.content,
79
+ }
80
+ )
78
81
 
79
82
  round_args_prev = round_args
80
83
 
@@ -44,6 +44,7 @@ class EvaluatorOptimizer(Pattern):
44
44
  messages: list[Message] = []
45
45
  current_output = ""
46
46
  scores: list[int] = []
47
+ eval_text = ""
47
48
 
48
49
  criteria_text = "\n".join(f"- {c}" for c in self._criteria)
49
50
 
@@ -40,6 +40,7 @@ class SelfReflection(Pattern):
40
40
  async def _execute(self, ctx: Context) -> Result:
41
41
  messages: list[Message] = []
42
42
  current_output = ""
43
+ critique_text = ""
43
44
 
44
45
  for round_num in range(1, self._max_rounds + 1):
45
46
  # Generate (or refine)
@@ -10,12 +10,12 @@ from __future__ import annotations
10
10
 
11
11
  import asyncio
12
12
  from collections import Counter
13
- from enum import Enum
13
+ from enum import StrEnum
14
14
 
15
15
  from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
16
16
 
17
17
 
18
- class VotingStrategy(str, Enum):
18
+ class VotingStrategy(StrEnum):
19
19
  MAJORITY = "majority"
20
20
  WEIGHTED = "weighted"
21
21
 
@@ -55,12 +55,12 @@ class Voting(Pattern):
55
55
 
56
56
  suffix = ""
57
57
  if self._normalize:
58
- suffix = "\n\nRespond with a concise answer (one word or short phrase) first, then explain."
58
+ suffix = (
59
+ "\n\nRespond with a concise answer (one word or short phrase) first, then explain."
60
+ )
59
61
 
60
62
  # All voters run in parallel
61
- tasks = [
62
- voter.run([Message.user(ctx.task + suffix)]) for voter in self._voters
63
- ]
63
+ tasks = [voter.run([Message.user(ctx.task + suffix)]) for voter in self._voters]
64
64
  votes = await asyncio.gather(*tasks)
65
65
  messages.extend(votes)
66
66
 
@@ -75,7 +75,7 @@ class Voting(Pattern):
75
75
  else:
76
76
  # Weighted voting
77
77
  weighted_counts: dict[str, float] = {}
78
- for label, weight in zip(vote_labels, self._weights):
78
+ for label, weight in zip(vote_labels, self._weights, strict=False):
79
79
  weighted_counts[label] = weighted_counts.get(label, 0.0) + weight
80
80
  winner = max(weighted_counts, key=weighted_counts.get) # type: ignore[arg-type]
81
81
  tally = weighted_counts
@@ -12,9 +12,12 @@ from __future__ import annotations
12
12
 
13
13
  import asyncio
14
14
  from dataclasses import dataclass, field
15
- from typing import Any, AsyncIterator
15
+ from typing import TYPE_CHECKING, Any
16
16
 
17
- from pyagent_patterns.base import Pattern, Result
17
+ if TYPE_CHECKING:
18
+ from collections.abc import AsyncIterator
19
+
20
+ from pyagent_patterns.base import Pattern
18
21
 
19
22
 
20
23
  @dataclass
@@ -36,8 +39,8 @@ async def stream_pattern(pattern: Pattern, task: str) -> AsyncIterator[StreamChu
36
39
  For Pipeline patterns, yields after each stage completes.
37
40
  For Fan-Out, yields as each parallel agent completes.
38
41
  """
39
- from pyagent_patterns.orchestration.pipeline import Pipeline
40
42
  from pyagent_patterns.orchestration.fan_out_fan_in import FanOutFanIn
43
+ from pyagent_patterns.orchestration.pipeline import Pipeline
41
44
 
42
45
  if isinstance(pattern, Pipeline):
43
46
  async for chunk in _stream_pipeline(pattern, task):
@@ -87,12 +90,14 @@ async def _stream_fanout(fanout: Pattern, task: str) -> AsyncIterator[StreamChun
87
90
  if agent.system_prompt:
88
91
  messages.insert(0, Message(role=Role.SYSTEM, content=agent.system_prompt))
89
92
  response = await agent.llm.generate(messages)
90
- await queue.put(StreamChunk(
91
- content=response,
92
- agent_name=agent.name,
93
- chunk_type="stage_complete",
94
- metadata={"agent_index": idx},
95
- ))
93
+ await queue.put(
94
+ StreamChunk(
95
+ content=response,
96
+ agent_name=agent.name,
97
+ chunk_type="stage_complete",
98
+ metadata={"agent_index": idx},
99
+ )
100
+ )
96
101
 
97
102
  tasks = [asyncio.create_task(run_agent(a, i)) for i, a in enumerate(fanout._agents)]
98
103
  done_count = 0
@@ -107,9 +112,7 @@ async def _stream_fanout(fanout: Pattern, task: str) -> AsyncIterator[StreamChun
107
112
  await asyncio.gather(*tasks)
108
113
 
109
114
  # Aggregate
110
- aggregated_input = "\n".join(
111
- f"[{a.name}]: (see prior chunks)" for a in fanout._agents
112
- )
115
+ "\n".join(f"[{a.name}]: (see prior chunks)" for a in fanout._agents)
113
116
  result = await fanout.run(task)
114
117
  yield StreamChunk(
115
118
  content=result.output,
@@ -5,4 +5,4 @@ from pyagent_patterns.structural.layered import Layered
5
5
  from pyagent_patterns.structural.role_based import RoleBased
6
6
  from pyagent_patterns.structural.topology import Topology, TopologyType
7
7
 
8
- __all__ = ["RoleBased", "Layered", "Topology", "TopologyType", "Blackboard"]
8
+ __all__ = ["Blackboard", "Layered", "RoleBased", "Topology", "TopologyType"]
@@ -103,7 +103,7 @@ class Blackboard(Pattern):
103
103
  for key, value in self._initial_state.items():
104
104
  board.write(key, value, "system")
105
105
 
106
- for round_num in range(1, self._rounds + 1):
106
+ for _round_num in range(1, self._rounds + 1):
107
107
  for ba in self._agents:
108
108
  # Build prompt from readable keys
109
109
  readable = {k: board.read(k) for k in ba.reads if board.read(k) is not None}
@@ -42,11 +42,9 @@ class Layered(Pattern):
42
42
  messages: list[Message] = []
43
43
  layer_input = ctx.task
44
44
 
45
- for i, layer in enumerate(self._layers):
45
+ for _i, layer in enumerate(self._layers):
46
46
  # Run all agents in this layer in parallel
47
- tasks = [
48
- agent.run([Message.user(layer_input)]) for agent in layer.agents
49
- ]
47
+ tasks = [agent.run([Message.user(layer_input)]) for agent in layer.agents]
50
48
  layer_results = await asyncio.gather(*tasks)
51
49
  messages.extend(layer_results)
52
50
 
@@ -55,8 +53,7 @@ class Layered(Pattern):
55
53
  layer_input = layer_results[0].content
56
54
  else:
57
55
  layer_input = "\n\n".join(
58
- f"[{layer.agents[j].name}]: {r.content}"
59
- for j, r in enumerate(layer_results)
56
+ f"[{layer.agents[j].name}]: {r.content}" for j, r in enumerate(layer_results)
60
57
  )
61
58
 
62
59
  return Result(
@@ -64,7 +61,7 @@ class Layered(Pattern):
64
61
  messages=messages,
65
62
  metadata={
66
63
  "layer_count": len(self._layers),
67
- "layer_names": [l.name for l in self._layers],
68
- "agents_per_layer": [len(l.agents) for l in self._layers],
64
+ "layer_names": [layer_.name for layer_ in self._layers],
65
+ "agents_per_layer": [len(layer_.agents) for layer_ in self._layers],
69
66
  },
70
67
  )
@@ -39,7 +39,7 @@ class RoleBased(Pattern):
39
39
  messages: list[Message] = []
40
40
  conversation: list[Message] = [Message.user(ctx.task)]
41
41
 
42
- for round_num in range(1, self._rounds + 1):
42
+ for _round_num in range(1, self._rounds + 1):
43
43
  for agent in self._agents:
44
44
  if self._shared_context:
45
45
  input_msgs = list(conversation)
@@ -11,12 +11,12 @@ flow, overhead, and result quality.
11
11
  from __future__ import annotations
12
12
 
13
13
  import asyncio
14
- from enum import Enum
14
+ from enum import StrEnum
15
15
 
16
16
  from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
17
17
 
18
18
 
19
- class TopologyType(str, Enum):
19
+ class TopologyType(StrEnum):
20
20
  CHAIN = "chain"
21
21
  STAR = "star"
22
22
  MESH = "mesh"
@@ -82,12 +82,10 @@ class Topology(Pattern):
82
82
  messages.extend(spoke_results)
83
83
 
84
84
  # Hub synthesizes
85
- summary = "\n".join(
86
- f"- {spokes[i].name}: {r.content}" for i, r in enumerate(spoke_results)
85
+ summary = "\n".join(f"- {spokes[i].name}: {r.content}" for i, r in enumerate(spoke_results))
86
+ hub_result = await hub.run(
87
+ [Message.user(f"Synthesize these inputs:\n{summary}\n\nOriginal task: {ctx.task}")]
87
88
  )
88
- hub_result = await hub.run([Message.user(
89
- f"Synthesize these inputs:\n{summary}\n\nOriginal task: {ctx.task}"
90
- )])
91
89
  messages.append(hub_result)
92
90
 
93
91
  return Result(output=hub_result.content, messages=messages, metadata={"topology": "star"})
@@ -100,7 +98,7 @@ class Topology(Pattern):
100
98
  # Initial round
101
99
  tasks = [a.run([Message.user(ctx.task)]) for a in self._agents]
102
100
  initial = await asyncio.gather(*tasks)
103
- for agent, result in zip(self._agents, initial):
101
+ for agent, result in zip(self._agents, initial, strict=False):
104
102
  outputs[agent.name] = result.content
105
103
  messages.append(result)
106
104
 
@@ -112,12 +110,18 @@ class Topology(Pattern):
112
110
  for name, content in outputs.items()
113
111
  if name != agent.name
114
112
  )
115
- result = await agent.run([Message.user(
116
- f"Task: {ctx.task}\n\nPeer outputs:\n{peer_outputs}\n\nProvide your updated response."
117
- )])
113
+ result = await agent.run(
114
+ [
115
+ Message.user(
116
+ f"Task: {ctx.task}\n\nPeer outputs:\n{peer_outputs}\n\nProvide your updated response."
117
+ )
118
+ ]
119
+ )
118
120
  outputs[agent.name] = result.content
119
121
  messages.append(result)
120
122
 
121
123
  # Final output is concatenation of all agent outputs
122
124
  final = "\n\n".join(f"[{name}]: {content}" for name, content in outputs.items())
123
- return Result(output=final, messages=messages, metadata={"topology": "mesh", "rounds": self._rounds})
125
+ return Result(
126
+ output=final, messages=messages, metadata={"topology": "mesh", "rounds": self._rounds}
127
+ )
@@ -3,10 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
-
7
- from pyagent_patterns.base import Agent, MockLLM
8
6
  from pyagent_patterns.advanced import HumanInTheLoop, ReAct, Swarm, TalkerReasoner
9
7
  from pyagent_patterns.advanced.human_in_the_loop import HumanDecision
8
+ from pyagent_patterns.base import Agent, MockLLM
10
9
 
11
10
 
12
11
  @pytest.mark.asyncio
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
-
7
6
  from pyagent_patterns.base import Agent, MockLLM
8
7
  from pyagent_patterns.composite import CompositePattern, min_length_check
9
8
  from pyagent_patterns.resolution import SelfReflection, Voting
@@ -32,10 +31,12 @@ async def test_composite_escalation():
32
31
  reflection = SelfReflection(agent=Agent("coder", short_llm), max_rounds=1)
33
32
 
34
33
  # Second pattern produces adequate output
35
- vote_llm = MockLLM(responses=[
36
- "A much longer and more detailed response that passes the length check",
37
- "A much longer and more detailed response that passes the length check",
38
- ])
34
+ vote_llm = MockLLM(
35
+ responses=[
36
+ "A much longer and more detailed response that passes the length check",
37
+ "A much longer and more detailed response that passes the length check",
38
+ ]
39
+ )
39
40
  voting = Voting(voters=[Agent("a", vote_llm), Agent("b", vote_llm)])
40
41
 
41
42
  composite = CompositePattern(
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
-
7
6
  from pyagent_patterns.base import Agent, MockLLM
8
7
  from pyagent_patterns.orchestration import (
9
8
  FanOutFanIn,
@@ -39,11 +38,13 @@ async def test_supervisor_routes_correctly():
39
38
  async def test_pipeline_sequential_stages():
40
39
  stage_llm = MockLLM(responses=["extracted text", "summarized text", "translated text"])
41
40
 
42
- pipeline = Pipeline(stages=[
43
- Agent("extract", stage_llm),
44
- Agent("summarize", stage_llm),
45
- Agent("translate", stage_llm),
46
- ])
41
+ pipeline = Pipeline(
42
+ stages=[
43
+ Agent("extract", stage_llm),
44
+ Agent("summarize", stage_llm),
45
+ Agent("translate", stage_llm),
46
+ ]
47
+ )
47
48
 
48
49
  result = await pipeline.run("Process this document")
49
50
  assert result.metadata["stages"] == 3
@@ -90,10 +91,12 @@ async def test_hierarchical_delegation():
90
91
 
91
92
  @pytest.mark.asyncio
92
93
  async def test_orchestrator_workers_dynamic():
93
- orch_llm = MockLLM(responses=[
94
- '{"assignments": [{"worker": "researcher", "subtask": "Find data"}]}',
95
- "Synthesized: research complete",
96
- ])
94
+ orch_llm = MockLLM(
95
+ responses=[
96
+ '{"assignments": [{"worker": "researcher", "subtask": "Find data"}]}',
97
+ "Synthesized: research complete",
98
+ ]
99
+ )
97
100
  worker_llm = MockLLM(responses=["Data found on topic X"])
98
101
 
99
102
  orch = OrchestratorWorkers(
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
-
7
6
  from pyagent_patterns.base import Agent, MockLLM
8
7
  from pyagent_patterns.resolution import (
9
8
  CrossReflection,
@@ -25,7 +24,16 @@ async def test_self_reflection_early_stop():
25
24
 
26
25
  @pytest.mark.asyncio
27
26
  async def test_self_reflection_max_rounds():
28
- llm = MockLLM(responses=["Draft 1", "Needs work", "Draft 2", "Still needs work", "Draft 3", "More feedback"])
27
+ llm = MockLLM(
28
+ responses=[
29
+ "Draft 1",
30
+ "Needs work",
31
+ "Draft 2",
32
+ "Still needs work",
33
+ "Draft 3",
34
+ "More feedback",
35
+ ]
36
+ )
29
37
  pattern = SelfReflection(agent=Agent("coder", llm), max_rounds=3)
30
38
  result = await pattern.run("Write code")
31
39
  assert result.metadata["rounds"] == 3
@@ -69,11 +77,13 @@ async def test_voting_majority():
69
77
  llm_b = MockLLM(responses=["YES\nBecause of Y"])
70
78
  llm_c = MockLLM(responses=["NO\nBecause of Z"])
71
79
 
72
- pattern = Voting(voters=[
73
- Agent("voter_a", llm_a),
74
- Agent("voter_b", llm_b),
75
- Agent("voter_c", llm_c),
76
- ])
80
+ pattern = Voting(
81
+ voters=[
82
+ Agent("voter_a", llm_a),
83
+ Agent("voter_b", llm_b),
84
+ Agent("voter_c", llm_c),
85
+ ]
86
+ )
77
87
  result = await pattern.run("Is this a good idea?")
78
88
  assert result.metadata["winner"] == "YES"
79
89
  assert result.metadata["tally"]["YES"] == 2
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
-
7
6
  from pyagent_patterns.base import Agent, MockLLM
8
7
  from pyagent_patterns.structural import Blackboard, Layered, RoleBased, Topology, TopologyType
9
8
  from pyagent_patterns.structural.blackboard import BlackboardAgent
@@ -29,11 +28,13 @@ async def test_role_based_shared_context():
29
28
  async def test_layered_multi_layer():
30
29
  llm = MockLLM(responses=["Raw data", "Analysis result", "Final synthesis"])
31
30
 
32
- pattern = Layered(layers=[
33
- Layer(name="gather", agents=[Agent("gatherer", llm)]),
34
- Layer(name="analyze", agents=[Agent("analyst", llm)]),
35
- Layer(name="synthesize", agents=[Agent("synthesizer", llm)]),
36
- ])
31
+ pattern = Layered(
32
+ layers=[
33
+ Layer(name="gather", agents=[Agent("gatherer", llm)]),
34
+ Layer(name="analyze", agents=[Agent("analyst", llm)]),
35
+ Layer(name="synthesize", agents=[Agent("synthesizer", llm)]),
36
+ ]
37
+ )
37
38
  result = await pattern.run("Analyze the market")
38
39
  assert result.metadata["layer_count"] == 3
39
40
  assert result.output == "Final synthesis"
@@ -3,16 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import pytest
6
-
7
6
  from pyagent_patterns.advisor import Constraints, Latency, PatternAdvisor, Quality
8
7
  from pyagent_patterns.base import Agent, MockLLM
9
8
  from pyagent_patterns.guardrails import ContentGuard, GuardrailChain, LengthGuard, PIIGuard
10
9
  from pyagent_patterns.orchestration import Pipeline
11
10
  from pyagent_patterns.recovery import BoundedExecution, CircuitBreaker, CircuitState
12
11
 
13
-
14
12
  # --- Recovery Tests ---
15
13
 
14
+
16
15
  @pytest.mark.asyncio
17
16
  async def test_bounded_execution_success():
18
17
  llm = MockLLM(responses=["Success output"])
@@ -50,6 +49,7 @@ def test_circuit_breaker_resets():
50
49
  cb._on_failure()
51
50
  assert cb.state == CircuitState.OPEN
52
51
  import time
52
+
53
53
  time.sleep(0.02)
54
54
  assert cb.state == CircuitState.HALF_OPEN
55
55
  cb._on_success()
@@ -58,6 +58,7 @@ def test_circuit_breaker_resets():
58
58
 
59
59
  # --- Guardrail Tests ---
60
60
 
61
+
61
62
  def test_length_guard_pass():
62
63
  guard = LengthGuard(max_chars=100)
63
64
  result = guard.check("Short text")
@@ -98,10 +99,12 @@ def test_content_guard_deny_words():
98
99
 
99
100
 
100
101
  def test_guardrail_chain():
101
- chain = GuardrailChain([
102
- LengthGuard(max_chars=1000),
103
- PIIGuard(redact=True),
104
- ])
102
+ chain = GuardrailChain(
103
+ [
104
+ LengthGuard(max_chars=1000),
105
+ PIIGuard(redact=True),
106
+ ]
107
+ )
105
108
  result = chain.check("Email: test@test.com. This is fine otherwise.")
106
109
  assert result.passed is True
107
110
  assert "REDACTED" in result.sanitized_content
@@ -109,6 +112,7 @@ def test_guardrail_chain():
109
112
 
110
113
  # --- Advisor Tests ---
111
114
 
115
+
112
116
  def test_advisor_simple_task():
113
117
  advisor = PatternAdvisor()
114
118
  rec = advisor.recommend(
@@ -1,18 +0,0 @@
1
- __pycache__/
2
- *.py[cod]
3
- *$py.class
4
- *.egg-info/
5
- dist/
6
- build/
7
- .eggs/
8
- *.egg
9
- .venv/
10
- venv/
11
- .mypy_cache/
12
- .ruff_cache/
13
- .pytest_cache/
14
- .coverage
15
- htmlcov/
16
- site/
17
- .env
18
- *.log
@@ -6,8 +6,8 @@ from pyagent_patterns.registry import get_pattern_class, list_patterns, register
6
6
 
7
7
  __all__ = [
8
8
  "Agent",
9
- "Context",
10
9
  "CompositePattern",
10
+ "Context",
11
11
  "Message",
12
12
  "MockLLM",
13
13
  "Pattern",