pyagent-patterns 0.1.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.
- pyagent_patterns-0.1.0/.gitignore +18 -0
- pyagent_patterns-0.1.0/PKG-INFO +59 -0
- pyagent_patterns-0.1.0/README.md +37 -0
- pyagent_patterns-0.1.0/pyproject.toml +28 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/__init__.py +20 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/__init__.py +8 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/human_in_the_loop.py +103 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/react.py +132 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/swarm.py +106 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/talker_reasoner.py +92 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/advisor.py +166 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/base.py +215 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/composite.py +105 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/guardrails.py +165 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/__init__.py +9 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/fan_out_fan_in.py +76 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/hierarchical.py +110 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/orchestrator_workers.py +97 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/pipeline.py +57 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/supervisor.py +88 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/py.typed +0 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/recovery.py +175 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/registry.py +71 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/__init__.py +9 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/cross_reflection.py +79 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/debate.py +103 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/evaluator_optimizer.py +108 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/self_reflection.py +80 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/voting.py +93 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/streaming.py +119 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/structural/__init__.py +8 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/structural/blackboard.py +137 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/structural/layered.py +70 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/structural/role_based.py +61 -0
- pyagent_patterns-0.1.0/src/pyagent_patterns/structural/topology.py +123 -0
- pyagent_patterns-0.1.0/tests/__init__.py +0 -0
- pyagent_patterns-0.1.0/tests/test_advanced.py +98 -0
- pyagent_patterns-0.1.0/tests/test_composite.py +47 -0
- pyagent_patterns-0.1.0/tests/test_orchestration.py +105 -0
- pyagent_patterns-0.1.0/tests/test_resolution.py +95 -0
- pyagent_patterns-0.1.0/tests/test_structural.py +89 -0
- pyagent_patterns-0.1.0/tests/test_valueadd.py +155 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyagent-patterns
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 18 reusable multi-agent orchestration patterns for LLMs
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: LLM,agents,multi-agent,orchestration,patterns
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pyagent-patterns
|
|
24
|
+
|
|
25
|
+
**18 reusable multi-agent orchestration patterns for LLMs** — zero dependencies, async-first, fully typed.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pyagent-patterns
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Patterns
|
|
34
|
+
|
|
35
|
+
| Tier | Patterns |
|
|
36
|
+
|------|----------|
|
|
37
|
+
| Orchestration | Supervisor, Pipeline, Fan-Out/Fan-In, Hierarchical, Orchestrator-Workers |
|
|
38
|
+
| Resolution | Self-Reflection, Cross-Reflection, Debate, Voting, Evaluator-Optimizer |
|
|
39
|
+
| Structural | Role-Based, Layered, Topology, Blackboard |
|
|
40
|
+
| Advanced | Talker-Reasoner, Swarm, Human-in-the-Loop, ReAct |
|
|
41
|
+
|
|
42
|
+
Plus: CompositePattern (escalation chains), PatternAdvisor, GuardrailChain, BoundedExecution, CircuitBreaker.
|
|
43
|
+
|
|
44
|
+
## Quick Example
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from pyagent_patterns.base import Agent, MockLLM
|
|
49
|
+
from pyagent_patterns.resolution import SelfReflection
|
|
50
|
+
|
|
51
|
+
llm = MockLLM(responses=["Draft code", "Needs error handling", "Improved code", "APPROVED"])
|
|
52
|
+
pattern = SelfReflection(agent=Agent("coder", llm), max_rounds=3)
|
|
53
|
+
result = asyncio.run(pattern.run("Write a sorting function"))
|
|
54
|
+
print(result.output)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
Full docs: [pyagent.dev](https://pyagent.dev)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# pyagent-patterns
|
|
2
|
+
|
|
3
|
+
**18 reusable multi-agent orchestration patterns for LLMs** — zero dependencies, async-first, fully typed.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pyagent-patterns
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Patterns
|
|
12
|
+
|
|
13
|
+
| Tier | Patterns |
|
|
14
|
+
|------|----------|
|
|
15
|
+
| Orchestration | Supervisor, Pipeline, Fan-Out/Fan-In, Hierarchical, Orchestrator-Workers |
|
|
16
|
+
| Resolution | Self-Reflection, Cross-Reflection, Debate, Voting, Evaluator-Optimizer |
|
|
17
|
+
| Structural | Role-Based, Layered, Topology, Blackboard |
|
|
18
|
+
| Advanced | Talker-Reasoner, Swarm, Human-in-the-Loop, ReAct |
|
|
19
|
+
|
|
20
|
+
Plus: CompositePattern (escalation chains), PatternAdvisor, GuardrailChain, BoundedExecution, CircuitBreaker.
|
|
21
|
+
|
|
22
|
+
## Quick Example
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
from pyagent_patterns.base import Agent, MockLLM
|
|
27
|
+
from pyagent_patterns.resolution import SelfReflection
|
|
28
|
+
|
|
29
|
+
llm = MockLLM(responses=["Draft code", "Needs error handling", "Improved code", "APPROVED"])
|
|
30
|
+
pattern = SelfReflection(agent=Agent("coder", llm), max_rounds=3)
|
|
31
|
+
result = asyncio.run(pattern.run("Write a sorting function"))
|
|
32
|
+
print(result.output)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
Full docs: [pyagent.dev](https://pyagent.dev)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyagent-patterns"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "18 reusable multi-agent orchestration patterns for LLMs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
keywords = ["agents", "multi-agent", "LLM", "patterns", "orchestration"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "ruff>=0.5", "mypy>=1.10"]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/pyagent_patterns"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""PyAgent Patterns — 18 reusable multi-agent orchestration patterns for LLMs."""
|
|
2
|
+
|
|
3
|
+
from pyagent_patterns.base import Agent, Context, Message, MockLLM, Pattern, Result, Role
|
|
4
|
+
from pyagent_patterns.composite import CompositePattern
|
|
5
|
+
from pyagent_patterns.registry import get_pattern_class, list_patterns, register_pattern
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Agent",
|
|
9
|
+
"Context",
|
|
10
|
+
"CompositePattern",
|
|
11
|
+
"Message",
|
|
12
|
+
"MockLLM",
|
|
13
|
+
"Pattern",
|
|
14
|
+
"Result",
|
|
15
|
+
"Role",
|
|
16
|
+
"get_pattern_class",
|
|
17
|
+
"list_patterns",
|
|
18
|
+
"register_pattern",
|
|
19
|
+
]
|
|
20
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Tier 4: Advanced/Emergent patterns — TalkerReasoner, Swarm, HumanInTheLoop, ReAct."""
|
|
2
|
+
|
|
3
|
+
from pyagent_patterns.advanced.human_in_the_loop import HumanInTheLoop
|
|
4
|
+
from pyagent_patterns.advanced.react import ReAct
|
|
5
|
+
from pyagent_patterns.advanced.swarm import Swarm
|
|
6
|
+
from pyagent_patterns.advanced.talker_reasoner import TalkerReasoner
|
|
7
|
+
|
|
8
|
+
__all__ = ["TalkerReasoner", "Swarm", "HumanInTheLoop", "ReAct"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Human-in-the-Loop pattern: approval gates at critical decision points.
|
|
2
|
+
|
|
3
|
+
An agent processes the task, then a human approval function decides
|
|
4
|
+
whether to accept, reject, or modify the output before it proceeds.
|
|
5
|
+
|
|
6
|
+
LLM calls: 1 agent + optional revision calls
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HumanDecision:
|
|
17
|
+
"""Result of a human review."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
approved: bool,
|
|
22
|
+
feedback: str = "",
|
|
23
|
+
modified_output: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.approved = approved
|
|
26
|
+
self.feedback = feedback
|
|
27
|
+
self.modified_output = modified_output
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Type alias for the human review callback
|
|
31
|
+
HumanReviewFn = Callable[[str, dict[str, object]], HumanDecision]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def auto_approve(output: str, metadata: dict[str, object]) -> HumanDecision:
|
|
35
|
+
"""Default: auto-approve all outputs (for testing)."""
|
|
36
|
+
return HumanDecision(approved=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HumanInTheLoop(Pattern):
|
|
40
|
+
"""Agent with human approval gate.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
agent: The LLM agent that processes the task.
|
|
44
|
+
review_fn: Callable that presents output to human and returns decision.
|
|
45
|
+
Signature: (output: str, metadata: dict) -> HumanDecision
|
|
46
|
+
max_revisions: Maximum number of revision attempts after rejection.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
agent: Agent,
|
|
52
|
+
review_fn: HumanReviewFn = auto_approve,
|
|
53
|
+
max_revisions: int = 3,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._agent = agent
|
|
56
|
+
self._review_fn = review_fn
|
|
57
|
+
self._max_revisions = max_revisions
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def pattern_type(self) -> str:
|
|
61
|
+
return "human_in_the_loop"
|
|
62
|
+
|
|
63
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
64
|
+
messages: list[Message] = []
|
|
65
|
+
|
|
66
|
+
# Initial agent response
|
|
67
|
+
response = await self._agent.run(ctx.messages)
|
|
68
|
+
messages.append(response)
|
|
69
|
+
current_output = response.content
|
|
70
|
+
|
|
71
|
+
for revision in range(self._max_revisions + 1):
|
|
72
|
+
# Human review
|
|
73
|
+
decision = self._review_fn(current_output, {"revision": revision, "task": ctx.task})
|
|
74
|
+
|
|
75
|
+
if decision.approved:
|
|
76
|
+
final_output = decision.modified_output or current_output
|
|
77
|
+
return Result(
|
|
78
|
+
output=final_output,
|
|
79
|
+
messages=messages,
|
|
80
|
+
metadata={
|
|
81
|
+
"approved": True,
|
|
82
|
+
"revisions": revision,
|
|
83
|
+
"human_modified": decision.modified_output is not None,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if revision < self._max_revisions:
|
|
88
|
+
# Revise based on human feedback
|
|
89
|
+
revision_prompt = Message.user(
|
|
90
|
+
f"Revise your output based on this feedback:\n\n"
|
|
91
|
+
f"Your output:\n{current_output}\n\n"
|
|
92
|
+
f"Feedback:\n{decision.feedback}"
|
|
93
|
+
)
|
|
94
|
+
response = await self._agent.run([revision_prompt])
|
|
95
|
+
messages.append(response)
|
|
96
|
+
current_output = response.content
|
|
97
|
+
|
|
98
|
+
# Max revisions exceeded — return last output with rejection flag
|
|
99
|
+
return Result(
|
|
100
|
+
output=current_output,
|
|
101
|
+
messages=messages,
|
|
102
|
+
metadata={"approved": False, "revisions": self._max_revisions},
|
|
103
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""ReAct pattern: Reason → Act → Observe cycle.
|
|
2
|
+
|
|
3
|
+
The agent iteratively reasons about the task, takes an action (e.g., tool call),
|
|
4
|
+
observes the result, then reasons again. Continues until the task is solved
|
|
5
|
+
or max steps reached.
|
|
6
|
+
|
|
7
|
+
LLM calls: 1 per step × max_steps
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Tool function type: (action_input: str) -> str
|
|
18
|
+
ToolFn = Callable[[str], str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReAct(Pattern):
|
|
22
|
+
"""Reasoning + Acting loop with tool use.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
agent: The reasoning agent.
|
|
26
|
+
tools: Mapping of tool names to callable functions.
|
|
27
|
+
max_steps: Maximum number of Thought→Action→Observation cycles.
|
|
28
|
+
finish_token: Token in agent response that signals task completion.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
agent: Agent,
|
|
34
|
+
tools: dict[str, ToolFn] | None = None,
|
|
35
|
+
max_steps: int = 5,
|
|
36
|
+
finish_token: str = "FINISH",
|
|
37
|
+
) -> None:
|
|
38
|
+
self._agent = agent
|
|
39
|
+
self._tools = tools or {}
|
|
40
|
+
self._max_steps = max_steps
|
|
41
|
+
self._finish_token = finish_token
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def pattern_type(self) -> str:
|
|
45
|
+
return "react"
|
|
46
|
+
|
|
47
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
48
|
+
messages: list[Message] = []
|
|
49
|
+
trace: list[dict[str, Any]] = []
|
|
50
|
+
|
|
51
|
+
tool_list = ", ".join(self._tools.keys()) if self._tools else "none"
|
|
52
|
+
system_prompt = (
|
|
53
|
+
f"You are a ReAct agent. Available tools: {tool_list}\n\n"
|
|
54
|
+
f"On each step, respond in this EXACT format:\n"
|
|
55
|
+
f"Thought: [your reasoning]\n"
|
|
56
|
+
f"Action: [tool_name(input)] OR {self._finish_token}[final answer]\n\n"
|
|
57
|
+
f"After receiving an observation, continue reasoning.\n"
|
|
58
|
+
f"When you have the final answer, use: {self._finish_token}[your answer]"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
conversation: list[Message] = [
|
|
62
|
+
Message.system(system_prompt),
|
|
63
|
+
Message.user(ctx.task),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
for step in range(1, self._max_steps + 1):
|
|
67
|
+
# Agent reasons and decides action
|
|
68
|
+
response = await self._agent.run(conversation)
|
|
69
|
+
messages.append(response)
|
|
70
|
+
conversation.append(response)
|
|
71
|
+
|
|
72
|
+
step_data: dict[str, Any] = {"step": step, "response": response.content}
|
|
73
|
+
|
|
74
|
+
# Check for finish
|
|
75
|
+
if self._finish_token in response.content:
|
|
76
|
+
# Extract final answer
|
|
77
|
+
parts = response.content.split(self._finish_token, 1)
|
|
78
|
+
final_answer = parts[1].strip() if len(parts) > 1 else response.content
|
|
79
|
+
step_data["action"] = "finish"
|
|
80
|
+
trace.append(step_data)
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
# Parse action
|
|
84
|
+
action_name, action_input = self._parse_action(response.content)
|
|
85
|
+
step_data["action"] = action_name
|
|
86
|
+
step_data["action_input"] = action_input
|
|
87
|
+
|
|
88
|
+
# Execute tool
|
|
89
|
+
if action_name and action_name in self._tools:
|
|
90
|
+
try:
|
|
91
|
+
observation = self._tools[action_name](action_input)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
observation = f"Error: {e}"
|
|
94
|
+
step_data["observation"] = observation
|
|
95
|
+
else:
|
|
96
|
+
observation = f"Unknown tool: {action_name}. Available: {tool_list}"
|
|
97
|
+
step_data["observation"] = observation
|
|
98
|
+
|
|
99
|
+
trace.append(step_data)
|
|
100
|
+
|
|
101
|
+
# Feed observation back
|
|
102
|
+
obs_msg = Message.user(f"Observation: {observation}")
|
|
103
|
+
conversation.append(obs_msg)
|
|
104
|
+
messages.append(obs_msg)
|
|
105
|
+
else:
|
|
106
|
+
final_answer = messages[-1].content if messages else ""
|
|
107
|
+
|
|
108
|
+
return Result(
|
|
109
|
+
output=final_answer,
|
|
110
|
+
messages=messages,
|
|
111
|
+
metadata={
|
|
112
|
+
"steps": len(trace),
|
|
113
|
+
"max_steps": self._max_steps,
|
|
114
|
+
"trace": trace,
|
|
115
|
+
"tools_used": [t["action"] for t in trace if t.get("action") != "finish"],
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _parse_action(content: str) -> tuple[str | None, str]:
|
|
121
|
+
"""Parse 'Action: tool_name(input)' from agent response."""
|
|
122
|
+
for line in content.split("\n"):
|
|
123
|
+
line = line.strip()
|
|
124
|
+
if line.lower().startswith("action:"):
|
|
125
|
+
action_text = line.split(":", 1)[1].strip()
|
|
126
|
+
# Parse tool_name(input)
|
|
127
|
+
if "(" in action_text and action_text.endswith(")"):
|
|
128
|
+
name = action_text[: action_text.index("(")]
|
|
129
|
+
inp = action_text[action_text.index("(") + 1 : -1]
|
|
130
|
+
return name.strip(), inp.strip().strip('"').strip("'")
|
|
131
|
+
return action_text, ""
|
|
132
|
+
return None, ""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Swarm pattern: emergent behavior from many agents with local rules, no controller.
|
|
2
|
+
|
|
3
|
+
Each agent operates independently with simple local rules.
|
|
4
|
+
Global behavior emerges from agent interactions. No central orchestrator.
|
|
5
|
+
|
|
6
|
+
LLM calls: N agents × rounds
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import random
|
|
13
|
+
|
|
14
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Swarm(Pattern):
|
|
18
|
+
"""Decentralized swarm: agents interact locally, behavior emerges globally.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
agents: Pool of swarm agents (all follow same local rules).
|
|
22
|
+
rounds: Number of interaction rounds.
|
|
23
|
+
neighbor_count: How many random peers each agent interacts with per round.
|
|
24
|
+
aggregation: How to produce final output from swarm state.
|
|
25
|
+
"last" = last round's outputs, "vote" = majority vote.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
agents: list[Agent],
|
|
31
|
+
rounds: int = 3,
|
|
32
|
+
neighbor_count: int = 2,
|
|
33
|
+
aggregation: str = "last",
|
|
34
|
+
) -> None:
|
|
35
|
+
if len(agents) < 2:
|
|
36
|
+
raise ValueError("Swarm requires at least 2 agents")
|
|
37
|
+
self._agents = agents
|
|
38
|
+
self._rounds = rounds
|
|
39
|
+
self._neighbor_count = min(neighbor_count, len(agents) - 1)
|
|
40
|
+
self._aggregation = aggregation
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def pattern_type(self) -> str:
|
|
44
|
+
return "swarm"
|
|
45
|
+
|
|
46
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
47
|
+
messages: list[Message] = []
|
|
48
|
+
# Initialize each agent's state
|
|
49
|
+
states: dict[str, str] = {}
|
|
50
|
+
|
|
51
|
+
# Round 0: each agent independently responds to the task
|
|
52
|
+
init_tasks = [agent.run([Message.user(ctx.task)]) for agent in self._agents]
|
|
53
|
+
init_results = await asyncio.gather(*init_tasks)
|
|
54
|
+
for agent, result in zip(self._agents, init_results):
|
|
55
|
+
states[agent.name] = result.content
|
|
56
|
+
messages.append(result)
|
|
57
|
+
|
|
58
|
+
# Subsequent rounds: agents interact with random neighbors
|
|
59
|
+
for round_num in range(1, self._rounds + 1):
|
|
60
|
+
new_states: dict[str, str] = {}
|
|
61
|
+
|
|
62
|
+
async def _update_agent(agent: Agent) -> tuple[str, str]:
|
|
63
|
+
# Select random neighbors
|
|
64
|
+
others = [a for a in self._agents if a.name != agent.name]
|
|
65
|
+
neighbors = random.sample(others, min(self._neighbor_count, len(others)))
|
|
66
|
+
|
|
67
|
+
neighbor_views = "\n".join(
|
|
68
|
+
f"- {n.name}: {states[n.name]}" for n in neighbors
|
|
69
|
+
)
|
|
70
|
+
prompt = Message.user(
|
|
71
|
+
f"Task: {ctx.task}\n\n"
|
|
72
|
+
f"Your current response: {states[agent.name]}\n\n"
|
|
73
|
+
f"Neighbor responses:\n{neighbor_views}\n\n"
|
|
74
|
+
f"Update your response considering your neighbors' views."
|
|
75
|
+
)
|
|
76
|
+
result = await agent.run([prompt])
|
|
77
|
+
return agent.name, result.content
|
|
78
|
+
|
|
79
|
+
update_tasks = [_update_agent(agent) for agent in self._agents]
|
|
80
|
+
updates = await asyncio.gather(*update_tasks)
|
|
81
|
+
for name, content in updates:
|
|
82
|
+
new_states[name] = content
|
|
83
|
+
messages.append(Message.assistant(content, name=name))
|
|
84
|
+
|
|
85
|
+
states = new_states
|
|
86
|
+
|
|
87
|
+
# Aggregate final output
|
|
88
|
+
if self._aggregation == "vote":
|
|
89
|
+
from collections import Counter
|
|
90
|
+
|
|
91
|
+
first_lines = [s.split("\n")[0].strip() for s in states.values()]
|
|
92
|
+
winner = Counter(first_lines).most_common(1)[0][0]
|
|
93
|
+
output = winner
|
|
94
|
+
else:
|
|
95
|
+
output = "\n\n".join(f"[{name}]: {content}" for name, content in states.items())
|
|
96
|
+
|
|
97
|
+
return Result(
|
|
98
|
+
output=output,
|
|
99
|
+
messages=messages,
|
|
100
|
+
metadata={
|
|
101
|
+
"agents": len(self._agents),
|
|
102
|
+
"rounds": self._rounds,
|
|
103
|
+
"aggregation": self._aggregation,
|
|
104
|
+
"final_states": states,
|
|
105
|
+
},
|
|
106
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Talker-Reasoner pattern: System 1 (fast/cheap) + System 2 (slow/expensive).
|
|
2
|
+
|
|
3
|
+
Inspired by Kahneman's dual-process theory and Google DeepMind's 2024 paper.
|
|
4
|
+
A fast "talker" handles routine queries; a slow "reasoner" is activated
|
|
5
|
+
only for complex queries that exceed a complexity threshold.
|
|
6
|
+
|
|
7
|
+
LLM calls: 1 (easy) or 2 (hard: classifier + reasoner) or 3 (with classifier)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pyagent_patterns.base import Agent, Context, Message, Pattern, Result
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TalkerReasoner(Pattern):
|
|
16
|
+
"""Dual-process: fast intuition (System 1) + slow deliberation (System 2).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
talker: Fast, cheap agent for routine queries (System 1).
|
|
20
|
+
reasoner: Slow, expensive agent for complex queries (System 2).
|
|
21
|
+
classifier: Optional agent that decides talker vs reasoner.
|
|
22
|
+
If None, always starts with talker and escalates on uncertainty keywords.
|
|
23
|
+
complexity_threshold: Keywords in talker output that trigger escalation to reasoner.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
talker: Agent,
|
|
29
|
+
reasoner: Agent,
|
|
30
|
+
classifier: Agent | None = None,
|
|
31
|
+
complexity_threshold: list[str] | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._talker = talker
|
|
34
|
+
self._reasoner = reasoner
|
|
35
|
+
self._classifier = classifier
|
|
36
|
+
self._complexity_keywords = complexity_threshold or [
|
|
37
|
+
"I'm not sure",
|
|
38
|
+
"I don't know",
|
|
39
|
+
"complex",
|
|
40
|
+
"need to think",
|
|
41
|
+
"uncertain",
|
|
42
|
+
"ESCALATE",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def pattern_type(self) -> str:
|
|
47
|
+
return "talker_reasoner"
|
|
48
|
+
|
|
49
|
+
async def _execute(self, ctx: Context) -> Result:
|
|
50
|
+
messages: list[Message] = []
|
|
51
|
+
|
|
52
|
+
if self._classifier:
|
|
53
|
+
# Use classifier to decide
|
|
54
|
+
classify_prompt = Message.user(
|
|
55
|
+
f"Is this query simple or complex? "
|
|
56
|
+
f"Respond with exactly 'SIMPLE' or 'COMPLEX'.\n\n"
|
|
57
|
+
f"Query: {ctx.task}"
|
|
58
|
+
)
|
|
59
|
+
classification = await self._classifier.run([classify_prompt])
|
|
60
|
+
messages.append(classification)
|
|
61
|
+
use_reasoner = "COMPLEX" in classification.content.upper()
|
|
62
|
+
else:
|
|
63
|
+
use_reasoner = False
|
|
64
|
+
|
|
65
|
+
if not use_reasoner:
|
|
66
|
+
# System 1: fast talker
|
|
67
|
+
talker_result = await self._talker.run(ctx.messages)
|
|
68
|
+
messages.append(talker_result)
|
|
69
|
+
|
|
70
|
+
# Check if talker signals uncertainty → escalate to reasoner
|
|
71
|
+
should_escalate = any(
|
|
72
|
+
kw.lower() in talker_result.content.lower() for kw in self._complexity_keywords
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if should_escalate:
|
|
76
|
+
use_reasoner = True
|
|
77
|
+
else:
|
|
78
|
+
return Result(
|
|
79
|
+
output=talker_result.content,
|
|
80
|
+
messages=messages,
|
|
81
|
+
metadata={"system": "talker", "escalated": False},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# System 2: slow reasoner
|
|
85
|
+
reasoner_result = await self._reasoner.run(ctx.messages)
|
|
86
|
+
messages.append(reasoner_result)
|
|
87
|
+
|
|
88
|
+
return Result(
|
|
89
|
+
output=reasoner_result.content,
|
|
90
|
+
messages=messages,
|
|
91
|
+
metadata={"system": "reasoner", "escalated": not bool(self._classifier)},
|
|
92
|
+
)
|