A self-contained ReAct (Reasoning + Acting) agent for the Autourgos framework.
ReAct is an agent pattern where the model alternates between Thought (reasoning about what to do next) and Action (calling a tool), looping until it has enough information to give a Final Answer.
Fully self-contained — no autourgos-core dependency. Zero required dependencies beyond Python 3.10+. Plug in any LLM wrapper you already have.
Almost every major LLM provider today exposes an OpenAI-compatible API. autourgos-react-agent was designed with this in mind — it works with any LLM that has .invoke() and .ainvoke() methods. You are not locked to a single provider.
OpenAI (gpt-4o, gpt-4o-mini, ...) ─────────┐
Groq (Llama 3, Mixtral, Gemma) ─────────────┤
Together AI (100+ open-source models) ──────┤ autourgos-react-agent
Mistral AI (mistral-large, codestral) ──────┤ (one agent, any LLM)
DeepSeek (deepseek-chat, reasoner) ─────────┤
Perplexity (sonar, web-connected) ──────────┤
Ollama — local models, no internet ─────────┤
LM Studio — local models, GUI-based ────────┤
vLLM — self-hosted, high throughput ────────┘
What does it do?
The agent receives a task and a list of tools. It then iterates:
- Think — what information do I need? which tool should I call?
- Act — call the tool, get the result
- Observe — add the result to the scratchpad, repeat
This continues until the agent has a final answer or hits the iteration/time limit.
- Install
- Quick Start
- How the ReAct Loop Works
- Defining Tools
- Works With Any LLM
- Async Agent
- Parallel Tool Calls
- Verbose Mode
- Memory
- Approval Callback
- Middleware / Callbacks
- Context Manager
- Time and Iteration Limits
- Custom System Prompt
- Constructor Reference
- Tool Dict Reference
- What the Agent Returns
- Error Tags
- v1 Backward Compatibility
pip install autourgos-react-agentNo required runtime dependencies. Bring your own LLM wrapper:
pip install autourgos-openaichat # Chat Completions API
# or
pip install autourgos-responses # OpenAI Responses APIRequires Python 3.10+.
from autourgos_react_agent import ReactAgent
from autourgos_openaichat import OpenAIChatModel
# 1. Define a tool
def add(a: float, b: float) -> float:
return a + b
calculator_tool = {
"name": "calculator",
"description": "Add two numbers together.",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"},
},
"required": ["a", "b"],
},
"func": add,
}
# 2. Create the agent
agent = ReactAgent(
llm=OpenAIChatModel(model="gpt-4o"),
verbose=True,
)
agent.add_tools(calculator_tool)
# 3. Run
result = agent.invoke("What is 123 + 456?")
print(result)
# 579Expected verbose output:
[ReactAgent] Step 1 | Thought: I need to add 123 and 456. I'll use the calculator tool.
[ReactAgent] Step 1 | Action: calculator({'a': 123, 'b': 456})
[ReactAgent] Step 1 | Observation [calculator]: 579.0
[ReactAgent] Step 2 | Thought: I have the result from the calculator.
[ReactAgent] Final Answer: 123 + 456 = 579
Each iteration the agent produces a JSON object:
{
"thought": "I need to search for the latest Python version.",
"actions": [
{"action": "search", "action_input": {"query": "latest Python version 2025"}}
],
"final_answer": null
}Rules the LLM must follow (enforced by the prompt):
- If tools are needed → fill
actions, setfinal_answertonull - If the answer is ready → fill
final_answer, setactionsto[] - Never set both
actionsandfinal_answerat the same time - Multiple tools can be called in one step if they are independent
The agent collects tool results into a scratchpad that is passed back to the LLM at each step so it always has full context of what was tried.
A tool is a plain Python dict with these keys:
| Key | Type | Required | Description |
|---|---|---|---|
name |
str |
yes | Tool name (used by the LLM to call it) |
description |
str |
yes | What the tool does — shown to the LLM |
parameters |
dict |
recommended | JSON-Schema object describing the inputs |
func |
callable |
yes | Python function to call |
tool = {
"name": "greet",
"description": "Return a greeting for a given name.",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Person's name"},
},
"required": ["name"],
},
"func": lambda name: f"Hello, {name}!",
}def get_weather(city: str, unit: str = "celsius") -> str:
# Replace with real API call
return f"The weather in {city} is 22°{unit[0].upper()} and sunny."
weather_tool = {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Tokyo"},
"unit": {"type": "string", "description": "celsius or fahrenheit"},
},
"required": ["city"],
},
"func": get_weather,
}# One at a time
agent.add_tools(weather_tool)
# Multiple at once
agent.add_tools(weather_tool, calculator_tool, search_tool)
# From a list
agent.add_tools([weather_tool, calculator_tool])
# Via constructor
agent = ReactAgent(llm=llm, tools=[weather_tool, calculator_tool])Change the llm= argument to switch providers. Everything else stays the same.
from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(llm=OpenAIChatModel(model="gpt-4o", api_key="sk-..."))from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="llama3-70b-8192",
api_key="gsk_...",
base_url="https://api.groq.com/openai/v1",
)
)ollama pull llama3from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="llama3",
api_key="ollama",
base_url="http://localhost:11434/v1",
)
)from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="meta-llama/Llama-3-70b-chat-hf",
api_key="...",
base_url="https://api.together.xyz/v1",
)
)from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="mistral-large-latest",
api_key="...",
base_url="https://api.mistral.ai/v1",
)
)from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="deepseek-chat",
api_key="...",
base_url="https://api.deepseek.com/v1",
)
)from autourgos_responses import OpenAIResponse
agent = ReactAgent(llm=OpenAIResponse(model="gpt-4o"))from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="local-model",
api_key="lm-studio",
base_url="http://localhost:1234/v1",
)
)from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(
llm=OpenAIChatModel(
model="meta-llama/Meta-Llama-3-8B-Instruct",
api_key="EMPTY",
base_url="http://your-server:8000/v1",
)
)All agent methods have an async counterpart.
import asyncio
from autourgos_react_agent import ReactAgent
from autourgos_openaichat import OpenAIChatModel
agent = ReactAgent(llm=OpenAIChatModel(model="gpt-4o"))
agent.add_tools(weather_tool, calculator_tool)
async def main():
result = await agent.ainvoke("What is the weather in Tokyo and what is 99 * 3?")
print(result)
# The weather in Tokyo is 22°C and sunny. 99 × 3 = 297.
asyncio.run(main())Async tools (coroutine functions) are also supported:
import httpx
async def async_search(query: str) -> str:
async with httpx.AsyncClient() as client:
r = await client.get(f"https://api.example.com/search?q={query}")
return r.text
search_tool = {
"name": "search",
"description": "Search the web.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search terms"},
},
"required": ["query"],
},
"func": async_search, # async function — works with ainvoke()
}The LLM can call multiple tools in a single step when they don't depend on each other. The agent executes them sequentially and collects all results before the next LLM call.
agent = ReactAgent(llm=OpenAIChatModel(model="gpt-4o"))
agent.add_tools(weather_tool, calculator_tool, search_tool)
result = agent.invoke(
"What is the weather in Paris and London, and what is 250 * 4?"
)
print(result)
# The weather in Paris is 18°C cloudy, London is 15°C rainy. 250 × 4 = 1000.The LLM produces three tool calls in one step:
{
"thought": "I can fetch both cities' weather and compute the multiplication in parallel.",
"actions": [
{"action": "get_weather", "action_input": {"city": "Paris"}},
{"action": "get_weather", "action_input": {"city": "London"}},
{"action": "calculator", "action_input": {"a": 250, "b": 4}}
],
"final_answer": null
}Enable verbose=True to print every step to stdout.
agent = ReactAgent(
llm=OpenAIChatModel(model="gpt-4o"),
verbose=True,
)
agent.add_tools(weather_tool)
result = agent.invoke("What is the weather in Sydney?")Output:
[ReactAgent] Step 1 | Thought: I need to get the weather in Sydney using the get_weather tool.
[ReactAgent] Step 1 | Action: get_weather({'city': 'Sydney'})
[ReactAgent] Step 1 | Observation [get_weather]: The weather in Sydney is 25°C and sunny.
[ReactAgent] Step 2 | Thought: I have the weather information for Sydney.
[ReactAgent] Final Answer: The weather in Sydney is 25°C and sunny.
Enable full_output=True to also print the raw LLM JSON at each step — useful for debugging prompt or parse issues:
agent = ReactAgent(llm=llm, verbose=True, full_output=True)Attach a memory backend to persist conversation history across calls.
from autourgos_react_agent import ReactAgent, MemoryProtocol
from typing import Dict, List
class SimpleMemory(MemoryProtocol):
def __init__(self):
self._history: List[Dict[str, str]] = []
def add_user_message(self, message: str) -> None:
self._history.append({"role": "user", "content": message})
def add_assistant_message(self, message: str) -> None:
self._history.append({"role": "assistant", "content": message})
def get_history(self) -> List[Dict[str, str]]:
return list(self._history)
memory = SimpleMemory()
agent = ReactAgent(llm=llm, memory=memory)
agent.add_tools(search_tool)
result1 = agent.invoke("Search for the capital of France.")
print(result1)
# The capital of France is Paris.
result2 = agent.invoke("What city did I just ask about?")
print(result2)
# You asked about Paris, the capital of France.Require human (or programmatic) approval before any tool is executed.
def require_approval(tool_name: str, tool_input: dict) -> bool:
print(f"\n[Approval required] Tool: {tool_name}")
print(f"Input: {tool_input}")
answer = input("Allow? (y/n): ").strip().lower()
return answer == "y"
agent = ReactAgent(
llm=OpenAIChatModel(model="gpt-4o"),
approval_callback=require_approval,
)
agent.add_tools(delete_file_tool)
result = agent.invoke("Delete the temp folder.")If the callback returns a falsy value, the tool is skipped and the agent sees:
Observation: Tool call was denied by the approval callback.
Use this to implement human-in-the-loop, audit logging, or safety checks for destructive tools.
Register event hooks to observe the agent without modifying it.
from autourgos_react_agent import ReactAgent, CallbackHandler
class MyLogger(CallbackHandler):
def on_agent_start(self, query: str, **kwargs) -> None:
print(f"Agent started with query: {query}")
def on_agent_end(self, result: str, **kwargs) -> None:
print(f"Agent finished: {result}")
def on_tool_start(self, tool_name: str, tool_input: dict, **kwargs) -> None:
print(f"Calling tool: {tool_name} with {tool_input}")
def on_tool_end(self, tool_name: str, result: str, **kwargs) -> None:
print(f"Tool {tool_name} returned: {result[:100]}")
def on_iteration(self, iteration: int, thought: str, **kwargs) -> None:
print(f"Iteration {iteration} — thought: {thought}")
def on_parse_error(self, iteration: int, raw_response: str, **kwargs) -> None:
print(f"Parse error at step {iteration}: {raw_response[:100]}")
agent = ReactAgent(llm=llm, middleware=[MyLogger()])
agent.add_tools(weather_tool)
result = agent.invoke("Weather in Berlin?")You can also add middleware after construction:
agent.add_middleware(MyLogger())The agent implements both sync and async context managers. They automatically close the LLM's HTTP client when the block exits.
with ReactAgent(llm=OpenAIChatModel(model="gpt-4o")) as agent:
agent.add_tools(calculator_tool)
result = agent.invoke("What is 7 * 8?")
print(result)
# 56
# LLM client closed hereAsync:
import asyncio
from autourgos_openaichat import OpenAIChatModel
async def main():
async with ReactAgent(llm=OpenAIChatModel(model="gpt-4o")) as agent:
agent.add_tools(calculator_tool)
result = await agent.ainvoke("What is 12 ** 2?")
print(result)
# 144
asyncio.run(main())Prevent runaway agents with hard limits.
agent = ReactAgent(
llm=OpenAIChatModel(model="gpt-4o"),
max_iterations=10, # stop after 10 Thought → Action → Observe cycles
max_execution_time=30.0, # stop after 30 seconds wall-clock time
)
agent.add_tools(search_tool)
result = agent.invoke("Research the entire history of the internet.")
# If the agent hasn't finished: "[Timeout] Agent stopped after 30.0s."
# or: "[Max Iterations] Agent stopped after 10 iterations..."You can also override max_iterations per call:
result = agent.invoke("Quick question: capital of Japan?", max_iterations=3)Add extra instructions that persist across all steps.
agent = ReactAgent(
llm=OpenAIChatModel(model="gpt-4o"),
system_prompt=(
"You are a helpful financial analyst. "
"Always cite your sources. "
"Never speculate without data."
),
)
agent.add_tools(search_tool, calculator_tool)
result = agent.invoke("What is the P/E ratio of Apple?")| Parameter | Type | Default | Description |
|---|---|---|---|
llm |
any | None |
LLM wrapper with .invoke() / .ainvoke(). Works with OpenAIChatModel, OpenAIResponse, or any compatible object |
verbose |
bool |
False |
Print step-by-step execution to stdout |
full_output |
bool |
False |
Also print raw LLM responses (implies verbose) |
memory |
MemoryProtocol |
None |
Memory backend for conversation history |
max_iterations |
int |
15 |
Max Thought → Action → Observe cycles before stopping |
max_execution_time |
float |
None |
Wall-clock time limit in seconds |
approval_callback |
callable |
None |
Called as fn(tool_name, tool_input) before each tool. Return truthy to allow |
middleware |
list[CallbackHandler] |
None |
Event hooks for lifecycle events |
max_consecutive_parse_errors |
int |
3 |
Stop after this many back-to-back JSON parse failures |
tools |
list[dict] |
None |
Initial tool list (more can be added with add_tools()) |
system_prompt |
str |
"" |
Extra system-level instruction added to every prompt |
| Key | Type | Required | Description |
|---|---|---|---|
name |
str |
yes | Identifier used by the LLM. Use snake_case |
description |
str |
yes | Plain-English description of what the tool does and when to use it |
parameters |
dict |
recommended | JSON-Schema object describing the function's inputs |
func |
callable |
yes | The Python function to call. Can be sync or async |
parameters format (JSON Schema):
"parameters": {
"type": "object",
"properties": {
"param_name": {
"type": "string", # string | number | integer | boolean | array | object
"description": "...", # shown to the LLM — make it clear
"enum": ["a", "b"], # optional: restrict to specific values
},
},
"required": ["param_name"], # list required params
}invoke() and ainvoke() always return a str.
- Normal completion — the final answer extracted from the LLM's
final_answerfield - Error / limit reached — a string starting with one of the tags below
| Tag | Meaning |
|---|---|
[Timeout] |
max_execution_time was exceeded |
[Max Iterations] |
max_iterations reached without a final answer |
[Parse Error] |
LLM failed to produce valid JSON max_consecutive_parse_errors times in a row |
[LLM Error] |
LLM raised an exception (network, rate limit, etc.) |
The old Create_ReAct_Agent factory function still works but emits a DeprecationWarning:
from autourgos_react_agent import Create_ReAct_Agent # DeprecationWarning
agent = Create_ReAct_Agent(llm=llm) # same as ReactAgent(llm=llm)Update your code to use ReactAgent directly.
MIT — Copyright (c) 2026 Jitin Kumar Sengar