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.
Files changed (42) hide show
  1. pyagent_patterns-0.1.0/.gitignore +18 -0
  2. pyagent_patterns-0.1.0/PKG-INFO +59 -0
  3. pyagent_patterns-0.1.0/README.md +37 -0
  4. pyagent_patterns-0.1.0/pyproject.toml +28 -0
  5. pyagent_patterns-0.1.0/src/pyagent_patterns/__init__.py +20 -0
  6. pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/__init__.py +8 -0
  7. pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/human_in_the_loop.py +103 -0
  8. pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/react.py +132 -0
  9. pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/swarm.py +106 -0
  10. pyagent_patterns-0.1.0/src/pyagent_patterns/advanced/talker_reasoner.py +92 -0
  11. pyagent_patterns-0.1.0/src/pyagent_patterns/advisor.py +166 -0
  12. pyagent_patterns-0.1.0/src/pyagent_patterns/base.py +215 -0
  13. pyagent_patterns-0.1.0/src/pyagent_patterns/composite.py +105 -0
  14. pyagent_patterns-0.1.0/src/pyagent_patterns/guardrails.py +165 -0
  15. pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/__init__.py +9 -0
  16. pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/fan_out_fan_in.py +76 -0
  17. pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/hierarchical.py +110 -0
  18. pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/orchestrator_workers.py +97 -0
  19. pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/pipeline.py +57 -0
  20. pyagent_patterns-0.1.0/src/pyagent_patterns/orchestration/supervisor.py +88 -0
  21. pyagent_patterns-0.1.0/src/pyagent_patterns/py.typed +0 -0
  22. pyagent_patterns-0.1.0/src/pyagent_patterns/recovery.py +175 -0
  23. pyagent_patterns-0.1.0/src/pyagent_patterns/registry.py +71 -0
  24. pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/__init__.py +9 -0
  25. pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/cross_reflection.py +79 -0
  26. pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/debate.py +103 -0
  27. pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/evaluator_optimizer.py +108 -0
  28. pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/self_reflection.py +80 -0
  29. pyagent_patterns-0.1.0/src/pyagent_patterns/resolution/voting.py +93 -0
  30. pyagent_patterns-0.1.0/src/pyagent_patterns/streaming.py +119 -0
  31. pyagent_patterns-0.1.0/src/pyagent_patterns/structural/__init__.py +8 -0
  32. pyagent_patterns-0.1.0/src/pyagent_patterns/structural/blackboard.py +137 -0
  33. pyagent_patterns-0.1.0/src/pyagent_patterns/structural/layered.py +70 -0
  34. pyagent_patterns-0.1.0/src/pyagent_patterns/structural/role_based.py +61 -0
  35. pyagent_patterns-0.1.0/src/pyagent_patterns/structural/topology.py +123 -0
  36. pyagent_patterns-0.1.0/tests/__init__.py +0 -0
  37. pyagent_patterns-0.1.0/tests/test_advanced.py +98 -0
  38. pyagent_patterns-0.1.0/tests/test_composite.py +47 -0
  39. pyagent_patterns-0.1.0/tests/test_orchestration.py +105 -0
  40. pyagent_patterns-0.1.0/tests/test_resolution.py +95 -0
  41. pyagent_patterns-0.1.0/tests/test_structural.py +89 -0
  42. pyagent_patterns-0.1.0/tests/test_valueadd.py +155 -0
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+ site/
17
+ .env
18
+ *.log
@@ -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
+ )