Packages
nous
0.10.0
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/agents/react_agent.ex
defmodule Nous.Agents.ReActAgent do
@moduledoc """
ReAct (Reasoning and Acting) Agent behaviour implementation.
ReAct is a prompting paradigm where AI agents interleave:
- **Reasoning**: Thinking about what to do next
- **Acting**: Using tools to gather information or perform actions
- **Observing**: Processing results to inform the next step
This module implements `Nous.Agent.Behaviour` with enhanced capabilities:
- Structured planning with facts survey
- Built-in todo list management
- Note-taking for observations
- Loop prevention (warns on duplicate tool calls)
- Mandatory `final_answer` for task completion
## Built-in Tools
- `plan` - Create structured plan before taking action
- `note` - Record observations and insights
- `add_todo` - Track subtasks
- `complete_todo` - Mark tasks done
- `list_todos` - View current todos
- `final_answer` - Complete the task (required)
## Example
agent = Agent.new("openai:gpt-4",
behaviour_module: Nous.Agents.ReActAgent,
tools: [&search/2, &calculate/2]
)
{:ok, result} = Agent.run(agent,
"Find the oldest F1 driver and when they won their first championship"
)
## Based on Research
This implementation draws from:
- "ReAct: Synergizing Reasoning and Acting in Language Models" (Yao et al., 2023)
- HuggingFace smolagents toolcalling_agent patterns
"""
@behaviour Nous.Agent.Behaviour
alias Nous.{Message, Messages, Tool}
alias Nous.Agent.Context
alias Nous.Tools.ReActTools
# System prompt that teaches the ReAct pattern
@react_system_prompt """
You are a ReAct (Reasoning + Acting) agent that solves problems through structured thinking and tool use.
MANDATORY WORKFLOW:
1. PLAN FIRST: Call 'plan' to create a structured approach
- Identify known facts, facts to look up, facts to derive
- Create step-by-step action plan
2. TRACK TASKS: Use 'add_todo' for each major step
- Break complex problems into subtasks
- Track progress as you work
3. ACT & OBSERVE: Use tools to gather information
- Each tool call creates an observation
- Call 'note' to record important findings
- Mark 'complete_todo' as you finish each step
4. COMPLETE: Call 'final_answer' when done
- REQUIRED to finish the task
- Provide complete, well-reasoned answer
IMPORTANT RULES:
- Never repeat the exact same tool call with identical parameters
- Always explain your reasoning before acting
- Use 'list_todos' to check progress
- Complete all pending todos before calling final_answer
- Call 'note' to document important observations
EXAMPLE FLOW:
1. User asks question
2. You call 'plan' with the question
3. You call 'add_todo' for each step
4. You use tools (search, calculate, etc.) to gather info
5. You call 'complete_todo' as you finish each step
6. You call 'final_answer' with the complete solution
Remember: Think step-by-step, document your process, and always provide a final_answer!
"""
@doc """
Initialize context for ReAct execution.
Sets up ReAct-specific state:
- `todos` - Task list
- `plans` - Planning history
- `notes` - Observations
- `tool_history` - Loop detection
- `final_answer` - Captured final answer
"""
@impl true
def init_context(_agent, ctx) do
# Initialize ReAct-specific deps
ctx
|> Context.merge_deps(%{
todos: Map.get(ctx.deps, :todos, []),
plans: Map.get(ctx.deps, :plans, []),
notes: Map.get(ctx.deps, :notes, []),
tool_history: Map.get(ctx.deps, :tool_history, []),
final_answer: nil
})
end
@doc """
Build messages with ReAct system prompt.
Combines the ReAct system prompt with any user instructions.
"""
@impl true
def build_messages(agent, ctx) do
# Build system prompt
system_prompt = build_react_system_prompt(agent, ctx)
# Filter out existing system messages
non_system_messages = Enum.reject(ctx.messages, &Message.is_system?/1)
# Return messages with ReAct system prompt first
if system_prompt do
[Message.system(system_prompt) | non_system_messages]
else
non_system_messages
end
end
@doc """
Process response and check for final_answer.
Updates context and sets `needs_response` to false when
final_answer tool is called.
"""
@impl true
def process_response(_agent, response, ctx) do
# Add message to context
ctx = Context.add_message(ctx, response)
# Check if final_answer was called
tool_calls = response.tool_calls || []
has_final_answer =
Enum.any?(tool_calls, fn call ->
call["name"] == "final_answer" or call[:name] == "final_answer"
end)
if has_final_answer do
# Extract final answer from tool call
final_call =
Enum.find(tool_calls, fn call ->
call["name"] == "final_answer" or call[:name] == "final_answer"
end)
answer =
get_in(final_call, [:arguments, "answer"]) ||
get_in(final_call, [:arguments, :answer]) ||
"No answer provided"
ctx
|> Context.merge_deps(%{final_answer: answer})
|> Context.set_needs_response(false)
else
# Continue processing - needs_response is already set by add_message
ctx
end
end
@doc """
Extract output from final_answer or last assistant message.
"""
@impl true
def extract_output(_agent, ctx) do
# First check for captured final_answer
case ctx.deps[:final_answer] do
nil ->
# Fall back to last assistant message
case find_last_assistant_text(ctx.messages) do
nil -> {:error, :no_output}
text -> {:ok, text}
end
answer ->
{:ok, answer}
end
end
@doc """
Get all tools including ReAct-specific tools.
"""
@impl true
def get_tools(agent) do
react_tools() ++ agent.tools
end
@doc """
Track tool calls for loop detection.
"""
@impl true
def after_tool(_agent, call, _result, ctx) do
# Record tool call in history
history_entry = %{
name: call["name"] || call[:name],
arguments: call["arguments"] || call[:arguments],
timestamp: DateTime.utc_now()
}
tool_history = [history_entry | ctx.deps[:tool_history] || []]
# Check for duplicate calls (loop detection)
if is_duplicate_call?(call, ctx.deps[:tool_history] || []) do
require Logger
Logger.warning("ReAct loop detection: duplicate tool call #{call["name"] || call[:name]}")
end
Context.merge_deps(ctx, %{tool_history: tool_history})
end
# Private helpers
defp build_react_system_prompt(agent, ctx) do
# Start with base ReAct prompt
base_prompt = @react_system_prompt
# Add user instructions if present
user_instructions = agent.instructions || ctx.system_prompt
if user_instructions do
"""
#{base_prompt}
ADDITIONAL CONTEXT:
#{user_instructions}
"""
else
base_prompt
end
end
defp react_tools do
[
Tool.from_function(&ReActTools.plan/2,
name: "plan",
description:
"Create a structured plan for solving the task. Analyzes known facts, facts to look up, and facts to derive. Use this FIRST before taking any actions."
),
Tool.from_function(&ReActTools.note/2,
name: "note",
description:
"Record an observation, insight, or intermediate finding. Use this to document important information discovered during your work."
),
Tool.from_function(&ReActTools.add_todo/2,
name: "add_todo",
description:
"Add a task to your todo list. Use this to break down complex problems into manageable subtasks. Parameters: item (required), priority (optional: high/medium/low)."
),
Tool.from_function(&ReActTools.complete_todo/2,
name: "complete_todo",
description:
"Mark a todo item as complete. Parameters: id (todo number) OR item (description matching the todo)."
),
Tool.from_function(&ReActTools.list_todos/2,
name: "list_todos",
description:
"View all current todos with their status. Shows pending and completed tasks to help track progress."
),
Tool.from_function(&ReActTools.final_answer/2,
name: "final_answer",
description:
"Provide the final answer to complete the task. REQUIRED to finish. Only call this after you have gathered all necessary information and solved the problem. Parameter: answer (your complete solution)."
)
]
end
defp find_last_assistant_text(messages) do
messages
|> Enum.reverse()
|> Enum.find_value(fn
%Message{role: :assistant} = msg -> Messages.extract_text(msg)
_ -> nil
end)
end
defp is_duplicate_call?(call, history) do
call_name = call["name"] || call[:name]
call_args = call["arguments"] || call[:arguments]
Enum.any?(history, fn entry ->
entry.name == call_name and entry.arguments == call_args
end)
end
end