Packages
nous
0.9.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/tools/react_tools.ex
defmodule Nous.Tools.ReActTools do
@moduledoc """
Built-in tools for ReAct (Reasoning and Acting) agents.
These tools provide structured planning, note-taking, todo management,
and explicit task completion for ReAct agents.
## Tools Included
- `plan/2` - Create structured plan with facts survey
- `note/2` - Record observations and insights
- `add_todo/2` - Add item to todo list
- `complete_todo/2` - Mark todo as complete
- `list_todos/2` - View current todos
- `final_answer/2` - Provide final answer (signals completion)
## Usage
agent = ReActAgent.new("lmstudio:qwen3-vl-4b-thinking-mlx",
instructions: "Solve complex problems step by step"
)
The agent will automatically have access to all ReAct tools.
"""
require Logger
@doc """
Create a structured plan for solving a task.
Helps the agent organize its approach by identifying:
- Known facts
- Facts to look up
- Facts to derive/calculate
- Step-by-step action plan
## Arguments
- `task` - The task or problem to plan for (string)
- Can also be called with no arguments to plan the current conversation
## Returns
A confirmation message. The plan is stored in context for reference.
"""
def plan(ctx, args) do
# Support multiple parameter formats
task = Map.get(args, "task") || Map.get(args, "query") || Map.get(args, "problem") || "Current task"
timestamp = DateTime.utc_now() |> DateTime.to_string()
plan_record = %{
task: task,
timestamp: timestamp,
created_at: timestamp
}
# Store plan in context
plans = [plan_record | (ctx.deps[:plans] || [])]
Logger.info("ReAct: Plan created for task: #{String.slice(task, 0, 60)}...")
%{
success: true,
message: """
Plan created. Now analyze:
📋 FACTS SURVEY:
- What facts do you already know?
- What facts do you need to look up?
- What facts do you need to derive/calculate?
📝 ACTION PLAN:
- List the steps you'll take to solve this
- Be specific about which tools you'll use
Remember: Create todos for each major step!
""",
__update_context__: %{plans: plans}
}
end
@doc """
Record an observation or insight during the reasoning process.
Use this to document important findings, intermediate results,
or thoughts that will inform future actions.
## Arguments
- `content` - The note content (string)
## Returns
Confirmation message.
"""
def note(ctx, %{"content" => content}) do
timestamp = DateTime.utc_now() |> DateTime.to_string()
note_record = %{
content: content,
timestamp: timestamp
}
notes = [note_record | (ctx.deps[:notes] || [])]
Logger.debug("ReAct: Note recorded: #{String.slice(content, 0, 60)}...")
%{
success: true,
message: "Note recorded: #{content}",
__update_context__: %{notes: notes}
}
end
@doc """
Add a task to the todo list.
Use this to break down complex problems into manageable subtasks.
Track what needs to be done and maintain focus.
## Arguments
- `item` - Description of the todo item (string)
- `priority` - Optional priority: "high", "medium", "low" (default: "medium")
## Returns
Confirmation with current todo count.
"""
def add_todo(ctx, args) do
item = Map.get(args, "item") || Map.get(args, "task", "")
priority = Map.get(args, "priority", "medium")
timestamp = DateTime.utc_now() |> DateTime.to_string()
todos = ctx.deps[:todos] || []
next_id = length(todos) + 1
new_todo = %{
id: next_id,
item: item,
priority: priority,
status: :pending,
created_at: timestamp
}
updated_todos = todos ++ [new_todo]
Logger.info("ReAct: Todo added (##{next_id}): #{item}")
%{
success: true,
message: "✓ Added todo ##{next_id}: #{item} (#{length(updated_todos)} total todos)",
todo_id: next_id,
__update_context__: %{todos: updated_todos}
}
end
@doc """
Mark a todo item as complete.
## Arguments
- `id` - The todo ID to complete (integer)
OR
- `item` - Description matching the todo item (string)
## Returns
Confirmation message.
"""
def complete_todo(ctx, args) do
todos = ctx.deps[:todos] || []
# Support both id and item-based completion
{completed_id, updated_todos} = cond do
Map.has_key?(args, "id") ->
id = args["id"]
complete_by_id(todos, id)
Map.has_key?(args, "item") ->
item = args["item"]
complete_by_item(todos, item)
true ->
{nil, todos}
end
if completed_id do
completed_count = Enum.count(updated_todos, fn t -> t.status == :completed end)
pending_count = Enum.count(updated_todos, fn t -> t.status == :pending end)
Logger.info("ReAct: Todo ##{completed_id} completed (#{completed_count}/#{length(updated_todos)} done)")
%{
success: true,
message: "✓ Completed todo ##{completed_id} (#{pending_count} remaining)",
completed_id: completed_id,
stats: %{completed: completed_count, pending: pending_count, total: length(updated_todos)},
__update_context__: %{todos: updated_todos}
}
else
%{
success: false,
message: "Todo not found. Use list_todos to see available todos.",
__update_context__: %{}
}
end
end
@doc """
List all current todos with their status.
Shows pending and completed tasks to help track progress.
## Returns
Formatted list of todos.
"""
def list_todos(ctx, _args \\ %{}) do
todos = ctx.deps[:todos] || []
if Enum.empty?(todos) do
%{
success: true,
message: "No todos yet. Use add_todo to create tasks.",
todos: []
}
else
pending = Enum.filter(todos, fn t -> t.status == :pending end)
completed = Enum.filter(todos, fn t -> t.status == :completed end)
formatted_pending = format_todos(pending, "📝 PENDING")
formatted_completed = format_todos(completed, "✅ COMPLETED")
message = """
#{formatted_pending}
#{formatted_completed}
Progress: #{length(completed)}/#{length(todos)} completed
"""
%{
success: true,
message: message,
todos: todos,
stats: %{
pending: length(pending),
completed: length(completed),
total: length(todos)
}
}
end
end
@doc """
Provide the final answer to complete the task.
This tool MUST be called to signal task completion.
The agent should only use this after gathering all necessary
information and solving the problem.
## Arguments
- `answer` - The final answer or solution (string)
## Returns
The final answer wrapped with completion metadata.
"""
def final_answer(ctx, %{"answer" => answer}) do
todos = ctx.deps[:todos] || []
plans = ctx.deps[:plans] || []
notes = ctx.deps[:notes] || []
completed_todos = Enum.count(todos, fn t -> t.status == :completed end)
pending_todos = Enum.count(todos, fn t -> t.status == :pending end)
Logger.info("ReAct: Final answer provided (#{completed_todos}/#{length(todos)} todos completed)")
if pending_todos > 0 do
Logger.warning("ReAct: Task completed with #{pending_todos} pending todos")
end
%{
success: true,
final_answer: answer,
message: """
🎯 FINAL ANSWER:
#{answer}
📊 Task Summary:
- Todos completed: #{completed_todos}/#{length(todos)}
- Plans created: #{length(plans)}
- Notes recorded: #{length(notes)}
""",
metadata: %{
todos_completed: completed_todos,
todos_pending: pending_todos,
plans_count: length(plans),
notes_count: length(notes)
}
}
end
# Private helper functions
defp complete_by_id(todos, id) do
case Enum.find_index(todos, fn t -> t.id == id end) do
nil ->
{nil, todos}
index ->
updated_todos = List.update_at(todos, index, fn todo ->
todo
|> Map.put(:status, :completed)
|> Map.put(:completed_at, DateTime.utc_now() |> DateTime.to_string())
end)
{id, updated_todos}
end
end
defp complete_by_item(todos, item) do
# Find by matching item text (case-insensitive partial match)
item_lower = String.downcase(item)
case Enum.find_index(todos, fn t ->
String.contains?(String.downcase(t.item), item_lower)
end) do
nil ->
{nil, todos}
index ->
todo = Enum.at(todos, index)
updated_todos = List.update_at(todos, index, fn t ->
t
|> Map.put(:status, :completed)
|> Map.put(:completed_at, DateTime.utc_now() |> DateTime.to_string())
end)
{todo.id, updated_todos}
end
end
defp format_todos([], _header), do: ""
defp format_todos(todos, header) do
formatted_items =
todos
|> Enum.map(fn todo ->
priority_icon = case todo.priority do
"high" -> "🔴"
"low" -> "🟢"
_ -> "🟡"
end
status_icon = if todo.status == :completed, do: "✅", else: "📝"
" #{status_icon} #{priority_icon} ##{todo.id}: #{todo.item}"
end)
|> Enum.join("\n")
"""
#{header}:
#{formatted_items}
"""
end
end