Skip to content

ReAct Module

The ReAct (Reasoning and Acting) module implements an agent that iteratively reasons about tasks and uses tools to accomplish goals.

Overview

ReAct combines:

  • Reasoning: Step-by-step thinking about what to do next
  • Acting: Calling tools to perform actions
  • Iteration: Repeating until the task is complete

This creates an agent that can break down complex tasks, use available tools, and ask for help when needed.

Basic Usage

from udspy import ReAct, Signature, InputField, OutputField, tool
from pydantic import Field

@tool(name="search", description="Search for information")
def search(query: str = Field(...)) -> str:
    return f"Results for: {query}"

class QA(Signature):
    """Answer questions using available tools."""
    question: str = InputField()
    answer: str = OutputField()

agent = ReAct(QA, tools=[search])
result = agent(question="What is the weather in Tokyo?")

print(result.answer)
print(result.trajectory)  # Full reasoning history

String Signatures

For quick prototyping:

agent = ReAct("question -> answer", tools=[search])
result = agent(question="What is Python?")

How ReAct Works

Iteration Loop

  1. Reason: Agent thinks about current situation
  2. Act: Agent calls a tool (or finish)
  3. Observe: Agent sees tool result
  4. Repeat: Until agent calls finish tool or max iterations

Built-in Tools

ReAct automatically provides:

  • finish: Call when task is complete
  • user_clarification: Ask user for clarification (if enabled)
# The agent automatically has these tools available:
# - finish(answer: str) - Complete the task
# - user_clarification(question: str) - Ask user for help

Trajectory

The trajectory records every step:

result = agent(question="Calculate 15 * 23")

# Access trajectory
print(result.trajectory)
# {
#   "reasoning_0": "I need to calculate 15 * 23",
#   "tool_name_0": "calculator",
#   "tool_args_0": {"expression": "15 * 23"},
#   "observation_0": "345",
#   "reasoning_1": "I have the answer",
#   "tool_name_1": "finish",
#   ...
# }

Configuration

Maximum Iterations

agent = ReAct(QA, tools=[search], max_iters=10)
result = agent(question="...", max_iters=5)  # Override per call

Disable Ask-to-User

agent = ReAct(QA, tools=[search])

Human-in-the-Loop

ReAct supports tools with require_confirmation that require human confirmation:

from udspy import ConfirmationRequired, tool

@tool(name="delete_file", require_confirmation=True)
def delete_file(path: str = Field(...)) -> str:
    return f"Deleted {path}"

agent = ReAct(QA, tools=[delete_file])

# Note: aresume() is not yet implemented in ReAct
# Use respond_to_confirmation() instead
from udspy import respond_to_confirmation

try:
    result = await agent.aforward(question="Delete /tmp/test.txt")
except ConfirmationRequired as e:
    print(f"Confirm: {e.question}")
    print(f"Tool: {e.tool_call.name}")
    print(f"Args: {e.tool_call.args}")

    # User approves
    respond_to_confirmation(e.confirmation_id, approved=True)
    result = await agent.aforward(question="Delete /tmp/test.txt")

    # Or user rejects
    respond_to_confirmation(e.confirmation_id, approved=False, status="rejected")

Resumption Flow

When a confirmation is requested:

  1. Agent pauses and raises ConfirmationRequired
  2. Exception contains saved state and pending tool call
  3. User reviews and responds
  4. Call aresume(response, saved_state) to continue

See Confirmation API for details.

Streaming

Stream the agent's reasoning in real-time:

async for event in agent.aexecute(
    stream=True,
    question="What is quantum computing?"
):
    if isinstance(event, OutputStreamChunk):
        if event.field == "reasoning":
            print(f"Thinking: {event.delta}", end="", flush=True)
    elif isinstance(event, Prediction):
        print(f"\n\nAnswer: {event.answer}")

See examples/react_streaming.py for a complete example.

Architecture

Internal Signatures

ReAct uses two internal signatures:

  1. react_signature: For reasoning and tool selection
  2. Inputs: Original inputs + trajectory
  3. Outputs: reasoning
  4. Tools: All provided tools + finish

  5. extract_signature: For extracting final answer

  6. Inputs: Original inputs + trajectory
  7. Outputs: Original outputs
  8. Uses ChainOfThought for extraction

