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 ADDED
@@ -0,0 +1,6 @@
1
+ """Agent2 - A modular agent system framework for learning and research."""
2
+
3
+ from agent2.utils.config import Settings
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["Settings"]
@@ -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
@@ -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."""