Packages
nous
0.13.2
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/react_agent.ex
defmodule Nous.ReActAgent do
@moduledoc """
ReAct (Reasoning and Acting) Agent wrapper with built-in planning and todo management.
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 wraps `Nous.Agent` 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
# Create ReAct agent with custom tools
agent = ReActAgent.new("lmstudio:qwen3-vl-4b-thinking-mlx",
instructions: "You are a research assistant",
tools: [&MyTools.search/2, &MyTools.calculate/2]
)
# Run with initial context
{:ok, result} = ReActAgent.run(agent,
"Find the oldest F1 driver and when they won their first championship",
deps: %{database: MyDB}
)
# The agent will automatically:
# 1. Create a plan
# 2. Add todos for each step
# 3. Use tools to gather info
# 4. Complete todos as it progresses
# 5. Call final_answer when done
IO.puts(result.output)
# Access metadata
IO.inspect(result.metadata)
## Comparison to Standard Agent
# Standard Agent
agent = Agent.new("model", tools: [tool1, tool2])
# ReAct Agent (enhanced)
agent = ReActAgent.new("model", tools: [tool1, tool2])
# Automatically includes: plan, note, add_todo, complete_todo,
# list_todos, final_answer
## Loop Prevention
The ReAct agent tracks tool call history and warns if the same tool
is called with identical arguments, helping prevent infinite loops.
## Based on Research
This implementation draws from:
- "ReAct: Synergizing Reasoning and Acting in Language Models" (Yao et al., 2023)
- HuggingFace smolagents toolcalling_agent patterns
"""
alias Nous.Agent
@type t :: Agent.t()
@doc """
Create a new ReAct agent.
Wraps `Nous.Agent.new/2` with ReAct behaviour module.
## Parameters
- `model_string` - Model in format "provider:model-name"
- `opts` - Configuration options (same as `Nous.Agent.new/2`)
## Options
All standard Agent options are supported, plus:
- `:react_system_prompt` - Override the default ReAct system prompt
- `:require_planning` - Whether to enforce planning step (default: false)
- `:track_history` - Track tool call history for loop detection (default: true)
## Examples
# Basic ReAct agent
agent = ReActAgent.new("openai:gpt-4")
# With custom tools
agent = ReActAgent.new("anthropic:claude-3-5-sonnet",
tools: [&MyTools.search/2, &MyTools.calculate/2],
instructions: "You are a research assistant"
)
# With custom model settings
agent = ReActAgent.new("lmstudio:qwen3-vl-4b-thinking-mlx",
model_settings: %{temperature: 0.3, max_tokens: 2000}
)
## Returns
A configured Agent struct with ReAct behaviour.
"""
@spec new(String.t(), keyword()) :: t()
def new(model_string, opts \\ []) do
# Use the new behaviour-based implementation
# Remove ReAct-specific options that are handled by the behaviour
agent_opts =
opts
|> Keyword.put(:behaviour_module, Nous.Agents.ReActAgent)
|> Keyword.delete(:react_system_prompt)
|> Keyword.delete(:require_planning)
|> Keyword.delete(:track_history)
# Create the underlying agent with ReAct behaviour
Agent.new(model_string, agent_opts)
end
@doc """
Run the ReAct agent synchronously.
Wraps `Nous.Agent.run/3` with additional ReAct context initialization.
## Options
Same as `Nous.Agent.run/3`, with automatic initialization of:
- `todos: []` - Empty todo list
- `plans: []` - Empty plans list
- `notes: []` - Empty notes list
- `tool_history: []` - Tool call history for loop detection
## Examples
{:ok, result} = ReActAgent.run(agent,
"What is the capital of France and what's its population?"
)
# With dependencies
{:ok, result} = ReActAgent.run(agent,
"Search for recent AI developments",
deps: %{api_key: "..."}
)
# Continue conversation
{:ok, result2} = ReActAgent.run(agent,
"Tell me more about that",
message_history: result1.new_messages
)
## Returns
Same as `Nous.Agent.run/3`:
{:ok, %{
output: "Final answer text...",
usage: %Usage{...},
all_messages: [...],
new_messages: [...],
metadata: %{
todos_completed: 3,
todos_pending: 0,
plans_count: 1,
notes_count: 5
}
}}
"""
@spec run(t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def run(%Agent{} = agent, prompt, opts \\ []) do
# Initialize ReAct-specific context
existing_deps = Keyword.get(opts, :deps, %{})
react_deps =
Map.merge(existing_deps, %{
todos: [],
plans: [],
notes: [],
tool_history: []
})
# Update opts with ReAct context
react_opts = Keyword.put(opts, :deps, react_deps)
# Run the agent
case Agent.run(agent, prompt, react_opts) do
{:ok, result} ->
# Extract metadata from the final result if present
metadata = extract_react_metadata(result)
enhanced_result = Map.put(result, :metadata, metadata)
{:ok, enhanced_result}
error ->
error
end
end
@doc """
Run the ReAct agent with streaming.
Wraps `Nous.Agent.run_stream/3` with ReAct context initialization.
## Example
{:ok, stream} = ReActAgent.run_stream(agent, "Solve this problem...")
stream
|> Stream.each(fn
{:text_delta, text} -> IO.write(text)
{:tool_call, call} -> IO.puts("Using tool")
{:tool_result, result} -> IO.puts("Got result")
{:complete, result} -> IO.puts("Done!")
end)
|> Stream.run()
"""
@spec run_stream(t(), String.t(), keyword()) :: {:ok, Enumerable.t()} | {:error, term()}
def run_stream(%Agent{} = agent, prompt, opts \\ []) do
# Initialize ReAct-specific context
existing_deps = Keyword.get(opts, :deps, %{})
react_deps =
Map.merge(existing_deps, %{
todos: [],
plans: [],
notes: [],
tool_history: []
})
react_opts = Keyword.put(opts, :deps, react_deps)
Agent.run_stream(agent, prompt, react_opts)
end
# Private functions
defp extract_react_metadata(_result) do
# Try to extract metadata from final_answer tool result or messages
# This is a best-effort extraction
%{
todos_completed: 0,
todos_pending: 0,
plans_count: 0,
notes_count: 0
}
end
end