mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-31 03:12:16 +00:00
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
6.2 KiB
Python
197 lines
6.2 KiB
Python
"""NativeReActAgent -- Thought-Action-Observation loop agent.
|
|
|
|
Renamed from ``ReActAgent`` to clarify this is OpenJarvis's native
|
|
implementation, not an integration with an external project.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any, List, Optional
|
|
|
|
from openjarvis.agents._stubs import AgentContext, AgentResult, ToolUsingAgent
|
|
from openjarvis.core.events import EventBus
|
|
from openjarvis.core.registry import AgentRegistry
|
|
from openjarvis.core.types import Message, Role, ToolCall, ToolResult
|
|
from openjarvis.engine._stubs import InferenceEngine
|
|
from openjarvis.tools._stubs import BaseTool, build_tool_descriptions
|
|
|
|
REACT_SYSTEM_PROMPT = """\
|
|
You are a ReAct agent. For each step, respond with exactly one of:
|
|
|
|
1. To think and act:
|
|
Thought: <your reasoning>
|
|
Action: <tool_name>
|
|
Action Input: <json arguments>
|
|
|
|
2. To give a final answer:
|
|
Thought: <your reasoning>
|
|
Final Answer: <your answer>
|
|
|
|
{tool_descriptions}"""
|
|
|
|
|
|
@AgentRegistry.register("native_react")
|
|
class NativeReActAgent(ToolUsingAgent):
|
|
"""ReAct agent: Thought -> Action -> Observation loop."""
|
|
|
|
agent_id = "native_react"
|
|
_default_temperature = 0.7
|
|
_default_max_tokens = 1024
|
|
_default_max_turns = 10
|
|
|
|
def __init__(
|
|
self,
|
|
engine: InferenceEngine,
|
|
model: str,
|
|
*,
|
|
tools: Optional[List[BaseTool]] = None,
|
|
bus: Optional[EventBus] = None,
|
|
max_turns: Optional[int] = None,
|
|
temperature: Optional[float] = None,
|
|
max_tokens: Optional[int] = None,
|
|
interactive: bool = False,
|
|
confirm_callback=None,
|
|
) -> None:
|
|
super().__init__(
|
|
engine,
|
|
model,
|
|
tools=tools,
|
|
bus=bus,
|
|
max_turns=max_turns,
|
|
temperature=temperature,
|
|
max_tokens=max_tokens,
|
|
interactive=interactive,
|
|
confirm_callback=confirm_callback,
|
|
)
|
|
|
|
def _parse_response(self, text: str) -> dict:
|
|
"""Parse ReAct structured output."""
|
|
result = {"thought": "", "action": "", "action_input": "", "final_answer": ""}
|
|
|
|
# Extract Thought
|
|
thought_match = re.search(
|
|
r"Thought:\s*(.+?)(?=\nAction:|\nFinal Answer:|\Z)",
|
|
text,
|
|
re.DOTALL | re.IGNORECASE,
|
|
)
|
|
if thought_match:
|
|
result["thought"] = thought_match.group(1).strip()
|
|
|
|
# Check for Final Answer
|
|
final_match = re.search(
|
|
r"Final Answer:\s*(.+)", text, re.DOTALL | re.IGNORECASE
|
|
)
|
|
if final_match:
|
|
result["final_answer"] = final_match.group(1).strip()
|
|
return result
|
|
|
|
# Extract Action and Action Input
|
|
action_match = re.search(r"Action:\s*(.+)", text, re.IGNORECASE)
|
|
if action_match:
|
|
result["action"] = action_match.group(1).strip()
|
|
|
|
input_match = re.search(
|
|
r"Action Input:\s*(.+?)(?=\n\n|\nThought:|\Z)",
|
|
text,
|
|
re.DOTALL | re.IGNORECASE,
|
|
)
|
|
if input_match:
|
|
result["action_input"] = input_match.group(1).strip()
|
|
|
|
return result
|
|
|
|
def run(
|
|
self,
|
|
input: str,
|
|
context: Optional[AgentContext] = None,
|
|
**kwargs: Any,
|
|
) -> AgentResult:
|
|
self._emit_turn_start(input)
|
|
|
|
# Build system prompt with rich tool descriptions
|
|
tool_desc = build_tool_descriptions(self._tools)
|
|
system_prompt = REACT_SYSTEM_PROMPT.format(tool_descriptions=tool_desc)
|
|
|
|
messages = self._build_messages(input, context, system_prompt=system_prompt)
|
|
|
|
all_tool_results: list[ToolResult] = []
|
|
turns = 0
|
|
total_usage: dict[str, int] = {
|
|
"prompt_tokens": 0,
|
|
"completion_tokens": 0,
|
|
"total_tokens": 0,
|
|
}
|
|
|
|
for _turn in range(self._max_turns):
|
|
turns += 1
|
|
|
|
if self._loop_guard:
|
|
messages = self._loop_guard.compress_context(messages)
|
|
|
|
result = self._generate(messages)
|
|
usage = result.get("usage", {})
|
|
for k in total_usage:
|
|
total_usage[k] += usage.get(k, 0)
|
|
|
|
content = result.get("content", "")
|
|
parsed = self._parse_response(content)
|
|
|
|
# Final answer?
|
|
if parsed["final_answer"]:
|
|
self._emit_turn_end(turns=turns)
|
|
return AgentResult(
|
|
content=parsed["final_answer"],
|
|
tool_results=all_tool_results,
|
|
turns=turns,
|
|
metadata=total_usage,
|
|
)
|
|
|
|
# No action? Treat content as final answer
|
|
if not parsed["action"]:
|
|
self._emit_turn_end(turns=turns)
|
|
return AgentResult(
|
|
content=content,
|
|
tool_results=all_tool_results,
|
|
turns=turns,
|
|
metadata=total_usage,
|
|
)
|
|
|
|
# Execute action
|
|
messages.append(Message(role=Role.ASSISTANT, content=content))
|
|
|
|
tool_call = ToolCall(
|
|
id=f"react_{turns}",
|
|
name=parsed["action"],
|
|
arguments=parsed["action_input"] or "{}",
|
|
)
|
|
|
|
# Loop guard check before execution
|
|
if self._loop_guard:
|
|
verdict = self._loop_guard.check_call(
|
|
tool_call.name,
|
|
tool_call.arguments,
|
|
)
|
|
if verdict.blocked:
|
|
tool_result = ToolResult(
|
|
tool_name=tool_call.name,
|
|
content=f"Loop guard: {verdict.reason}",
|
|
success=False,
|
|
)
|
|
all_tool_results.append(tool_result)
|
|
observation = f"Observation: {tool_result.content}"
|
|
messages.append(Message(role=Role.USER, content=observation))
|
|
continue
|
|
|
|
tool_result = self._executor.execute(tool_call)
|
|
all_tool_results.append(tool_result)
|
|
|
|
observation = f"Observation: {tool_result.content}"
|
|
messages.append(Message(role=Role.USER, content=observation))
|
|
|
|
# Max turns exceeded
|
|
return self._max_turns_result(all_tool_results, turns, metadata=total_usage)
|
|
|
|
|
|
__all__ = ["NativeReActAgent", "REACT_SYSTEM_PROMPT"]
|