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,108 @@
|
|
|
1
|
+
"""Evaluator-Optimizer pattern: one agent generates, another evaluates against criteria.
|
|
2
|
+
|
|
3
|
+
The generator produces output, the evaluator scores it against predefined criteria.
|
|
4
|
+
If the score is below threshold, the generator revises based on evaluator feedback.
|
|
5
|
+
|
|
6
|
+
LLM calls: 2 per round (generate + evaluate) × rounds
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EvaluatorOptimizer(Pattern):
|
|
15
|
+
"""Generate → evaluate → revise loop with explicit evaluation criteria.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
generator: Agent that produces and revises output.
|
|
19
|
+
evaluator: Agent that scores output against criteria.
|
|
20
|
+
criteria: List of evaluation criteria the evaluator checks.
|
|
21
|
+
max_rounds: Maximum optimization rounds.
|
|
22
|
+
pass_threshold: Score (1-10) at which the output is considered acceptable.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
generator: Agent,
|
|
28
|
+
evaluator: Agent,
|
|
29
|
+
criteria: list[str] | None = None,
|
|
30
|
+
max_rounds: int = 3,
|
|
31
|
+
pass_threshold: int = 7,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._generator = generator
|
|
34
|
+
self._evaluator = evaluator
|
|
35
|
+
self._criteria = criteria or ["correctness", "clarity", "completeness"]
|
|
36
|
+
self._max_rounds = max_rounds
|
|
37
|
+
self._pass_threshold = pass_threshold
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def pattern_type(self) -> str:
|
|
41
|
+
return "evaluator_optimizer"
|
|
42
|
+
|
|
43
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
44
|
+
messages: list[Message] = []
|
|
45
|
+
current_output = ""
|
|
46
|
+
scores: list[int] = []
|
|
47
|
+
|
|
48
|
+
criteria_text = "\n".join(f"- {c}" for c in self._criteria)
|
|
49
|
+
|
|
50
|
+
for round_num in range(1, self._max_rounds + 1):
|
|
51
|
+
# Generate or revise
|
|
52
|
+
if round_num == 1:
|
|
53
|
+
gen_prompt = Message.user(ctx.task)
|
|
54
|
+
else:
|
|
55
|
+
gen_prompt = Message.user(
|
|
56
|
+
f"Revise your output based on evaluator feedback:\n\n"
|
|
57
|
+
f"Previous output:\n{current_output}\n\n"
|
|
58
|
+
f"Feedback:\n{eval_text}"
|
|
59
|
+
)
|
|
60
|
+
gen_result = await self._generator.run([gen_prompt])
|
|
61
|
+
messages.append(gen_result)
|
|
62
|
+
current_output = gen_result.content
|
|
63
|
+
|
|
64
|
+
# Evaluate
|
|
65
|
+
eval_prompt = Message.user(
|
|
66
|
+
f"Evaluate this output against these criteria:\n{criteria_text}\n\n"
|
|
67
|
+
f"Output to evaluate:\n{current_output}\n\n"
|
|
68
|
+
f"Provide a score (1-10) and specific feedback for improvement. "
|
|
69
|
+
f"Format: SCORE: N\nFEEDBACK: ..."
|
|
70
|
+
)
|
|
71
|
+
eval_result = await self._evaluator.run([eval_prompt])
|
|
72
|
+
messages.append(eval_result)
|
|
73
|
+
eval_text = eval_result.content
|
|
74
|
+
|
|
75
|
+
# Parse score
|
|
76
|
+
score = self._parse_score(eval_result.content)
|
|
77
|
+
scores.append(score)
|
|
78
|
+
|
|
79
|
+
if score >= self._pass_threshold:
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
return Result(
|
|
83
|
+
output=current_output,
|
|
84
|
+
messages=messages,
|
|
85
|
+
metadata={
|
|
86
|
+
"rounds": round_num,
|
|
87
|
+
"scores": scores,
|
|
88
|
+
"final_score": scores[-1] if scores else 0,
|
|
89
|
+
"passed": scores[-1] >= self._pass_threshold if scores else False,
|
|
90
|
+
"criteria": self._criteria,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _parse_score(content: str) -> int:
|
|
96
|
+
"""Extract numeric score from evaluator response."""
|
|
97
|
+
for line in content.split("\n"):
|
|
98
|
+
line = line.strip().upper()
|
|
99
|
+
if line.startswith("SCORE:"):
|
|
100
|
+
try:
|
|
101
|
+
return int(line.split(":")[1].strip().split()[0])
|
|
102
|
+
except (ValueError, IndexError):
|
|
103
|
+
pass
|
|
104
|
+
# Fallback: look for any number between 1-10
|
|
105
|
+
import re
|
|
106
|
+
|
|
107
|
+
numbers = re.findall(r"\b([1-9]|10)\b", content)
|
|
108
|
+
return int(numbers[0]) if numbers else 5
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Self-Reflection pattern: generate → self-critique → refine loop.
|
|
2
|
+
|
|
3
|
+
The agent generates an initial output, critiques it for errors,
|
|
4
|
+
then produces a revised output. Repeats until max rounds or confidence met.
|
|
5
|
+
|
|
6
|
+
LLM calls: 2 per round × 1-N rounds = 2-2N
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SelfReflection(Pattern):
|
|
15
|
+
"""Generate → critique → refine iterative loop.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
agent: The agent that generates and refines output.
|
|
19
|
+
critic: Optional separate critic agent. If None, the same agent self-critiques.
|
|
20
|
+
max_rounds: Maximum number of generate-critique rounds.
|
|
21
|
+
stop_phrase: If the critic's response contains this phrase, stop early.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
agent: Agent,
|
|
27
|
+
critic: Agent | None = None,
|
|
28
|
+
max_rounds: int = 3,
|
|
29
|
+
stop_phrase: str = "APPROVED",
|
|
30
|
+
) -> None:
|
|
31
|
+
self._agent = agent
|
|
32
|
+
self._critic = critic or agent
|
|
33
|
+
self._max_rounds = max_rounds
|
|
34
|
+
self._stop_phrase = stop_phrase
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def pattern_type(self) -> str:
|
|
38
|
+
return "self_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 refine)
|
|
46
|
+
if round_num == 1:
|
|
47
|
+
gen_prompt = Message.user(ctx.task)
|
|
48
|
+
else:
|
|
49
|
+
gen_prompt = Message.user(
|
|
50
|
+
f"Revise your previous output based on this feedback:\n\n"
|
|
51
|
+
f"Previous output:\n{current_output}\n\n"
|
|
52
|
+
f"Feedback:\n{critique_text}"
|
|
53
|
+
)
|
|
54
|
+
gen_result = await self._agent.run([gen_prompt])
|
|
55
|
+
messages.append(gen_result)
|
|
56
|
+
current_output = gen_result.content
|
|
57
|
+
|
|
58
|
+
# Critique
|
|
59
|
+
critique_prompt = Message.user(
|
|
60
|
+
f"Review the following output for errors, gaps, or improvements. "
|
|
61
|
+
f"If the output is satisfactory, respond with '{self._stop_phrase}'. "
|
|
62
|
+
f"Otherwise, provide specific feedback.\n\n"
|
|
63
|
+
f"Output to review:\n{current_output}"
|
|
64
|
+
)
|
|
65
|
+
critique_result = await self._critic.run([critique_prompt])
|
|
66
|
+
messages.append(critique_result)
|
|
67
|
+
critique_text = critique_result.content
|
|
68
|
+
|
|
69
|
+
if self._stop_phrase in critique_result.content:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
return Result(
|
|
73
|
+
output=current_output,
|
|
74
|
+
messages=messages,
|
|
75
|
+
metadata={
|
|
76
|
+
"rounds": round_num,
|
|
77
|
+
"max_rounds": self._max_rounds,
|
|
78
|
+
"early_stop": self._stop_phrase in messages[-1].content,
|
|
79
|
+
},
|
|
80
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Voting pattern: multiple agents vote independently, majority wins.
|
|
2
|
+
|
|
3
|
+
Each agent produces an answer independently (can run in parallel).
|
|
4
|
+
Supports majority voting, weighted voting, and ranked-choice.
|
|
5
|
+
|
|
6
|
+
LLM calls: N agents (parallel)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VotingStrategy(str, Enum):
|
|
19
|
+
MAJORITY = "majority"
|
|
20
|
+
WEIGHTED = "weighted"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Voting(Pattern):
|
|
24
|
+
"""Independent voting with configurable aggregation strategy.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
voters: List of agents that each cast a vote.
|
|
28
|
+
strategy: Voting strategy (majority or weighted).
|
|
29
|
+
weights: Optional per-agent weights for weighted voting.
|
|
30
|
+
Must match length of voters. Defaults to equal weights.
|
|
31
|
+
normalize: If True, ask each voter to respond with a concise answer
|
|
32
|
+
suitable for comparison.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
voters: list[Agent],
|
|
38
|
+
strategy: VotingStrategy = VotingStrategy.MAJORITY,
|
|
39
|
+
weights: list[float] | None = None,
|
|
40
|
+
normalize: bool = True,
|
|
41
|
+
) -> None:
|
|
42
|
+
if len(voters) < 2:
|
|
43
|
+
raise ValueError("Voting requires at least 2 voters")
|
|
44
|
+
self._voters = voters
|
|
45
|
+
self._strategy = strategy
|
|
46
|
+
self._weights = weights or [1.0] * len(voters)
|
|
47
|
+
self._normalize = normalize
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def pattern_type(self) -> str:
|
|
51
|
+
return "voting"
|
|
52
|
+
|
|
53
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
54
|
+
messages: list[Message] = []
|
|
55
|
+
|
|
56
|
+
suffix = ""
|
|
57
|
+
if self._normalize:
|
|
58
|
+
suffix = "\n\nRespond with a concise answer (one word or short phrase) first, then explain."
|
|
59
|
+
|
|
60
|
+
# All voters run in parallel
|
|
61
|
+
tasks = [
|
|
62
|
+
voter.run([Message.user(ctx.task + suffix)]) for voter in self._voters
|
|
63
|
+
]
|
|
64
|
+
votes = await asyncio.gather(*tasks)
|
|
65
|
+
messages.extend(votes)
|
|
66
|
+
|
|
67
|
+
# Extract vote labels (first line of each response)
|
|
68
|
+
vote_labels = [v.content.strip().split("\n")[0].strip() for v in votes]
|
|
69
|
+
|
|
70
|
+
# Tally
|
|
71
|
+
if self._strategy == VotingStrategy.MAJORITY:
|
|
72
|
+
counter = Counter(vote_labels)
|
|
73
|
+
winner = counter.most_common(1)[0][0]
|
|
74
|
+
tally = dict(counter)
|
|
75
|
+
else:
|
|
76
|
+
# Weighted voting
|
|
77
|
+
weighted_counts: dict[str, float] = {}
|
|
78
|
+
for label, weight in zip(vote_labels, self._weights):
|
|
79
|
+
weighted_counts[label] = weighted_counts.get(label, 0.0) + weight
|
|
80
|
+
winner = max(weighted_counts, key=weighted_counts.get) # type: ignore[arg-type]
|
|
81
|
+
tally = weighted_counts
|
|
82
|
+
|
|
83
|
+
return Result(
|
|
84
|
+
output=winner,
|
|
85
|
+
messages=messages,
|
|
86
|
+
metadata={
|
|
87
|
+
"strategy": self._strategy.value,
|
|
88
|
+
"votes": vote_labels,
|
|
89
|
+
"tally": tally,
|
|
90
|
+
"winner": winner,
|
|
91
|
+
"voter_names": [v.name for v in self._voters],
|
|
92
|
+
},
|
|
93
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Streaming support for patterns.
|
|
2
|
+
|
|
3
|
+
Allows patterns to yield intermediate results as they execute, rather than
|
|
4
|
+
waiting for the full result. Useful for real-time UIs and long-running workflows.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
async for chunk in stream_pattern(pattern, "task"):
|
|
8
|
+
print(chunk.content, end="", flush=True)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, AsyncIterator
|
|
16
|
+
|
|
17
|
+
from pyagent_patterns.base import Pattern, Result
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StreamChunk:
|
|
22
|
+
"""A single chunk of streaming output."""
|
|
23
|
+
|
|
24
|
+
content: str
|
|
25
|
+
agent_name: str = ""
|
|
26
|
+
chunk_type: str = "text" # "text", "stage_complete", "round_complete", "final"
|
|
27
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def stream_pattern(pattern: Pattern, task: str) -> AsyncIterator[StreamChunk]:
|
|
31
|
+
"""Stream a pattern's execution, yielding chunks as they complete.
|
|
32
|
+
|
|
33
|
+
For patterns that don't natively support streaming, this wraps the
|
|
34
|
+
full execution and yields the result as a single final chunk.
|
|
35
|
+
|
|
36
|
+
For Pipeline patterns, yields after each stage completes.
|
|
37
|
+
For Fan-Out, yields as each parallel agent completes.
|
|
38
|
+
"""
|
|
39
|
+
from pyagent_patterns.orchestration.pipeline import Pipeline
|
|
40
|
+
from pyagent_patterns.orchestration.fan_out_fan_in import FanOutFanIn
|
|
41
|
+
|
|
42
|
+
if isinstance(pattern, Pipeline):
|
|
43
|
+
async for chunk in _stream_pipeline(pattern, task):
|
|
44
|
+
yield chunk
|
|
45
|
+
elif isinstance(pattern, FanOutFanIn):
|
|
46
|
+
async for chunk in _stream_fanout(pattern, task):
|
|
47
|
+
yield chunk
|
|
48
|
+
else:
|
|
49
|
+
# Fallback: run full pattern and yield result as single chunk
|
|
50
|
+
result = await pattern.run(task)
|
|
51
|
+
yield StreamChunk(
|
|
52
|
+
content=result.output,
|
|
53
|
+
chunk_type="final",
|
|
54
|
+
metadata=result.metadata,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _stream_pipeline(pipeline: Pattern, task: str) -> AsyncIterator[StreamChunk]:
|
|
59
|
+
"""Stream a pipeline stage by stage."""
|
|
60
|
+
from pyagent_patterns.base import Message, Role
|
|
61
|
+
|
|
62
|
+
current_input = task
|
|
63
|
+
for i, stage in enumerate(pipeline._stages):
|
|
64
|
+
messages = [Message(role=Role.USER, content=current_input)]
|
|
65
|
+
if stage.system_prompt:
|
|
66
|
+
messages.insert(0, Message(role=Role.SYSTEM, content=stage.system_prompt))
|
|
67
|
+
response = await stage.llm.generate(messages)
|
|
68
|
+
current_input = response
|
|
69
|
+
|
|
70
|
+
is_last = i == len(pipeline._stages) - 1
|
|
71
|
+
yield StreamChunk(
|
|
72
|
+
content=response,
|
|
73
|
+
agent_name=stage.name,
|
|
74
|
+
chunk_type="final" if is_last else "stage_complete",
|
|
75
|
+
metadata={"stage": i, "stage_name": stage.name},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _stream_fanout(fanout: Pattern, task: str) -> AsyncIterator[StreamChunk]:
|
|
80
|
+
"""Stream fan-out as each parallel agent completes."""
|
|
81
|
+
from pyagent_patterns.base import Message, Role
|
|
82
|
+
|
|
83
|
+
queue: asyncio.Queue[StreamChunk | None] = asyncio.Queue()
|
|
84
|
+
|
|
85
|
+
async def run_agent(agent, idx: int) -> None:
|
|
86
|
+
messages = [Message(role=Role.USER, content=task)]
|
|
87
|
+
if agent.system_prompt:
|
|
88
|
+
messages.insert(0, Message(role=Role.SYSTEM, content=agent.system_prompt))
|
|
89
|
+
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
|
+
))
|
|
96
|
+
|
|
97
|
+
tasks = [asyncio.create_task(run_agent(a, i)) for i, a in enumerate(fanout._agents)]
|
|
98
|
+
done_count = 0
|
|
99
|
+
total = len(tasks)
|
|
100
|
+
|
|
101
|
+
while done_count < total:
|
|
102
|
+
chunk = await queue.get()
|
|
103
|
+
if chunk is not None:
|
|
104
|
+
done_count += 1
|
|
105
|
+
yield chunk
|
|
106
|
+
|
|
107
|
+
await asyncio.gather(*tasks)
|
|
108
|
+
|
|
109
|
+
# Aggregate
|
|
110
|
+
aggregated_input = "\n".join(
|
|
111
|
+
f"[{a.name}]: (see prior chunks)" for a in fanout._agents
|
|
112
|
+
)
|
|
113
|
+
result = await fanout.run(task)
|
|
114
|
+
yield StreamChunk(
|
|
115
|
+
content=result.output,
|
|
116
|
+
agent_name="aggregator",
|
|
117
|
+
chunk_type="final",
|
|
118
|
+
metadata=result.metadata,
|
|
119
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Tier 3: Structural patterns — RoleBased, Layered, Topology, Blackboard."""
|
|
2
|
+
|
|
3
|
+
from pyagent_patterns.structural.blackboard import Blackboard
|
|
4
|
+
from pyagent_patterns.structural.layered import Layered
|
|
5
|
+
from pyagent_patterns.structural.role_based import RoleBased
|
|
6
|
+
from pyagent_patterns.structural.topology import Topology, TopologyType
|
|
7
|
+
|
|
8
|
+
__all__ = ["RoleBased", "Layered", "Topology", "TopologyType", "Blackboard"]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Blackboard / Shared-State pattern: agents read/write a shared async store.
|
|
2
|
+
|
|
3
|
+
Agents don't communicate directly — they read from and write to a shared
|
|
4
|
+
blackboard. Each agent specializes in reading certain keys and writing others.
|
|
5
|
+
|
|
6
|
+
LLM calls: N agents × rounds
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BlackboardEntry:
|
|
17
|
+
"""A typed entry on the blackboard with history."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, key: str, value: Any = None, author: str = "") -> None:
|
|
20
|
+
self.key = key
|
|
21
|
+
self.value = value
|
|
22
|
+
self.author = author
|
|
23
|
+
self.history: list[tuple[str, Any]] = []
|
|
24
|
+
if value is not None:
|
|
25
|
+
self.history.append((author, value))
|
|
26
|
+
|
|
27
|
+
def update(self, value: Any, author: str) -> None:
|
|
28
|
+
self.value = value
|
|
29
|
+
self.author = author
|
|
30
|
+
self.history.append((author, value))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BlackboardState:
|
|
34
|
+
"""Shared state that agents read from and write to."""
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._entries: dict[str, BlackboardEntry] = {}
|
|
38
|
+
|
|
39
|
+
def read(self, key: str) -> Any:
|
|
40
|
+
entry = self._entries.get(key)
|
|
41
|
+
return entry.value if entry else None
|
|
42
|
+
|
|
43
|
+
def write(self, key: str, value: Any, author: str) -> None:
|
|
44
|
+
if key in self._entries:
|
|
45
|
+
self._entries[key].update(value, author)
|
|
46
|
+
else:
|
|
47
|
+
self._entries[key] = BlackboardEntry(key, value, author)
|
|
48
|
+
|
|
49
|
+
def keys(self) -> list[str]:
|
|
50
|
+
return list(self._entries.keys())
|
|
51
|
+
|
|
52
|
+
def snapshot(self) -> dict[str, Any]:
|
|
53
|
+
return {k: e.value for k, e in self._entries.items()}
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return f"BlackboardState({self.snapshot()})"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BlackboardAgent:
|
|
60
|
+
"""An agent that reads from and writes to specific blackboard keys.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
agent: The underlying LLM agent.
|
|
64
|
+
reads: Keys this agent reads from the blackboard.
|
|
65
|
+
writes: Keys this agent writes to the blackboard.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, agent: Agent, reads: list[str], writes: list[str]) -> None:
|
|
69
|
+
self.agent = agent
|
|
70
|
+
self.reads = reads
|
|
71
|
+
self.writes = writes
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Blackboard(Pattern):
|
|
75
|
+
"""Shared-state communication via a blackboard store.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
agents: List of BlackboardAgent wrappers specifying read/write keys.
|
|
79
|
+
rounds: Number of rounds agents process the blackboard.
|
|
80
|
+
initial_state: Optional initial blackboard values.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
agents: list[BlackboardAgent],
|
|
86
|
+
rounds: int = 1,
|
|
87
|
+
initial_state: dict[str, Any] | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
self._agents = agents
|
|
90
|
+
self._rounds = rounds
|
|
91
|
+
self._initial_state = initial_state or {}
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def pattern_type(self) -> str:
|
|
95
|
+
return "blackboard"
|
|
96
|
+
|
|
97
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
98
|
+
messages: list[Message] = []
|
|
99
|
+
board = BlackboardState()
|
|
100
|
+
|
|
101
|
+
# Initialize blackboard
|
|
102
|
+
board.write("task", ctx.task, "system")
|
|
103
|
+
for key, value in self._initial_state.items():
|
|
104
|
+
board.write(key, value, "system")
|
|
105
|
+
|
|
106
|
+
for round_num in range(1, self._rounds + 1):
|
|
107
|
+
for ba in self._agents:
|
|
108
|
+
# Build prompt from readable keys
|
|
109
|
+
readable = {k: board.read(k) for k in ba.reads if board.read(k) is not None}
|
|
110
|
+
prompt = (
|
|
111
|
+
f"Task: {ctx.task}\n\n"
|
|
112
|
+
f"Blackboard state (your readable keys):\n"
|
|
113
|
+
+ "\n".join(f" {k}: {v}" for k, v in readable.items())
|
|
114
|
+
+ f"\n\nYou must produce values for: {', '.join(ba.writes)}\n"
|
|
115
|
+
f"Respond with one value per line in format KEY: VALUE"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
result = await ba.agent.run([Message.user(prompt)])
|
|
119
|
+
messages.append(result)
|
|
120
|
+
|
|
121
|
+
# Parse and write results to blackboard
|
|
122
|
+
for line in result.content.split("\n"):
|
|
123
|
+
if ":" in line:
|
|
124
|
+
key, _, value = line.partition(":")
|
|
125
|
+
key = key.strip().lower().replace(" ", "_")
|
|
126
|
+
if key in ba.writes:
|
|
127
|
+
board.write(key, value.strip(), ba.agent.name)
|
|
128
|
+
|
|
129
|
+
return Result(
|
|
130
|
+
output=str(board.snapshot()),
|
|
131
|
+
messages=messages,
|
|
132
|
+
metadata={
|
|
133
|
+
"rounds": self._rounds,
|
|
134
|
+
"final_state": board.snapshot(),
|
|
135
|
+
"agents": [a.agent.name for a in self._agents],
|
|
136
|
+
},
|
|
137
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Layered Cooperation pattern: agents organized into hierarchical abstraction layers.
|
|
2
|
+
|
|
3
|
+
Each layer processes the output of the previous layer at a different level
|
|
4
|
+
of abstraction. Layer 1 gathers data, Layer 2 analyzes, Layer 3 synthesizes.
|
|
5
|
+
|
|
6
|
+
LLM calls: sum of agents across all layers
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Layer:
|
|
19
|
+
"""A named layer containing one or more parallel agents."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
agents: list[Agent]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Layered(Pattern):
|
|
26
|
+
"""Hierarchical layers of agents with increasing abstraction.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
layers: Ordered list of layers from bottom (data) to top (synthesis).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, layers: list[Layer]) -> None:
|
|
33
|
+
if not layers:
|
|
34
|
+
raise ValueError("Layered requires at least one layer")
|
|
35
|
+
self._layers = layers
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def pattern_type(self) -> str:
|
|
39
|
+
return "layered"
|
|
40
|
+
|
|
41
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
42
|
+
messages: list[Message] = []
|
|
43
|
+
layer_input = ctx.task
|
|
44
|
+
|
|
45
|
+
for i, layer in enumerate(self._layers):
|
|
46
|
+
# Run all agents in this layer in parallel
|
|
47
|
+
tasks = [
|
|
48
|
+
agent.run([Message.user(layer_input)]) for agent in layer.agents
|
|
49
|
+
]
|
|
50
|
+
layer_results = await asyncio.gather(*tasks)
|
|
51
|
+
messages.extend(layer_results)
|
|
52
|
+
|
|
53
|
+
# Combine layer output as input for next layer
|
|
54
|
+
if len(layer_results) == 1:
|
|
55
|
+
layer_input = layer_results[0].content
|
|
56
|
+
else:
|
|
57
|
+
layer_input = "\n\n".join(
|
|
58
|
+
f"[{layer.agents[j].name}]: {r.content}"
|
|
59
|
+
for j, r in enumerate(layer_results)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return Result(
|
|
63
|
+
output=layer_input,
|
|
64
|
+
messages=messages,
|
|
65
|
+
metadata={
|
|
66
|
+
"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],
|
|
69
|
+
},
|
|
70
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Role-Based Cooperation pattern: agents adopt distinct functional roles.
|
|
2
|
+
|
|
3
|
+
Most frequently used pattern (46.8% in arxiv:2511.08475).
|
|
4
|
+
Each agent has a specialized role and they communicate in a structured order.
|
|
5
|
+
|
|
6
|
+
LLM calls: N agents × rounds
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RoleBased(Pattern):
|
|
15
|
+
"""Agents with distinct roles collaborate in structured rounds.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
agents: List of role-specialized agents (order matters for turn-taking).
|
|
19
|
+
rounds: Number of communication rounds.
|
|
20
|
+
shared_context: If True, all agents see all prior messages. If False,
|
|
21
|
+
each agent only sees the immediately preceding message.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
agents: list[Agent],
|
|
27
|
+
rounds: int = 1,
|
|
28
|
+
shared_context: bool = True,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._agents = agents
|
|
31
|
+
self._rounds = rounds
|
|
32
|
+
self._shared_context = shared_context
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def pattern_type(self) -> str:
|
|
36
|
+
return "role_based"
|
|
37
|
+
|
|
38
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
39
|
+
messages: list[Message] = []
|
|
40
|
+
conversation: list[Message] = [Message.user(ctx.task)]
|
|
41
|
+
|
|
42
|
+
for round_num in range(1, self._rounds + 1):
|
|
43
|
+
for agent in self._agents:
|
|
44
|
+
if self._shared_context:
|
|
45
|
+
input_msgs = list(conversation)
|
|
46
|
+
else:
|
|
47
|
+
input_msgs = [conversation[-1]] if conversation else [Message.user(ctx.task)]
|
|
48
|
+
|
|
49
|
+
response = await agent.run(input_msgs)
|
|
50
|
+
messages.append(response)
|
|
51
|
+
conversation.append(response)
|
|
52
|
+
|
|
53
|
+
return Result(
|
|
54
|
+
output=conversation[-1].content,
|
|
55
|
+
messages=messages,
|
|
56
|
+
metadata={
|
|
57
|
+
"rounds": self._rounds,
|
|
58
|
+
"roles": [a.name for a in self._agents],
|
|
59
|
+
"shared_context": self._shared_context,
|
|
60
|
+
},
|
|
61
|
+
)
|