pyagent-patterns 0.1.0__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.
- pyagent_patterns/__init__.py +20 -0
- pyagent_patterns/advanced/__init__.py +8 -0
- pyagent_patterns/advanced/human_in_the_loop.py +103 -0
- pyagent_patterns/advanced/react.py +132 -0
- pyagent_patterns/advanced/swarm.py +106 -0
- pyagent_patterns/advanced/talker_reasoner.py +92 -0
- pyagent_patterns/advisor.py +166 -0
- pyagent_patterns/base.py +215 -0
- pyagent_patterns/composite.py +105 -0
- pyagent_patterns/guardrails.py +165 -0
- pyagent_patterns/orchestration/__init__.py +9 -0
- pyagent_patterns/orchestration/fan_out_fan_in.py +76 -0
- pyagent_patterns/orchestration/hierarchical.py +110 -0
- pyagent_patterns/orchestration/orchestrator_workers.py +97 -0
- pyagent_patterns/orchestration/pipeline.py +57 -0
- pyagent_patterns/orchestration/supervisor.py +88 -0
- pyagent_patterns/py.typed +0 -0
- pyagent_patterns/recovery.py +175 -0
- pyagent_patterns/registry.py +71 -0
- pyagent_patterns/resolution/__init__.py +9 -0
- pyagent_patterns/resolution/cross_reflection.py +79 -0
- pyagent_patterns/resolution/debate.py +103 -0
- pyagent_patterns/resolution/evaluator_optimizer.py +108 -0
- pyagent_patterns/resolution/self_reflection.py +80 -0
- pyagent_patterns/resolution/voting.py +93 -0
- pyagent_patterns/streaming.py +119 -0
- pyagent_patterns/structural/__init__.py +8 -0
- pyagent_patterns/structural/blackboard.py +137 -0
- pyagent_patterns/structural/layered.py +70 -0
- pyagent_patterns/structural/role_based.py +61 -0
- pyagent_patterns/structural/topology.py +123 -0
- pyagent_patterns-0.1.0.dist-info/METADATA +59 -0
- pyagent_patterns-0.1.0.dist-info/RECORD +34 -0
- pyagent_patterns-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Orchestrator-Workers pattern: central LLM dynamically delegates tasks to workers.
|
|
2
|
+
|
|
3
|
+
Unlike Supervisor (static routes) or Hierarchical (fixed teams), the Orchestrator
|
|
4
|
+
dynamically decides how many workers to spawn and what each should do.
|
|
5
|
+
|
|
6
|
+
LLM calls: 1 planning + N workers (dynamic) + 1 synthesis
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OrchestratorWorkers(Pattern):
|
|
18
|
+
"""Dynamic task delegation: orchestrator plans → workers execute → synthesize.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
orchestrator: Agent that plans the work and synthesizes results.
|
|
22
|
+
workers: Pool of available worker agents. The orchestrator selects from these.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, orchestrator: Agent, workers: list[Agent]) -> None:
|
|
26
|
+
self._orchestrator = orchestrator
|
|
27
|
+
self._workers = {w.name: w for w in workers}
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def pattern_type(self) -> str:
|
|
31
|
+
return "orchestrator_workers"
|
|
32
|
+
|
|
33
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
34
|
+
messages: list[Message] = []
|
|
35
|
+
worker_names = list(self._workers.keys())
|
|
36
|
+
|
|
37
|
+
# Step 1: Orchestrator plans the work
|
|
38
|
+
plan_prompt = Message.user(
|
|
39
|
+
f"You have these workers available: {', '.join(worker_names)}.\n"
|
|
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"
|
|
42
|
+
f"Task: {ctx.task}"
|
|
43
|
+
)
|
|
44
|
+
plan_msg = await self._orchestrator.run([plan_prompt])
|
|
45
|
+
messages.append(plan_msg)
|
|
46
|
+
|
|
47
|
+
# Step 2: Parse assignments and dispatch
|
|
48
|
+
assignments = self._parse_assignments(plan_msg.content)
|
|
49
|
+
worker_results: list[tuple[str, str]] = []
|
|
50
|
+
|
|
51
|
+
async def _run_assignment(worker_name: str, subtask: str) -> tuple[str, str]:
|
|
52
|
+
worker = self._workers.get(worker_name)
|
|
53
|
+
if worker is None:
|
|
54
|
+
# Fallback to first available worker
|
|
55
|
+
worker = next(iter(self._workers.values()))
|
|
56
|
+
result = await worker.run([Message.user(subtask)])
|
|
57
|
+
return worker.name, result.content
|
|
58
|
+
|
|
59
|
+
tasks = [_run_assignment(a["worker"], a["subtask"]) for a in assignments]
|
|
60
|
+
results = await asyncio.gather(*tasks)
|
|
61
|
+
for name, content in results:
|
|
62
|
+
messages.append(Message.assistant(content, name=name))
|
|
63
|
+
worker_results.append((name, content))
|
|
64
|
+
|
|
65
|
+
# Step 3: Orchestrator synthesizes
|
|
66
|
+
results_summary = "\n\n".join(
|
|
67
|
+
f"--- {name} ---\n{content}" for name, content in worker_results
|
|
68
|
+
)
|
|
69
|
+
synthesis_prompt = Message.user(
|
|
70
|
+
f"Synthesize these worker results into a final response:\n\n{results_summary}"
|
|
71
|
+
)
|
|
72
|
+
final = await self._orchestrator.run([synthesis_prompt])
|
|
73
|
+
messages.append(final)
|
|
74
|
+
|
|
75
|
+
return Result(
|
|
76
|
+
output=final.content,
|
|
77
|
+
messages=messages,
|
|
78
|
+
metadata={
|
|
79
|
+
"assignments": assignments,
|
|
80
|
+
"workers_used": len(assignments),
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _parse_assignments(content: str) -> list[dict[str, str]]:
|
|
86
|
+
"""Parse orchestrator's JSON assignment plan. Falls back gracefully."""
|
|
87
|
+
try:
|
|
88
|
+
# Try to extract JSON from the response
|
|
89
|
+
start = content.find("{")
|
|
90
|
+
end = content.rfind("}") + 1
|
|
91
|
+
if start >= 0 and end > start:
|
|
92
|
+
data = json.loads(content[start:end])
|
|
93
|
+
return data.get("assignments", [])
|
|
94
|
+
except (json.JSONDecodeError, KeyError):
|
|
95
|
+
pass
|
|
96
|
+
# Fallback: single assignment to first worker
|
|
97
|
+
return [{"worker": "default", "subtask": content}]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Pipeline pattern: sequential chain of agents where each stage's output feeds the next.
|
|
2
|
+
|
|
3
|
+
Each agent processes the output of the previous stage.
|
|
4
|
+
Ideal for document processing, ETL, and multi-step transformations.
|
|
5
|
+
|
|
6
|
+
LLM calls: N (one per stage)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import AsyncIterator
|
|
12
|
+
|
|
13
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Pipeline(Pattern):
|
|
17
|
+
"""Sequential stage chain — output of stage N becomes input of stage N+1.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
stages: Ordered list of agents, each processing the previous output.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, stages: list[Agent]) -> None:
|
|
24
|
+
if not stages:
|
|
25
|
+
raise ValueError("Pipeline requires at least one stage")
|
|
26
|
+
self._stages = stages
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def pattern_type(self) -> str:
|
|
30
|
+
return "pipeline"
|
|
31
|
+
|
|
32
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
33
|
+
messages: list[Message] = []
|
|
34
|
+
current_input = ctx.task
|
|
35
|
+
|
|
36
|
+
for i, stage in enumerate(self._stages):
|
|
37
|
+
stage_msg = Message.user(current_input)
|
|
38
|
+
response = await stage.run([stage_msg])
|
|
39
|
+
messages.append(response)
|
|
40
|
+
current_input = response.content
|
|
41
|
+
|
|
42
|
+
return Result(
|
|
43
|
+
output=current_input,
|
|
44
|
+
messages=messages,
|
|
45
|
+
metadata={"stages": len(self._stages), "stage_names": [s.name for s in self._stages]},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def stream(self, task: str, context: Context | None = None) -> AsyncIterator[str]:
|
|
49
|
+
"""Stream stage completions as they finish."""
|
|
50
|
+
ctx = context or Context(task=task)
|
|
51
|
+
current_input = task
|
|
52
|
+
|
|
53
|
+
for i, stage in enumerate(self._stages):
|
|
54
|
+
stage_msg = Message.user(current_input)
|
|
55
|
+
response = await stage.run([stage_msg])
|
|
56
|
+
current_input = response.content
|
|
57
|
+
yield f"[Stage {i + 1}/{len(self._stages)} — {stage.name}] {current_input}"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Supervisor pattern: classify input → route to specialist agent → collect result.
|
|
2
|
+
|
|
3
|
+
The Supervisor inspects the incoming task, classifies it into a category,
|
|
4
|
+
dispatches to the appropriate specialist agent, and optionally formats
|
|
5
|
+
the final response.
|
|
6
|
+
|
|
7
|
+
LLM calls: 2-3 (classify + specialist + optional format)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Supervisor(Pattern):
|
|
16
|
+
"""Classify → route → collect orchestration pattern.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
classifier: Agent that classifies the task into a route key.
|
|
20
|
+
routes: Mapping of route keys to specialist agents.
|
|
21
|
+
formatter: Optional agent that formats the final response.
|
|
22
|
+
default_route: Key to use when classification doesn't match any route.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
classifier: Agent,
|
|
28
|
+
routes: dict[str, Agent],
|
|
29
|
+
formatter: Agent | None = None,
|
|
30
|
+
default_route: str | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._classifier = classifier
|
|
33
|
+
self._routes = routes
|
|
34
|
+
self._formatter = formatter
|
|
35
|
+
self._default_route = default_route
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def pattern_type(self) -> str:
|
|
39
|
+
return "supervisor"
|
|
40
|
+
|
|
41
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
42
|
+
messages: list[Message] = []
|
|
43
|
+
|
|
44
|
+
# Step 1: Classify the task
|
|
45
|
+
classify_prompt = (
|
|
46
|
+
f"Classify the following task into exactly one of these categories: "
|
|
47
|
+
f"{', '.join(self._routes.keys())}.\n"
|
|
48
|
+
f"Respond with ONLY the category name, nothing else.\n\n"
|
|
49
|
+
f"Task: {ctx.task}"
|
|
50
|
+
)
|
|
51
|
+
classify_msg = Message.user(classify_prompt)
|
|
52
|
+
classification = await self._classifier.run([classify_msg])
|
|
53
|
+
messages.append(classification)
|
|
54
|
+
|
|
55
|
+
# Step 2: Route to specialist
|
|
56
|
+
route_key = classification.content.strip().lower()
|
|
57
|
+
agent = self._routes.get(route_key)
|
|
58
|
+
if agent is None and self._default_route:
|
|
59
|
+
agent = self._routes.get(self._default_route)
|
|
60
|
+
route_key = self._default_route
|
|
61
|
+
if agent is None:
|
|
62
|
+
# Fallback: use first available route
|
|
63
|
+
route_key = next(iter(self._routes))
|
|
64
|
+
agent = self._routes[route_key]
|
|
65
|
+
|
|
66
|
+
specialist_msg = await agent.run(ctx.messages)
|
|
67
|
+
messages.append(specialist_msg)
|
|
68
|
+
|
|
69
|
+
# Step 3: Optional formatting
|
|
70
|
+
output = specialist_msg.content
|
|
71
|
+
if self._formatter:
|
|
72
|
+
format_msgs = [
|
|
73
|
+
Message.user(
|
|
74
|
+
f"Format the following response for the user:\n\n{specialist_msg.content}"
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
formatted = await self._formatter.run(format_msgs)
|
|
78
|
+
messages.append(formatted)
|
|
79
|
+
output = formatted.content
|
|
80
|
+
|
|
81
|
+
return Result(
|
|
82
|
+
output=output,
|
|
83
|
+
messages=messages,
|
|
84
|
+
metadata={
|
|
85
|
+
"route_key": route_key,
|
|
86
|
+
"classifier_output": classification.content,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Error Recovery: BoundedExecution and CircuitBreaker for multi-agent patterns.
|
|
2
|
+
|
|
3
|
+
BoundedExecution: wrap any pattern with max iterations, token, and timeout limits.
|
|
4
|
+
CircuitBreaker: stop cascading failures with failure threshold + reset timeout.
|
|
5
|
+
|
|
6
|
+
Based on: arxiv:2604.11378 "three-level recovery protocol"
|
|
7
|
+
Augment 2026 "Bounded Execution" emergent pattern
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from pyagent_patterns.base import Context, Pattern, Result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CircuitState(str, Enum):
|
|
22
|
+
CLOSED = "closed" # Normal operation
|
|
23
|
+
OPEN = "open" # Failing, rejecting requests
|
|
24
|
+
HALF_OPEN = "half_open" # Testing if recovered
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class BoundedExecution:
|
|
29
|
+
"""Wrap a pattern with resource limits.
|
|
30
|
+
|
|
31
|
+
Three-level recovery:
|
|
32
|
+
1. Retry with same pattern
|
|
33
|
+
2. Fallback to a cheaper/simpler pattern
|
|
34
|
+
3. Graceful degradation (return partial result)
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
pattern: The primary pattern to execute.
|
|
38
|
+
fallback: Optional simpler pattern to use on failure.
|
|
39
|
+
max_retries: Maximum retry attempts before escalating.
|
|
40
|
+
timeout_seconds: Maximum wall-clock time for the entire execution.
|
|
41
|
+
max_tokens: Maximum total tokens before stopping.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
pattern: Pattern
|
|
45
|
+
fallback: Pattern | None = None
|
|
46
|
+
max_retries: int = 2
|
|
47
|
+
timeout_seconds: float = 300.0
|
|
48
|
+
max_tokens: int = 100_000
|
|
49
|
+
|
|
50
|
+
async def run(self, task: str, context: Context | None = None) -> Result:
|
|
51
|
+
"""Execute with bounded resources and three-level recovery."""
|
|
52
|
+
start = time.perf_counter()
|
|
53
|
+
|
|
54
|
+
# Level 1: Try primary pattern with retries
|
|
55
|
+
last_error: Exception | None = None
|
|
56
|
+
for attempt in range(1, self.max_retries + 1):
|
|
57
|
+
elapsed = time.perf_counter() - start
|
|
58
|
+
if elapsed > self.timeout_seconds:
|
|
59
|
+
break
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
remaining = self.timeout_seconds - elapsed
|
|
63
|
+
result = await asyncio.wait_for(
|
|
64
|
+
self.pattern.run(task, context),
|
|
65
|
+
timeout=remaining,
|
|
66
|
+
)
|
|
67
|
+
if result.token_estimate <= self.max_tokens:
|
|
68
|
+
result.metadata["recovery_level"] = 0
|
|
69
|
+
result.metadata["attempts"] = attempt
|
|
70
|
+
return result
|
|
71
|
+
# Token limit exceeded — try next level
|
|
72
|
+
last_error = Exception(f"Token limit exceeded: {result.token_estimate}/{self.max_tokens}")
|
|
73
|
+
break
|
|
74
|
+
except asyncio.TimeoutError:
|
|
75
|
+
last_error = asyncio.TimeoutError(f"Timeout after {self.timeout_seconds}s")
|
|
76
|
+
break
|
|
77
|
+
except Exception as e:
|
|
78
|
+
last_error = e
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Level 2: Fallback pattern
|
|
82
|
+
if self.fallback:
|
|
83
|
+
try:
|
|
84
|
+
remaining = max(0.0, self.timeout_seconds - (time.perf_counter() - start))
|
|
85
|
+
result = await asyncio.wait_for(
|
|
86
|
+
self.fallback.run(task, context),
|
|
87
|
+
timeout=remaining if remaining > 0 else 30.0,
|
|
88
|
+
)
|
|
89
|
+
result.metadata["recovery_level"] = 1
|
|
90
|
+
result.metadata["fallback_reason"] = str(last_error)
|
|
91
|
+
return result
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Level 3: Graceful degradation
|
|
96
|
+
return Result(
|
|
97
|
+
output=f"[Degraded] Unable to complete task after {self.max_retries} attempts. Last error: {last_error}",
|
|
98
|
+
metadata={
|
|
99
|
+
"recovery_level": 2,
|
|
100
|
+
"degraded": True,
|
|
101
|
+
"last_error": str(last_error),
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CircuitBreaker:
|
|
107
|
+
"""Prevent cascading failures in multi-agent systems.
|
|
108
|
+
|
|
109
|
+
When a pattern fails repeatedly, the circuit opens and rejects
|
|
110
|
+
requests immediately. After a reset timeout, it enters half-open
|
|
111
|
+
state and allows one test request through.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
failure_threshold: Number of consecutive failures before opening.
|
|
115
|
+
reset_timeout_seconds: Seconds before transitioning from open to half-open.
|
|
116
|
+
fallback_result: Result to return when circuit is open.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
failure_threshold: int = 3,
|
|
122
|
+
reset_timeout_seconds: float = 60.0,
|
|
123
|
+
fallback_result: str = "[Circuit Open] Service temporarily unavailable.",
|
|
124
|
+
) -> None:
|
|
125
|
+
self._threshold = failure_threshold
|
|
126
|
+
self._reset_timeout = reset_timeout_seconds
|
|
127
|
+
self._fallback_result = fallback_result
|
|
128
|
+
self._state = CircuitState.CLOSED
|
|
129
|
+
self._failure_count = 0
|
|
130
|
+
self._last_failure_time = 0.0
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
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
|
|
137
|
+
return self._state
|
|
138
|
+
|
|
139
|
+
async def execute(self, pattern: Pattern, task: str, context: Context | None = None) -> Result:
|
|
140
|
+
"""Execute a pattern through the circuit breaker."""
|
|
141
|
+
current_state = self.state
|
|
142
|
+
|
|
143
|
+
if current_state == CircuitState.OPEN:
|
|
144
|
+
return Result(
|
|
145
|
+
output=self._fallback_result,
|
|
146
|
+
metadata={"circuit_state": "open", "failure_count": self._failure_count},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
result = await pattern.run(task, context)
|
|
151
|
+
self._on_success()
|
|
152
|
+
result.metadata["circuit_state"] = self._state.value
|
|
153
|
+
return result
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self._on_failure()
|
|
156
|
+
if self._state == CircuitState.OPEN:
|
|
157
|
+
return Result(
|
|
158
|
+
output=self._fallback_result,
|
|
159
|
+
metadata={
|
|
160
|
+
"circuit_state": "open",
|
|
161
|
+
"failure_count": self._failure_count,
|
|
162
|
+
"last_error": str(e),
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
raise
|
|
166
|
+
|
|
167
|
+
def _on_success(self) -> None:
|
|
168
|
+
self._failure_count = 0
|
|
169
|
+
self._state = CircuitState.CLOSED
|
|
170
|
+
|
|
171
|
+
def _on_failure(self) -> None:
|
|
172
|
+
self._failure_count += 1
|
|
173
|
+
self._last_failure_time = time.time()
|
|
174
|
+
if self._failure_count >= self._threshold:
|
|
175
|
+
self._state = CircuitState.OPEN
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Pattern registry: discover and instantiate patterns by name."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pyagent_patterns.base import Pattern
|
|
8
|
+
|
|
9
|
+
_REGISTRY: dict[str, type[Pattern]] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_pattern(name: str, cls: type[Pattern]) -> None:
|
|
13
|
+
"""Register a pattern class under a name."""
|
|
14
|
+
_REGISTRY[name] = cls
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_pattern_class(name: str) -> type[Pattern] | None:
|
|
18
|
+
"""Look up a pattern class by name."""
|
|
19
|
+
return _REGISTRY.get(name)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def list_patterns() -> list[str]:
|
|
23
|
+
"""Return all registered pattern names."""
|
|
24
|
+
return sorted(_REGISTRY.keys())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _auto_register() -> None:
|
|
28
|
+
"""Auto-register all built-in patterns."""
|
|
29
|
+
from pyagent_patterns.advanced.human_in_the_loop import HumanInTheLoop
|
|
30
|
+
from pyagent_patterns.advanced.react import ReAct
|
|
31
|
+
from pyagent_patterns.advanced.swarm import Swarm
|
|
32
|
+
from pyagent_patterns.advanced.talker_reasoner import TalkerReasoner
|
|
33
|
+
from pyagent_patterns.orchestration.fan_out_fan_in import FanOutFanIn
|
|
34
|
+
from pyagent_patterns.orchestration.hierarchical import Hierarchical
|
|
35
|
+
from pyagent_patterns.orchestration.orchestrator_workers import OrchestratorWorkers
|
|
36
|
+
from pyagent_patterns.orchestration.pipeline import Pipeline
|
|
37
|
+
from pyagent_patterns.orchestration.supervisor import Supervisor
|
|
38
|
+
from pyagent_patterns.resolution.cross_reflection import CrossReflection
|
|
39
|
+
from pyagent_patterns.resolution.debate import Debate
|
|
40
|
+
from pyagent_patterns.resolution.evaluator_optimizer import EvaluatorOptimizer
|
|
41
|
+
from pyagent_patterns.resolution.self_reflection import SelfReflection
|
|
42
|
+
from pyagent_patterns.resolution.voting import Voting
|
|
43
|
+
from pyagent_patterns.structural.blackboard import Blackboard
|
|
44
|
+
from pyagent_patterns.structural.layered import Layered
|
|
45
|
+
from pyagent_patterns.structural.role_based import RoleBased
|
|
46
|
+
from pyagent_patterns.structural.topology import Topology
|
|
47
|
+
|
|
48
|
+
for name, cls in [
|
|
49
|
+
("supervisor", Supervisor),
|
|
50
|
+
("pipeline", Pipeline),
|
|
51
|
+
("fan_out_fan_in", FanOutFanIn),
|
|
52
|
+
("hierarchical", Hierarchical),
|
|
53
|
+
("orchestrator_workers", OrchestratorWorkers),
|
|
54
|
+
("self_reflection", SelfReflection),
|
|
55
|
+
("cross_reflection", CrossReflection),
|
|
56
|
+
("debate", Debate),
|
|
57
|
+
("voting", Voting),
|
|
58
|
+
("evaluator_optimizer", EvaluatorOptimizer),
|
|
59
|
+
("role_based", RoleBased),
|
|
60
|
+
("layered", Layered),
|
|
61
|
+
("topology", Topology),
|
|
62
|
+
("blackboard", Blackboard),
|
|
63
|
+
("talker_reasoner", TalkerReasoner),
|
|
64
|
+
("swarm", Swarm),
|
|
65
|
+
("human_in_the_loop", HumanInTheLoop),
|
|
66
|
+
("react", ReAct),
|
|
67
|
+
]:
|
|
68
|
+
register_pattern(name, cls)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_auto_register()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Tier 2: Resolution patterns — Reflection, CrossReflection, Debate, Voting, EvaluatorOptimizer."""
|
|
2
|
+
|
|
3
|
+
from pyagent_patterns.resolution.cross_reflection import CrossReflection
|
|
4
|
+
from pyagent_patterns.resolution.debate import Debate
|
|
5
|
+
from pyagent_patterns.resolution.evaluator_optimizer import EvaluatorOptimizer
|
|
6
|
+
from pyagent_patterns.resolution.self_reflection import SelfReflection
|
|
7
|
+
from pyagent_patterns.resolution.voting import Voting
|
|
8
|
+
|
|
9
|
+
__all__ = ["SelfReflection", "CrossReflection", "Debate", "Voting", "EvaluatorOptimizer"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Cross-Reflection pattern: Agent A generates → Agent B reviews → Agent A revises.
|
|
2
|
+
|
|
3
|
+
Unlike Self-Reflection where one agent critiques itself, Cross-Reflection
|
|
4
|
+
uses a separate peer agent to provide independent feedback, reducing bias.
|
|
5
|
+
|
|
6
|
+
LLM calls: 3 minimum (generate + review + revise)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CrossReflection(Pattern):
|
|
15
|
+
"""Peer review: one agent generates, another reviews, generator revises.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
generator: Agent that produces the initial output and revisions.
|
|
19
|
+
reviewer: Agent that reviews and provides feedback.
|
|
20
|
+
max_rounds: Maximum number of generate-review-revise cycles.
|
|
21
|
+
stop_phrase: If reviewer says this, stop early.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
generator: Agent,
|
|
27
|
+
reviewer: Agent,
|
|
28
|
+
max_rounds: int = 2,
|
|
29
|
+
stop_phrase: str = "APPROVED",
|
|
30
|
+
) -> None:
|
|
31
|
+
self._generator = generator
|
|
32
|
+
self._reviewer = reviewer
|
|
33
|
+
self._max_rounds = max_rounds
|
|
34
|
+
self._stop_phrase = stop_phrase
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def pattern_type(self) -> str:
|
|
38
|
+
return "cross_reflection"
|
|
39
|
+
|
|
40
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
41
|
+
messages: list[Message] = []
|
|
42
|
+
current_output = ""
|
|
43
|
+
|
|
44
|
+
for round_num in range(1, self._max_rounds + 1):
|
|
45
|
+
# Generate or revise
|
|
46
|
+
if round_num == 1:
|
|
47
|
+
gen_prompt = Message.user(ctx.task)
|
|
48
|
+
else:
|
|
49
|
+
gen_prompt = Message.user(
|
|
50
|
+
f"Revise based on peer feedback:\n\n"
|
|
51
|
+
f"Your output:\n{current_output}\n\n"
|
|
52
|
+
f"Peer feedback:\n{review_text}"
|
|
53
|
+
)
|
|
54
|
+
gen_result = await self._generator.run([gen_prompt])
|
|
55
|
+
messages.append(gen_result)
|
|
56
|
+
current_output = gen_result.content
|
|
57
|
+
|
|
58
|
+
# Peer review
|
|
59
|
+
review_prompt = Message.user(
|
|
60
|
+
f"Review this output from your peer. Provide constructive feedback. "
|
|
61
|
+
f"If the output is satisfactory, respond with '{self._stop_phrase}'.\n\n"
|
|
62
|
+
f"{current_output}"
|
|
63
|
+
)
|
|
64
|
+
review_result = await self._reviewer.run([review_prompt])
|
|
65
|
+
messages.append(review_result)
|
|
66
|
+
review_text = review_result.content
|
|
67
|
+
|
|
68
|
+
if self._stop_phrase in review_result.content:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
return Result(
|
|
72
|
+
output=current_output,
|
|
73
|
+
messages=messages,
|
|
74
|
+
metadata={
|
|
75
|
+
"rounds": round_num,
|
|
76
|
+
"generator": self._generator.name,
|
|
77
|
+
"reviewer": self._reviewer.name,
|
|
78
|
+
},
|
|
79
|
+
)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Debate pattern: adversarial multi-round argumentation with a judge.
|
|
2
|
+
|
|
3
|
+
Multiple debater agents argue different positions over several rounds.
|
|
4
|
+
A judge agent evaluates arguments and renders a final decision.
|
|
5
|
+
|
|
6
|
+
LLM calls: D debaters × R rounds + 1 judge = D*R + 1
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Debate(Pattern):
|
|
15
|
+
"""Structured adversarial debate with judge resolution.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
debaters: List of agents, each arguing a different position.
|
|
19
|
+
judge: Agent that evaluates arguments and renders final decision.
|
|
20
|
+
rounds: Number of argumentation rounds.
|
|
21
|
+
positions: Optional list of position labels (e.g., ["BUY", "SELL"]).
|
|
22
|
+
If not provided, positions are assigned as "Position 1", "Position 2", etc.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
debaters: list[Agent],
|
|
28
|
+
judge: Agent,
|
|
29
|
+
rounds: int = 3,
|
|
30
|
+
positions: list[str] | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
if len(debaters) < 2:
|
|
33
|
+
raise ValueError("Debate requires at least 2 debaters")
|
|
34
|
+
self._debaters = debaters
|
|
35
|
+
self._judge = judge
|
|
36
|
+
self._rounds = rounds
|
|
37
|
+
self._positions = positions or [f"Position {i + 1}" for i in range(len(debaters))]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def pattern_type(self) -> str:
|
|
41
|
+
return "debate"
|
|
42
|
+
|
|
43
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
44
|
+
messages: list[Message] = []
|
|
45
|
+
debate_log: list[dict[str, str]] = []
|
|
46
|
+
|
|
47
|
+
for round_num in range(1, self._rounds + 1):
|
|
48
|
+
round_args: list[str] = []
|
|
49
|
+
|
|
50
|
+
for i, debater in enumerate(self._debaters):
|
|
51
|
+
position = self._positions[i]
|
|
52
|
+
if round_num == 1:
|
|
53
|
+
prompt = Message.user(
|
|
54
|
+
f"You are arguing for '{position}' on this topic: {ctx.task}\n"
|
|
55
|
+
f"Present your opening argument."
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
opponent_args = "\n".join(
|
|
59
|
+
f"- {self._positions[j]}: {a}"
|
|
60
|
+
for j, a in enumerate(round_args_prev)
|
|
61
|
+
if j != i
|
|
62
|
+
)
|
|
63
|
+
prompt = Message.user(
|
|
64
|
+
f"You are arguing for '{position}'. Round {round_num}.\n"
|
|
65
|
+
f"Counter these arguments:\n{opponent_args}\n\n"
|
|
66
|
+
f"Strengthen your position."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
arg_result = await debater.run([prompt])
|
|
70
|
+
messages.append(arg_result)
|
|
71
|
+
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
|
+
})
|
|
78
|
+
|
|
79
|
+
round_args_prev = round_args
|
|
80
|
+
|
|
81
|
+
# Judge evaluates all arguments
|
|
82
|
+
full_debate = "\n\n".join(
|
|
83
|
+
f"[Round {entry['round']}] {entry['position']} ({entry['debater']}):\n{entry['argument']}"
|
|
84
|
+
for entry in debate_log
|
|
85
|
+
)
|
|
86
|
+
judge_prompt = Message.user(
|
|
87
|
+
f"You are the judge. Evaluate these arguments and render a final decision.\n"
|
|
88
|
+
f"Topic: {ctx.task}\n\n{full_debate}\n\n"
|
|
89
|
+
f"State your decision and reasoning."
|
|
90
|
+
)
|
|
91
|
+
verdict = await self._judge.run([judge_prompt])
|
|
92
|
+
messages.append(verdict)
|
|
93
|
+
|
|
94
|
+
return Result(
|
|
95
|
+
output=verdict.content,
|
|
96
|
+
messages=messages,
|
|
97
|
+
metadata={
|
|
98
|
+
"rounds": self._rounds,
|
|
99
|
+
"debaters": [d.name for d in self._debaters],
|
|
100
|
+
"positions": self._positions,
|
|
101
|
+
"debate_log": debate_log,
|
|
102
|
+
},
|
|
103
|
+
)
|