Modules

ReAct composes two modules:

  • react_module: Predict with tools for reasoning/acting
  • extract_module: ChainOfThought for final answer extraction

Example Flow

User: "What is the capital of France?"

Iteration 0:
  Reasoning: "I need to search for France's capital"
  Tool: search
  Args: {"query": "capital of France"}
  Observation: "Paris is the capital of France"

Iteration 1:
  Reasoning: "I have the answer, I can finish"
  Tool: finish
  Args: {}
  Observation: "Task completed"

Extract:
  Reasoning: "Based on the search, Paris is the capital"
  Answer: "Paris"

Advanced Usage

Custom Tools

from pydantic import Field

@tool(
    name="calculator",
    description="Evaluate mathematical expressions"
)
def calc(expression: str = Field(description="Math expression")) -> str:
    return str(eval(expression))

@tool(
    name="web_search",
    description="Search the web for information"
)
async def web_search(query: str = Field(...)) -> str:
    # Async tools are supported
    return await search_api(query)

agent = ReAct(QA, tools=[calc, web_search])

Multiple Outputs

class Research(Signature):
    """Research a topic thoroughly."""
    topic: str = InputField()
    summary: str = OutputField()
    sources: str = OutputField()
    confidence: str = OutputField()

agent = ReAct(Research, tools=[search])
result = agent(topic="Quantum Computing")

print(result.summary)
print(result.sources)
print(result.confidence)

Tool Error Handling

Tools can raise exceptions - they're caught and added to observations:

@tool(name="divide")
def divide(a: int = Field(...), b: int = Field(...)) -> str:
    return str(a / b)

agent = ReAct(QA, tools=[divide])
result = agent(question="What is 10 divided by 0?")

# Agent sees: "Error executing divide: division by zero"
# Agent can reason about the error and try alternative approaches

Design Rationale

Why Two Phases (React + Extract)?

  1. react_module: Focuses on tool usage and reasoning
  2. extract_module: Focuses on clean output formatting

This separation ensures: - Tool-using prompts stay focused on actions - Final outputs are well-formatted - Trajectory doesn't pollute final answer

Why user_clarification Tool?

The built-in user clarification tool allows agents to: - Request clarification when ambiguous - Ask for additional information - Interact naturally with users

It's implemented as a tool with require_confirmation, so users can provide responses that the agent incorporates into its reasoning.

Why finish Tool?

The finish tool signals task completion: - Explicit end condition (vs implicit max iterations) - Agent decides when it has enough information - More natural than counting iterations

Common Patterns

Research Agent

@tool(name="search")
def search(query: str = Field(...)) -> str:
    return search_web(query)

@tool(name="summarize")
def summarize(text: str = Field(...)) -> str:
    return llm_summarize(text)

researcher = ReAct(
    "topic -> summary, sources",
    tools=[search, summarize]
)
result = researcher(topic="AI Safety")

Task Automation

@tool(name="read_file")
def read_file(path: str = Field(...)) -> str:
    return open(path).read()

@tool(name="write_file", require_confirmation=True)
def write_file(path: str = Field(...), content: str = Field(...)) -> str:
    with open(path, 'w') as f:
        f.write(content)
    return f"Wrote to {path}"

assistant = ReAct(
    "task -> result",
    tools=[read_file, write_file]
)

Multi-tool Problem Solving

@tool(name="calculator")
def calc(expr: str = Field(...)) -> str:
    return str(eval(expr))

@tool(name="unit_converter")
def convert(value: float = Field(...), from_unit: str = Field(...), to_unit: str = Field(...)) -> str:
    # Conversion logic
    return f"{result} {to_unit}"

solver = ReAct(
    "problem -> solution",
    tools=[calc, convert]
)
result = solver(problem="Convert 100 fahrenheit to celsius and add 10")

Limitations

  1. Token Usage: Each iteration adds to token count
  2. Latency: Multiple LLM calls increase response time
  3. Reliability: Agent may not always pick the right tool
  4. Max Iterations: Tasks may not complete within iteration limit

See Also