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.
Files changed (34) hide show
  1. pyagent_patterns/__init__.py +20 -0
  2. pyagent_patterns/advanced/__init__.py +8 -0
  3. pyagent_patterns/advanced/human_in_the_loop.py +103 -0
  4. pyagent_patterns/advanced/react.py +132 -0
  5. pyagent_patterns/advanced/swarm.py +106 -0
  6. pyagent_patterns/advanced/talker_reasoner.py +92 -0
  7. pyagent_patterns/advisor.py +166 -0
  8. pyagent_patterns/base.py +215 -0
  9. pyagent_patterns/composite.py +105 -0
  10. pyagent_patterns/guardrails.py +165 -0
  11. pyagent_patterns/orchestration/__init__.py +9 -0
  12. pyagent_patterns/orchestration/fan_out_fan_in.py +76 -0
  13. pyagent_patterns/orchestration/hierarchical.py +110 -0
  14. pyagent_patterns/orchestration/orchestrator_workers.py +97 -0
  15. pyagent_patterns/orchestration/pipeline.py +57 -0
  16. pyagent_patterns/orchestration/supervisor.py +88 -0
  17. pyagent_patterns/py.typed +0 -0
  18. pyagent_patterns/recovery.py +175 -0
  19. pyagent_patterns/registry.py +71 -0
  20. pyagent_patterns/resolution/__init__.py +9 -0
  21. pyagent_patterns/resolution/cross_reflection.py +79 -0
  22. pyagent_patterns/resolution/debate.py +103 -0
  23. pyagent_patterns/resolution/evaluator_optimizer.py +108 -0
  24. pyagent_patterns/resolution/self_reflection.py +80 -0
  25. pyagent_patterns/resolution/voting.py +93 -0
  26. pyagent_patterns/streaming.py +119 -0
  27. pyagent_patterns/structural/__init__.py +8 -0
  28. pyagent_patterns/structural/blackboard.py +137 -0
  29. pyagent_patterns/structural/layered.py +70 -0
  30. pyagent_patterns/structural/role_based.py +61 -0
  31. pyagent_patterns/structural/topology.py +123 -0
  32. pyagent_patterns-0.1.0.dist-info/METADATA +59 -0
  33. pyagent_patterns-0.1.0.dist-info/RECORD +34 -0
  34. 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
+ )