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,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
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""PatternAdvisor: recommend the best pattern based on task constraints.
|
|
2
|
+
|
|
3
|
+
Based on: Augment 2026 "Five Decision Rules" for pattern selection.
|
|
4
|
+
|
|
5
|
+
Decision factors:
|
|
6
|
+
1. Task complexity (simple vs multi-step vs adversarial)
|
|
7
|
+
2. Quality requirement (draft vs production)
|
|
8
|
+
3. Budget constraint (tokens/$)
|
|
9
|
+
4. Latency requirement (real-time vs batch)
|
|
10
|
+
5. Reliability requirement (best-effort vs fault-tolerant)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from enum import Enum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Quality(str, Enum):
|
|
20
|
+
DRAFT = "draft"
|
|
21
|
+
STANDARD = "standard"
|
|
22
|
+
HIGH = "high"
|
|
23
|
+
CRITICAL = "critical"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Latency(str, Enum):
|
|
27
|
+
REALTIME = "realtime" # < 2s
|
|
28
|
+
INTERACTIVE = "interactive" # < 10s
|
|
29
|
+
BATCH = "batch" # minutes OK
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Constraints:
|
|
34
|
+
"""Task constraints for pattern selection."""
|
|
35
|
+
|
|
36
|
+
quality: Quality = Quality.STANDARD
|
|
37
|
+
latency: Latency = Latency.INTERACTIVE
|
|
38
|
+
max_cost_usd: float = 0.05
|
|
39
|
+
fault_tolerant: bool = False
|
|
40
|
+
multi_step: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Recommendation:
|
|
45
|
+
"""A pattern recommendation with reasoning."""
|
|
46
|
+
|
|
47
|
+
pattern: str
|
|
48
|
+
reason: str
|
|
49
|
+
estimated_calls: int
|
|
50
|
+
estimated_cost_range: str
|
|
51
|
+
alternatives: list[str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PatternAdvisor:
|
|
55
|
+
"""Recommend patterns based on task description and constraints.
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
advisor = PatternAdvisor()
|
|
59
|
+
rec = advisor.recommend("Write and review code", Constraints(quality=Quality.HIGH))
|
|
60
|
+
print(rec.pattern, rec.reason)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def recommend(self, task: str, constraints: Constraints | None = None) -> Recommendation:
|
|
64
|
+
"""Recommend the best pattern for the given task and constraints."""
|
|
65
|
+
c = constraints or Constraints()
|
|
66
|
+
task_lower = task.lower()
|
|
67
|
+
|
|
68
|
+
# Decision tree based on Augment 2026 "Five Decision Rules"
|
|
69
|
+
|
|
70
|
+
# Rule 1: Simple single-step tasks → Pipeline or single agent
|
|
71
|
+
if not c.multi_step and c.quality in (Quality.DRAFT, Quality.STANDARD):
|
|
72
|
+
if c.latency == Latency.REALTIME:
|
|
73
|
+
return Recommendation(
|
|
74
|
+
pattern="pipeline",
|
|
75
|
+
reason="Simple task with real-time latency → minimal sequential processing",
|
|
76
|
+
estimated_calls=1,
|
|
77
|
+
estimated_cost_range="$0.001-0.003",
|
|
78
|
+
alternatives=["talker_reasoner"],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Rule 2: Cost-sensitive → Route to cheapest viable model
|
|
82
|
+
if c.max_cost_usd < 0.01:
|
|
83
|
+
return Recommendation(
|
|
84
|
+
pattern="talker_reasoner",
|
|
85
|
+
reason="Tight budget → use cheap model for easy, expensive only when needed",
|
|
86
|
+
estimated_calls=1,
|
|
87
|
+
estimated_cost_range="$0.001-0.005",
|
|
88
|
+
alternatives=["pipeline"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Rule 3: High reliability / fault tolerance → Voting or Fan-Out
|
|
92
|
+
if c.fault_tolerant:
|
|
93
|
+
return Recommendation(
|
|
94
|
+
pattern="voting",
|
|
95
|
+
reason="Fault-tolerant requirement → multiple independent agents with consensus",
|
|
96
|
+
estimated_calls=3,
|
|
97
|
+
estimated_cost_range="$0.006-0.012",
|
|
98
|
+
alternatives=["fan_out_fan_in"],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Rule 4: High quality → Reflection, Debate, or Evaluator
|
|
102
|
+
if c.quality in (Quality.HIGH, Quality.CRITICAL):
|
|
103
|
+
# Check for adversarial/debate keywords
|
|
104
|
+
if any(w in task_lower for w in ["compare", "pros and cons", "debate", "argue", "versus"]):
|
|
105
|
+
return Recommendation(
|
|
106
|
+
pattern="debate",
|
|
107
|
+
reason="High quality + adversarial task → structured debate with judge",
|
|
108
|
+
estimated_calls=7,
|
|
109
|
+
estimated_cost_range="$0.014-0.028",
|
|
110
|
+
alternatives=["evaluator_optimizer", "cross_reflection"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Check for code/writing that benefits from review
|
|
114
|
+
if any(w in task_lower for w in ["write", "code", "generate", "create", "draft"]):
|
|
115
|
+
return Recommendation(
|
|
116
|
+
pattern="self_reflection",
|
|
117
|
+
reason="High quality creative/code task → generate-critique-refine loop",
|
|
118
|
+
estimated_calls=4,
|
|
119
|
+
estimated_cost_range="$0.004-0.012",
|
|
120
|
+
alternatives=["cross_reflection", "evaluator_optimizer"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return Recommendation(
|
|
124
|
+
pattern="evaluator_optimizer",
|
|
125
|
+
reason="High quality task → explicit evaluation criteria with optimization",
|
|
126
|
+
estimated_calls=4,
|
|
127
|
+
estimated_cost_range="$0.004-0.008",
|
|
128
|
+
alternatives=["self_reflection"],
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Rule 5: Multi-step/complex → Supervisor, Hierarchical, or Pipeline
|
|
132
|
+
if c.multi_step or any(w in task_lower for w in ["steps", "process", "workflow", "pipeline"]):
|
|
133
|
+
if any(w in task_lower for w in ["team", "delegate", "manage", "coordinate"]):
|
|
134
|
+
return Recommendation(
|
|
135
|
+
pattern="hierarchical",
|
|
136
|
+
reason="Multi-step with team coordination → hierarchical delegation",
|
|
137
|
+
estimated_calls=7,
|
|
138
|
+
estimated_cost_range="$0.010-0.020",
|
|
139
|
+
alternatives=["supervisor", "orchestrator_workers"],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if any(w in task_lower for w in ["classify", "route", "triage", "categorize"]):
|
|
143
|
+
return Recommendation(
|
|
144
|
+
pattern="supervisor",
|
|
145
|
+
reason="Multi-step with classification → supervisor routes to specialists",
|
|
146
|
+
estimated_calls=3,
|
|
147
|
+
estimated_cost_range="$0.004-0.008",
|
|
148
|
+
alternatives=["pipeline"],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return Recommendation(
|
|
152
|
+
pattern="pipeline",
|
|
153
|
+
reason="Multi-step sequential task → stage-by-stage processing",
|
|
154
|
+
estimated_calls=4,
|
|
155
|
+
estimated_cost_range="$0.004-0.008",
|
|
156
|
+
alternatives=["supervisor"],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Default: Pipeline (safest general-purpose)
|
|
160
|
+
return Recommendation(
|
|
161
|
+
pattern="pipeline",
|
|
162
|
+
reason="General-purpose task → sequential pipeline with composable stages",
|
|
163
|
+
estimated_calls=2,
|
|
164
|
+
estimated_cost_range="$0.002-0.004",
|
|
165
|
+
alternatives=["supervisor", "self_reflection"],
|
|
166
|
+
)
|