Packages
nous
0.11.3
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/research/planner.ex
defmodule Nous.Research.Planner do
@moduledoc """
Decomposes a complex research query into a structured plan with sub-questions.
Supports three strategies:
- `:parallel` - Independent sub-queries executed simultaneously
- `:sequential` - Each step depends on prior results
- `:tree` - Branching exploration with pruning
"""
require Logger
@type step :: %{
query: String.t(),
strategy: :parallel | :sequential,
depends_on: [non_neg_integer()]
}
@type plan :: %{
query: String.t(),
steps: [step()],
strategy: :parallel | :sequential | :tree,
estimated_searches: non_neg_integer()
}
@doc """
Generate a research plan from a user query.
Uses an LLM to decompose the query into searchable sub-questions.
"""
@spec plan(String.t(), keyword()) :: {:ok, plan()} | {:error, term()}
def plan(query, opts \\ []) do
model = Keyword.get(opts, :model, "openai:gpt-4o-mini")
strategy = Keyword.get(opts, :strategy, :parallel)
prompt = """
You are a research planner. Break down this research query into 3-7 specific, \
searchable sub-questions. Each sub-question should be independently answerable \
via web search.
Research query: #{query}
Strategy: #{strategy}
Respond with a numbered list of sub-questions, one per line:
1. [sub-question]
2. [sub-question]
...
Keep questions specific and searchable. Avoid overly broad questions.
"""
agent =
Nous.Agent.new(model,
instructions: "You are a research planning specialist.",
model_settings: %{temperature: 0.3, max_tokens: 1000}
)
case Nous.Agent.run(agent, prompt) do
{:ok, result} ->
steps = parse_plan_steps(result.output, strategy)
{:ok,
%{
query: query,
steps: steps,
strategy: strategy,
estimated_searches: length(steps)
}}
{:error, reason} ->
# Fallback: use the original query as a single step
Logger.warning("Planning failed, using single-step fallback: #{inspect(reason)}")
{:ok,
%{
query: query,
steps: [%{query: query, strategy: :parallel, depends_on: []}],
strategy: :parallel,
estimated_searches: 1
}}
end
end
defp parse_plan_steps(output, strategy) do
output
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.filter(&Regex.match?(~r/^\d+[\.\)]\s+/, &1))
|> Enum.map(fn line ->
# Strip the numbering
query = Regex.replace(~r/^\d+[\.\)]\s+/, line, "") |> String.trim()
%{query: query, strategy: strategy, depends_on: []}
end)
|> case do
[] ->
# Parsing failed, use original query
[%{query: output, strategy: strategy, depends_on: []}]
steps ->
if strategy == :sequential do
# Add sequential dependencies
steps
|> Enum.with_index()
|> Enum.map(fn {step, i} ->
deps = if i > 0, do: [i - 1], else: []
%{step | depends_on: deps}
end)
else
steps
end
end
end
end