add agent_core.py
This commit is contained in:
+102
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import json
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
class DeepAgent:
|
||||
"""Simple ReAct style agent implemented from scratch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
llm: callable
|
||||
A callable that accepts a list of messages and returns a dict with
|
||||
a ``content`` field containing the LLM response.
|
||||
tools: list
|
||||
List of tool callables. Each tool must be a function that accepts
|
||||
a single string argument and returns a string.
|
||||
"""
|
||||
|
||||
def __init__(self, llm, tools: List):
|
||||
self.llm = llm
|
||||
self.tools = {t.__name__: t for t in tools}
|
||||
self.history: List[dict] = []
|
||||
|
||||
def _format_tools_prompt(self) -> str:
|
||||
"""Return a human‑readable description of available tools.
|
||||
|
||||
The format is used in the system prompt so the LLM knows what it can
|
||||
call. Each tool is described by its name and the first line of its
|
||||
docstring.
|
||||
"""
|
||||
lines = ["Available tools:"]
|
||||
for name, func in self.tools.items():
|
||||
doc = func.__doc__ or "No description"
|
||||
first_line = doc.strip().split("\n")[0]
|
||||
lines.append(f"- {name}: {first_line}")
|
||||
return "\n".join(lines)
|
||||
|
||||
def _parse_action(self, text: str) -> Optional[Tuple[str, str]]:
|
||||
"""Parse an Action and Action Input from a ReAct response.
|
||||
|
||||
Expected format (case‑insensitive):
|
||||
Action: <tool_name>
|
||||
Action Input: <json or plain string>
|
||||
The method returns a tuple ``(tool_name, action_input)`` or
|
||||
``None`` if the pattern is not found.
|
||||
"""
|
||||
import re
|
||||
pattern = r"(?i)Action:\s*(?P<tool>\w+)\s*\nAction\s+Input:\s*(?P<input>.+)"
|
||||
match = re.search(pattern, text, re.DOTALL)
|
||||
if not match:
|
||||
return None
|
||||
tool = match.group("tool").strip()
|
||||
action_input = match.group("input").strip()
|
||||
return tool, action_input
|
||||
|
||||
def run(self, query: str) -> str:
|
||||
"""Execute a ReAct loop until the LLM returns a FINAL ANSWER.
|
||||
|
||||
The method returns the final answer string.
|
||||
"""
|
||||
# System prompt with tool descriptions
|
||||
system_prompt = (
|
||||
"You are a helpful assistant that can use the following tools. "
|
||||
"When you need to perform an action, output the tool name and the input. "
|
||||
"When you are finished, output FINAL ANSWER."
|
||||
f"\n\n{self._format_tools_prompt()}"
|
||||
)
|
||||
self.history = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query},
|
||||
]
|
||||
|
||||
while True:
|
||||
# Ask LLM for next step
|
||||
response = self.llm(self.history)
|
||||
content = response.get("content", "")
|
||||
# Append LLM output to history
|
||||
self.history.append({"role": "assistant", "content": content})
|
||||
|
||||
# Check for FINAL ANSWER
|
||||
if "FINAL ANSWER:" in content.upper():
|
||||
# Extract everything after FINAL ANSWER:
|
||||
final = content.split("FINAL ANSWER:", 1)[1].strip()
|
||||
return final
|
||||
|
||||
# Try to parse an action
|
||||
parsed = self._parse_action(content)
|
||||
if not parsed:
|
||||
# If no action found, continue loop (LLM may just think)
|
||||
continue
|
||||
tool_name, action_input = parsed
|
||||
tool = self.tools.get(tool_name)
|
||||
if not tool:
|
||||
observation = f"Error: unknown tool {tool_name}"
|
||||
else:
|
||||
try:
|
||||
observation = tool(action_input)
|
||||
except Exception as e:
|
||||
observation = f"Error executing {tool_name}: {e}"
|
||||
# Append observation
|
||||
self.history.append({"role": "assistant", "content": f"Observation: {observation}"})
|
||||
|
||||
# End of DeepAgent class
|
||||
Reference in New Issue
Block a user