agent2 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.
- agent2/__init__.py +6 -0
- agent2/agent/__init__.py +21 -0
- agent2/agent/base.py +131 -0
- agent2/agent/planner.py +251 -0
- agent2/agent/react.py +104 -0
- agent2/agent/reflection.py +117 -0
- agent2/app/__init__.py +1 -0
- agent2/app/chat.py +208 -0
- agent2/app/config.py +164 -0
- agent2/crew/__init__.py +13 -0
- agent2/crew/base.py +65 -0
- agent2/crew/debate.py +122 -0
- agent2/crew/sequential.py +54 -0
- agent2/crew/supervisor.py +140 -0
- agent2/llm/__init__.py +126 -0
- agent2/llm/anthropic.py +176 -0
- agent2/llm/base.py +85 -0
- agent2/llm/google.py +179 -0
- agent2/llm/message.py +177 -0
- agent2/llm/ollama.py +45 -0
- agent2/llm/openai.py +180 -0
- agent2/memory/__init__.py +11 -0
- agent2/memory/base.py +76 -0
- agent2/memory/longterm.py +230 -0
- agent2/memory/working.py +147 -0
- agent2/tools/__init__.py +18 -0
- agent2/tools/base.py +168 -0
- agent2/tools/builtin/__init__.py +7 -0
- agent2/tools/builtin/file_ops.py +70 -0
- agent2/tools/builtin/python_exec.py +78 -0
- agent2/tools/builtin/web_search.py +45 -0
- agent2/tools/registry.py +72 -0
- agent2/utils/__init__.py +1 -0
- agent2/utils/config.py +61 -0
- agent2/utils/logging.py +194 -0
- agent2-0.1.0.dist-info/METADATA +100 -0
- agent2-0.1.0.dist-info/RECORD +38 -0
- agent2-0.1.0.dist-info/WHEEL +4 -0
agent2/__init__.py
ADDED
agent2/agent/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Agent module — reasoning cores for LLM-powered agents.
|
|
2
|
+
|
|
3
|
+
Available agent types:
|
|
4
|
+
|
|
5
|
+
- :class:`ReActAgent` — Thought → Action → Observation loop
|
|
6
|
+
- :class:`PlannerAgent` — Plan-and-Execute pattern
|
|
7
|
+
- :class:`ReflectionMixin` — Self-critique mixin for any agent
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from agent2.agent.base import BaseAgent, MaxIterationsExceeded
|
|
11
|
+
from agent2.agent.react import ReActAgent
|
|
12
|
+
from agent2.agent.planner import PlannerAgent
|
|
13
|
+
from agent2.agent.reflection import ReflectionMixin
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BaseAgent",
|
|
17
|
+
"MaxIterationsExceeded",
|
|
18
|
+
"ReActAgent",
|
|
19
|
+
"PlannerAgent",
|
|
20
|
+
"ReflectionMixin",
|
|
21
|
+
]
|
agent2/agent/base.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Base Agent class — the foundation for all agent types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent2.llm.base import BaseLLM
|
|
9
|
+
from agent2.llm.message import Message
|
|
10
|
+
from agent2.tools.base import Tool
|
|
11
|
+
from agent2.tools.registry import ToolRegistry
|
|
12
|
+
from agent2.utils.config import settings
|
|
13
|
+
from agent2.utils.logging import AgentLogger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseAgent(ABC):
|
|
17
|
+
"""Abstract base class for all agents.
|
|
18
|
+
|
|
19
|
+
An agent combines an LLM with tools and a system prompt to perform
|
|
20
|
+
tasks through an iterative reasoning loop.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
name : str
|
|
25
|
+
Human-readable agent name.
|
|
26
|
+
llm : BaseLLM
|
|
27
|
+
The language model to use for reasoning.
|
|
28
|
+
system_prompt : str
|
|
29
|
+
Instructions defining the agent's role and behaviour.
|
|
30
|
+
tools : list[Tool] | None
|
|
31
|
+
Tools available to this agent.
|
|
32
|
+
max_iterations : int
|
|
33
|
+
Safety limit for the reasoning loop.
|
|
34
|
+
verbose : bool
|
|
35
|
+
Enable detailed logging of the reasoning process.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
name: str,
|
|
41
|
+
*,
|
|
42
|
+
llm: BaseLLM,
|
|
43
|
+
system_prompt: str = "You are a helpful AI assistant.",
|
|
44
|
+
tools: list[Tool] | None = None,
|
|
45
|
+
max_iterations: int | None = None,
|
|
46
|
+
verbose: bool | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.name = name
|
|
49
|
+
self.llm = llm
|
|
50
|
+
self.system_prompt = system_prompt
|
|
51
|
+
self.max_iterations = max_iterations or settings.agent_max_iterations
|
|
52
|
+
self.verbose = verbose if verbose is not None else settings.agent_verbose
|
|
53
|
+
|
|
54
|
+
# Set up tool registry
|
|
55
|
+
self.tool_registry = ToolRegistry()
|
|
56
|
+
if tools:
|
|
57
|
+
for t in tools:
|
|
58
|
+
self.tool_registry.register(t)
|
|
59
|
+
|
|
60
|
+
# Set up logger
|
|
61
|
+
self.log = AgentLogger(name, verbose=self.verbose)
|
|
62
|
+
|
|
63
|
+
# Conversation history for the current run
|
|
64
|
+
self._messages: list[Message] = []
|
|
65
|
+
|
|
66
|
+
# ── Public API ──────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
async def run(self, task: str) -> str:
|
|
69
|
+
"""Execute a task and return the final answer.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
task : str
|
|
74
|
+
The user's task or question.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
str
|
|
79
|
+
The agent's final response.
|
|
80
|
+
"""
|
|
81
|
+
self.log.start(task)
|
|
82
|
+
|
|
83
|
+
# Initialize conversation
|
|
84
|
+
self._messages = [
|
|
85
|
+
Message.system(self.system_prompt),
|
|
86
|
+
Message.user(task),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = await self._run_loop()
|
|
91
|
+
except MaxIterationsExceeded:
|
|
92
|
+
result = (
|
|
93
|
+
f"I was unable to complete the task within {self.max_iterations} steps. "
|
|
94
|
+
f"Here is what I've done so far based on the conversation."
|
|
95
|
+
)
|
|
96
|
+
self.log.observation(result, is_error=True)
|
|
97
|
+
|
|
98
|
+
self.log.finish(result)
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
# ── Abstract method for subclasses ──────────────────────────────
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def _run_loop(self) -> str:
|
|
105
|
+
"""The core reasoning loop. Subclasses implement this.
|
|
106
|
+
|
|
107
|
+
Returns the final answer string.
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
# ── Helpers ─────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async def _execute_tool_calls(self, tool_calls: list[Any]) -> list[Message]:
|
|
114
|
+
"""Execute a list of tool calls and return result messages."""
|
|
115
|
+
results: list[Message] = []
|
|
116
|
+
for tc in tool_calls:
|
|
117
|
+
self.log.action(tc.name, tc.arguments)
|
|
118
|
+
output = await self.tool_registry.execute(tc.name, **tc.arguments)
|
|
119
|
+
is_error = output.startswith("Error")
|
|
120
|
+
self.log.observation(output, is_error=is_error)
|
|
121
|
+
results.append(Message.tool(tc.id, output, is_error=is_error))
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
def __repr__(self) -> str:
|
|
125
|
+
tools = [t.name for t in self.tool_registry.list_tools()]
|
|
126
|
+
return f"{self.__class__.__name__}(name={self.name!r}, llm={self.llm!r}, tools={tools})"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MaxIterationsExceeded(Exception):
|
|
130
|
+
"""Raised when agent exceeds its maximum iteration count."""
|
|
131
|
+
pass
|
agent2/agent/planner.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Plan-and-Execute Agent — separates planning from execution.
|
|
2
|
+
|
|
3
|
+
The agent first generates a high-level plan (list of steps), then
|
|
4
|
+
executes each step sequentially. It can revise the plan dynamically
|
|
5
|
+
based on intermediate results.
|
|
6
|
+
|
|
7
|
+
This pattern improves reliability for complex, multi-step tasks
|
|
8
|
+
compared to pure ReAct.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from agent2.agent.base import BaseAgent, MaxIterationsExceeded
|
|
17
|
+
from agent2.llm.base import BaseLLM
|
|
18
|
+
from agent2.llm.message import Message
|
|
19
|
+
from agent2.tools.base import Tool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_PLANNER_PROMPT = """You are a planning agent. Given a task, create a detailed step-by-step plan.
|
|
23
|
+
|
|
24
|
+
Respond ONLY with a JSON array of strings, where each string is one step.
|
|
25
|
+
Example: ["Step 1: Search for ...", "Step 2: Analyse ...", "Step 3: Summarise ..."]
|
|
26
|
+
|
|
27
|
+
Keep the plan concise (3-7 steps). Each step should be actionable and specific.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
_EXECUTOR_PROMPT = """You are a helpful AI assistant executing one step of a larger plan.
|
|
31
|
+
|
|
32
|
+
The overall task is:
|
|
33
|
+
{task}
|
|
34
|
+
|
|
35
|
+
The full plan is:
|
|
36
|
+
{plan}
|
|
37
|
+
|
|
38
|
+
You are currently executing step {step_number}: {step_description}
|
|
39
|
+
|
|
40
|
+
Previous step results:
|
|
41
|
+
{previous_results}
|
|
42
|
+
|
|
43
|
+
Execute this step using the available tools. Provide your result clearly.
|
|
44
|
+
When done, respond with your findings (no tool calls).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_REPLANNER_PROMPT = """You are a replanning agent. Based on the results so far, decide if the remaining plan needs adjustment.
|
|
48
|
+
|
|
49
|
+
Original task: {task}
|
|
50
|
+
Original plan: {plan}
|
|
51
|
+
Completed steps and results: {completed}
|
|
52
|
+
Remaining steps: {remaining}
|
|
53
|
+
|
|
54
|
+
If the remaining plan is still good, respond with: {{"action": "continue"}}
|
|
55
|
+
If you need to revise, respond with: {{"action": "revise", "new_remaining_steps": ["step 1", "step 2", ...]}}
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PlannerAgent(BaseAgent):
|
|
60
|
+
"""Agent using the Plan-and-Execute pattern.
|
|
61
|
+
|
|
62
|
+
1. Generate a plan (list of steps)
|
|
63
|
+
2. Execute each step using a ReAct-style sub-loop
|
|
64
|
+
3. Optionally re-plan after each step
|
|
65
|
+
|
|
66
|
+
Usage::
|
|
67
|
+
|
|
68
|
+
agent = PlannerAgent("planner", llm=llm, tools=[web_search])
|
|
69
|
+
result = await agent.run("Compare Python and Rust for web development")
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
name: str,
|
|
75
|
+
*,
|
|
76
|
+
llm: BaseLLM,
|
|
77
|
+
system_prompt: str | None = None,
|
|
78
|
+
tools: list[Tool] | None = None,
|
|
79
|
+
max_iterations: int | None = None,
|
|
80
|
+
verbose: bool | None = None,
|
|
81
|
+
enable_replan: bool = True,
|
|
82
|
+
max_step_iterations: int = 5,
|
|
83
|
+
) -> None:
|
|
84
|
+
super().__init__(
|
|
85
|
+
name,
|
|
86
|
+
llm=llm,
|
|
87
|
+
system_prompt=system_prompt or "You are a helpful planning assistant.",
|
|
88
|
+
tools=tools,
|
|
89
|
+
max_iterations=max_iterations,
|
|
90
|
+
verbose=verbose,
|
|
91
|
+
)
|
|
92
|
+
self.enable_replan = enable_replan
|
|
93
|
+
self.max_step_iterations = max_step_iterations
|
|
94
|
+
|
|
95
|
+
async def _run_loop(self) -> str:
|
|
96
|
+
"""Plan → Execute each step → Synthesise."""
|
|
97
|
+
task = self._messages[-1].content or ""
|
|
98
|
+
|
|
99
|
+
# Phase 1: Generate plan
|
|
100
|
+
plan = await self._generate_plan(task)
|
|
101
|
+
self.log.plan(plan)
|
|
102
|
+
|
|
103
|
+
# Phase 2: Execute steps
|
|
104
|
+
step_results: list[dict[str, str]] = []
|
|
105
|
+
|
|
106
|
+
while plan:
|
|
107
|
+
step_desc = plan.pop(0)
|
|
108
|
+
step_num = len(step_results) + 1
|
|
109
|
+
self.log.plan_step_start(step_num, step_desc)
|
|
110
|
+
|
|
111
|
+
result = await self._execute_step(
|
|
112
|
+
task=task,
|
|
113
|
+
plan_overview=[r["step"] for r in step_results] + [step_desc] + plan,
|
|
114
|
+
step_number=step_num,
|
|
115
|
+
step_description=step_desc,
|
|
116
|
+
previous_results=step_results,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
step_results.append({"step": step_desc, "result": result})
|
|
120
|
+
self.log.plan_step_done(step_num)
|
|
121
|
+
|
|
122
|
+
# Phase 2.5: Optionally re-plan
|
|
123
|
+
if self.enable_replan and plan:
|
|
124
|
+
plan = await self._maybe_replan(task, step_results, plan)
|
|
125
|
+
|
|
126
|
+
# Phase 3: Synthesise final answer
|
|
127
|
+
return await self._synthesise(task, step_results)
|
|
128
|
+
|
|
129
|
+
async def _generate_plan(self, task: str) -> list[str]:
|
|
130
|
+
"""Use the LLM to create an execution plan."""
|
|
131
|
+
response = await self.llm.chat([
|
|
132
|
+
Message.system(_PLANNER_PROMPT),
|
|
133
|
+
Message.user(task),
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
content = response.content or "[]"
|
|
137
|
+
# Extract JSON from possible markdown code blocks
|
|
138
|
+
if "```" in content:
|
|
139
|
+
content = content.split("```")[1]
|
|
140
|
+
if content.startswith("json"):
|
|
141
|
+
content = content[4:]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
plan = json.loads(content.strip())
|
|
145
|
+
if isinstance(plan, list):
|
|
146
|
+
return [str(s) for s in plan]
|
|
147
|
+
except json.JSONDecodeError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# Fallback: split by newlines
|
|
151
|
+
return [line.strip() for line in content.strip().split("\n") if line.strip()]
|
|
152
|
+
|
|
153
|
+
async def _execute_step(
|
|
154
|
+
self,
|
|
155
|
+
task: str,
|
|
156
|
+
plan_overview: list[str],
|
|
157
|
+
step_number: int,
|
|
158
|
+
step_description: str,
|
|
159
|
+
previous_results: list[dict[str, str]],
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Execute a single plan step using a mini ReAct loop."""
|
|
162
|
+
prev_text = "\n".join(
|
|
163
|
+
f"Step {i+1} ({r['step']}): {r['result'][:200]}"
|
|
164
|
+
for i, r in enumerate(previous_results)
|
|
165
|
+
) or "None yet."
|
|
166
|
+
|
|
167
|
+
system = _EXECUTOR_PROMPT.format(
|
|
168
|
+
task=task,
|
|
169
|
+
plan="\n".join(f"{i+1}. {s}" for i, s in enumerate(plan_overview)),
|
|
170
|
+
step_number=step_number,
|
|
171
|
+
step_description=step_description,
|
|
172
|
+
previous_results=prev_text,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
messages = [
|
|
176
|
+
Message.system(system),
|
|
177
|
+
Message.user(f"Execute step {step_number}: {step_description}"),
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
tool_schemas = self.tool_registry.list_schemas() or None
|
|
181
|
+
|
|
182
|
+
for _ in range(self.max_step_iterations):
|
|
183
|
+
response = await self.llm.chat(messages, tools=tool_schemas)
|
|
184
|
+
|
|
185
|
+
if response.has_tool_calls:
|
|
186
|
+
if response.content:
|
|
187
|
+
self.log.thought(response.content)
|
|
188
|
+
messages.append(response.message)
|
|
189
|
+
tool_results = await self._execute_tool_calls(response.tool_calls)
|
|
190
|
+
messages.extend(tool_results)
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
return response.content or ""
|
|
194
|
+
|
|
195
|
+
return "(Step execution reached iteration limit)"
|
|
196
|
+
|
|
197
|
+
async def _maybe_replan(
|
|
198
|
+
self,
|
|
199
|
+
task: str,
|
|
200
|
+
completed: list[dict[str, str]],
|
|
201
|
+
remaining: list[str],
|
|
202
|
+
) -> list[str]:
|
|
203
|
+
"""Ask the LLM if the remaining plan needs revision."""
|
|
204
|
+
prompt = _REPLANNER_PROMPT.format(
|
|
205
|
+
task=task,
|
|
206
|
+
plan="(see completed + remaining)",
|
|
207
|
+
completed=json.dumps(completed, ensure_ascii=False, indent=2),
|
|
208
|
+
remaining=json.dumps(remaining, ensure_ascii=False),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
response = await self.llm.chat([
|
|
212
|
+
Message.system("You are a replanning agent. Respond in JSON only."),
|
|
213
|
+
Message.user(prompt),
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
content = response.content or ""
|
|
217
|
+
try:
|
|
218
|
+
data = json.loads(content.strip())
|
|
219
|
+
if data.get("action") == "revise" and "new_remaining_steps" in data:
|
|
220
|
+
new_plan = [str(s) for s in data["new_remaining_steps"]]
|
|
221
|
+
self.log.plan(new_plan)
|
|
222
|
+
return new_plan
|
|
223
|
+
except (json.JSONDecodeError, AttributeError):
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
return remaining
|
|
227
|
+
|
|
228
|
+
async def _synthesise(
|
|
229
|
+
self, task: str, step_results: list[dict[str, str]]
|
|
230
|
+
) -> str:
|
|
231
|
+
"""Synthesise a final answer from all step results."""
|
|
232
|
+
results_text = "\n\n".join(
|
|
233
|
+
f"### Step {i+1}: {r['step']}\n{r['result']}"
|
|
234
|
+
for i, r in enumerate(step_results)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
response = await self.llm.chat([
|
|
238
|
+
Message.system(
|
|
239
|
+
"Synthesise all the step results into a clear, comprehensive final answer. "
|
|
240
|
+
"Be concise but thorough."
|
|
241
|
+
),
|
|
242
|
+
Message.user(
|
|
243
|
+
f"Original task: {task}\n\n"
|
|
244
|
+
f"Step results:\n{results_text}\n\n"
|
|
245
|
+
f"Provide a final comprehensive answer."
|
|
246
|
+
),
|
|
247
|
+
])
|
|
248
|
+
|
|
249
|
+
answer = response.content or ""
|
|
250
|
+
self.log.final_answer(answer)
|
|
251
|
+
return answer
|
agent2/agent/react.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""ReAct Agent — Reasoning + Acting pattern.
|
|
2
|
+
|
|
3
|
+
The ReAct loop alternates between:
|
|
4
|
+
1. **Thought**: The LLM reasons about what to do next
|
|
5
|
+
2. **Action**: The LLM calls a tool
|
|
6
|
+
3. **Observation**: The tool result is fed back to the LLM
|
|
7
|
+
4. **Repeat** until the LLM provides a final answer (no tool calls)
|
|
8
|
+
|
|
9
|
+
Reference: Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models" (2022)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from agent2.agent.base import BaseAgent, MaxIterationsExceeded
|
|
17
|
+
from agent2.llm.base import BaseLLM
|
|
18
|
+
from agent2.llm.message import Message
|
|
19
|
+
from agent2.tools.base import Tool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_DEFAULT_REACT_PROMPT = """You are a helpful AI assistant that can use tools to accomplish tasks.
|
|
23
|
+
|
|
24
|
+
When you need information or need to perform an action, use the available tools.
|
|
25
|
+
Think step by step about what you need to do.
|
|
26
|
+
When you have gathered enough information to answer the user's question, provide your final answer directly without calling any more tools.
|
|
27
|
+
|
|
28
|
+
Important:
|
|
29
|
+
- Always think before acting.
|
|
30
|
+
- Use tools when you need external information or capabilities.
|
|
31
|
+
- When you're ready to give the final answer, respond with text only (no tool calls).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReActAgent(BaseAgent):
|
|
36
|
+
"""Agent implementing the ReAct (Reasoning + Acting) pattern.
|
|
37
|
+
|
|
38
|
+
This is the most fundamental agent type. It iteratively reasons
|
|
39
|
+
and acts until it arrives at a final answer.
|
|
40
|
+
|
|
41
|
+
Usage::
|
|
42
|
+
|
|
43
|
+
from agent2.llm import create_llm
|
|
44
|
+
from agent2.agent import ReActAgent
|
|
45
|
+
from agent2.tools.builtin import web_search
|
|
46
|
+
|
|
47
|
+
llm = create_llm("openai", model="gpt-4o-mini")
|
|
48
|
+
agent = ReActAgent("researcher", llm=llm, tools=[web_search])
|
|
49
|
+
result = await agent.run("What is the capital of France?")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
name: str,
|
|
55
|
+
*,
|
|
56
|
+
llm: BaseLLM,
|
|
57
|
+
system_prompt: str | None = None,
|
|
58
|
+
tools: list[Tool] | None = None,
|
|
59
|
+
max_iterations: int | None = None,
|
|
60
|
+
verbose: bool | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(
|
|
63
|
+
name,
|
|
64
|
+
llm=llm,
|
|
65
|
+
system_prompt=system_prompt or _DEFAULT_REACT_PROMPT,
|
|
66
|
+
tools=tools,
|
|
67
|
+
max_iterations=max_iterations,
|
|
68
|
+
verbose=verbose,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def _run_loop(self) -> str:
|
|
72
|
+
"""Execute the ReAct loop: Thought → Action → Observation → repeat."""
|
|
73
|
+
tool_schemas = self.tool_registry.list_schemas() or None
|
|
74
|
+
|
|
75
|
+
for iteration in range(1, self.max_iterations + 1):
|
|
76
|
+
# Ask the LLM to think and optionally call tools
|
|
77
|
+
response = await self.llm.chat(
|
|
78
|
+
self._messages,
|
|
79
|
+
tools=tool_schemas,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# If the LLM wants to call tools → Action + Observation
|
|
83
|
+
if response.has_tool_calls:
|
|
84
|
+
# Log the thinking (if any content accompanies tool calls)
|
|
85
|
+
if response.content:
|
|
86
|
+
self.log.thought(response.content)
|
|
87
|
+
|
|
88
|
+
# Record the assistant message with tool calls
|
|
89
|
+
self._messages.append(response.message)
|
|
90
|
+
|
|
91
|
+
# Execute each tool call
|
|
92
|
+
tool_results = await self._execute_tool_calls(response.tool_calls)
|
|
93
|
+
self._messages.extend(tool_results)
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# No tool calls → this is the final answer
|
|
97
|
+
final_answer = response.content or ""
|
|
98
|
+
self.log.final_answer(final_answer)
|
|
99
|
+
return final_answer
|
|
100
|
+
|
|
101
|
+
# Exceeded max iterations
|
|
102
|
+
raise MaxIterationsExceeded(
|
|
103
|
+
f"Agent '{self.name}' exceeded {self.max_iterations} iterations"
|
|
104
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Reflection mixin — self-critique and iterative refinement.
|
|
2
|
+
|
|
3
|
+
Adds a reflection capability that lets an agent evaluate its own output
|
|
4
|
+
and retry if quality is insufficient. Can be mixed into any agent type.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agent2.llm.base import BaseLLM
|
|
12
|
+
from agent2.llm.message import Message
|
|
13
|
+
from agent2.utils.logging import AgentLogger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_REFLECTION_PROMPT = """You are a critical reviewer. Evaluate the following response to the given task.
|
|
17
|
+
|
|
18
|
+
Task: {task}
|
|
19
|
+
Response: {response}
|
|
20
|
+
|
|
21
|
+
Evaluate the response on these criteria:
|
|
22
|
+
1. **Correctness**: Is the information accurate?
|
|
23
|
+
2. **Completeness**: Does it fully address the task?
|
|
24
|
+
3. **Clarity**: Is it well-structured and easy to understand?
|
|
25
|
+
|
|
26
|
+
Respond in this exact JSON format:
|
|
27
|
+
{{
|
|
28
|
+
"score": <1-10>,
|
|
29
|
+
"passed": <true if score >= 7, false otherwise>,
|
|
30
|
+
"feedback": "<specific suggestions for improvement>"
|
|
31
|
+
}}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ReflectionMixin:
|
|
36
|
+
"""Mixin that adds self-reflection capability to any agent.
|
|
37
|
+
|
|
38
|
+
After the agent produces an answer, it evaluates the answer and
|
|
39
|
+
retries with the feedback if the quality is below threshold.
|
|
40
|
+
|
|
41
|
+
Usage::
|
|
42
|
+
|
|
43
|
+
class MyReflectiveAgent(ReflectionMixin, ReActAgent):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
agent = MyReflectiveAgent("reflector", llm=llm, max_reflections=2)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
max_reflections: int = 2
|
|
50
|
+
reflection_threshold: int = 7 # Minimum score (1-10) to pass
|
|
51
|
+
|
|
52
|
+
async def run(self, task: str) -> str:
|
|
53
|
+
"""Override run() to add reflection loop."""
|
|
54
|
+
# Get the base agent's run result
|
|
55
|
+
result = await super().run(task) # type: ignore[misc]
|
|
56
|
+
|
|
57
|
+
# Access the logger from the base agent
|
|
58
|
+
log: AgentLogger = getattr(self, "log", AgentLogger("reflection"))
|
|
59
|
+
llm: BaseLLM = getattr(self, "llm")
|
|
60
|
+
|
|
61
|
+
for attempt in range(self.max_reflections):
|
|
62
|
+
evaluation = await self._reflect(llm, task, result)
|
|
63
|
+
|
|
64
|
+
if evaluation.get("passed", True):
|
|
65
|
+
return result
|
|
66
|
+
|
|
67
|
+
feedback = evaluation.get("feedback", "Try to improve your response.")
|
|
68
|
+
score = evaluation.get("score", "?")
|
|
69
|
+
log.thought(
|
|
70
|
+
f"Reflection {attempt + 1}/{self.max_reflections}: "
|
|
71
|
+
f"Score {score}/10 — {feedback}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Retry with feedback
|
|
75
|
+
result = await self._retry_with_feedback(llm, task, result, feedback)
|
|
76
|
+
log.final_answer(result)
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
async def _reflect(
|
|
82
|
+
llm: BaseLLM, task: str, response: str
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Ask the LLM to evaluate a response."""
|
|
85
|
+
import json
|
|
86
|
+
|
|
87
|
+
prompt = _REFLECTION_PROMPT.format(task=task, response=response)
|
|
88
|
+
llm_response = await llm.chat([
|
|
89
|
+
Message.system("You are a critical reviewer. Respond in JSON only."),
|
|
90
|
+
Message.user(prompt),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
content = llm_response.content or ""
|
|
94
|
+
try:
|
|
95
|
+
return json.loads(content.strip())
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
# If parsing fails, assume it passed
|
|
98
|
+
return {"score": 10, "passed": True, "feedback": ""}
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
async def _retry_with_feedback(
|
|
102
|
+
llm: BaseLLM, task: str, previous: str, feedback: str
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Generate an improved response incorporating feedback."""
|
|
105
|
+
response = await llm.chat([
|
|
106
|
+
Message.system(
|
|
107
|
+
"You previously answered a task but your answer needs improvement. "
|
|
108
|
+
"Generate an improved response based on the feedback."
|
|
109
|
+
),
|
|
110
|
+
Message.user(
|
|
111
|
+
f"Original task: {task}\n\n"
|
|
112
|
+
f"Your previous answer:\n{previous}\n\n"
|
|
113
|
+
f"Reviewer feedback:\n{feedback}\n\n"
|
|
114
|
+
f"Please provide an improved answer."
|
|
115
|
+
),
|
|
116
|
+
])
|
|
117
|
+
return response.content or previous
|
agent2/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Agent2 applications — CLI tools built on the agent2 framework."""
|