Packages
nous
0.12.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
README.md

# Nous AI
> *"Nous (νοῦς) — the ancient Greek concept of mind, reason, and intellect; the faculty of understanding that grasps truth directly."*
AI agent framework for Elixir with multi-provider LLM support.
[](https://elixir-lang.org)
[](https://github.com/nyo16/nous/blob/master/LICENSE)
[](#features)
## Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:nous, "~> 0.12.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
### Simple Text Generation
For quick LLM calls without agents:
```elixir
# One-liner
{:ok, text} = Nous.generate_text("lmstudio:qwen3", "What is Elixir?")
IO.puts(text)
# With options
{:ok, text} = Nous.generate_text("openai:gpt-4", "Explain monads",
system: "You are a functional programming expert",
temperature: 0.7,
max_tokens: 500
)
# Streaming
{:ok, stream} = Nous.stream_text("lmstudio:qwen3", "Write a haiku")
stream |> Stream.each(&IO.write/1) |> Stream.run()
# With prompt templates
alias Nous.PromptTemplate
template = PromptTemplate.from_template("""
Summarize the following text in <%= @style %> style:
<text>
<%= @content %>
</text>
""")
prompt = PromptTemplate.format(template, %{
style: "bullet points",
content: "Elixir is a dynamic, functional language for building scalable applications..."
})
{:ok, summary} = Nous.generate_text("openai:gpt-4", prompt)
```
### With Agents
For multi-turn conversations, tools, and complex workflows:
```elixir
# Create an agent
agent = Nous.new("lmstudio:qwen3",
instructions: "Be helpful and concise."
)
# Run it
{:ok, result} = Nous.run(agent, "What is Elixir?")
IO.puts(result.output)
IO.puts("Tokens: #{result.usage.total_tokens}")
```
## Supported Providers
| Provider | Model String | Streaming |
|----------|-------------|-----------|
| LM Studio | `lmstudio:qwen3` | ✅ |
| OpenAI | `openai:gpt-4` | ✅ |
| Anthropic | `anthropic:claude-sonnet-4-5-20250929` | ✅ |
| Groq | `groq:llama-3.1-70b-versatile` | ✅ |
| Ollama | `ollama:llama2` | ✅ |
| OpenRouter | `openrouter:anthropic/claude-3.5-sonnet` | ✅ |
| Together AI | `together:meta-llama/Llama-3-70b-chat-hf` | ✅ |
| Custom | `openai_compatible:model` + `:base_url` | ✅ |
All providers use pure Elixir HTTP clients (Req + Finch).
```elixir
# Switch providers with one line change
agent = Nous.new("lmstudio:qwen3") # Local (free)
agent = Nous.new("openai:gpt-4") # OpenAI
agent = Nous.new("anthropic:claude-sonnet-4-5-20250929") # Anthropic
```
## Features
### Tool Calling
Define Elixir functions as tools. The AI calls them automatically when needed.
```elixir
get_weather = fn _ctx, %{"city" => city} ->
%{city: city, temperature: 72, conditions: "sunny"}
end
agent = Nous.new("openai:gpt-4",
instructions: "You can check the weather.",
tools: [get_weather]
)
{:ok, result} = Nous.run(agent, "What's the weather in Tokyo?")
```
### Tools with Context
Pass dependencies (user, database, API keys) via context:
```elixir
get_balance = fn ctx, _args ->
user = ctx.deps[:user]
%{balance: user.balance}
end
agent = Nous.new("openai:gpt-4", tools: [get_balance])
{:ok, result} = Nous.run(agent, "What's my balance?",
deps: %{user: %{id: 123, balance: 1000}}
)
```
### Context Continuation
Continue conversations with full context preservation:
```elixir
{:ok, result1} = Nous.run(agent, "My name is Alice")
{:ok, result2} = Nous.run(agent, "What's my name?", context: result1.context)
# => "Your name is Alice"
```
### Streaming
```elixir
{:ok, stream} = Nous.run_stream(agent, "Write a haiku")
stream
|> Enum.each(fn
{:text_delta, text} -> IO.write(text)
{:finish, _} -> IO.puts("")
_ -> :ok
end)
```
### Callbacks
Monitor execution with callbacks or process messages:
```elixir
# Map-based callbacks
{:ok, result} = Nous.run(agent, "Hello",
callbacks: %{
on_llm_new_delta: fn _event, delta -> IO.write(delta) end,
on_tool_call: fn _event, call -> IO.puts("Tool: #{call.name}") end
}
)
# Process messages (for LiveView)
{:ok, result} = Nous.run(agent, "Hello", notify_pid: self())
# Receives: {:agent_delta, text}, {:tool_call, call}, {:agent_complete, result}
```
### Module-Based Tools
Define tools as modules for better organization and testability:
```elixir
defmodule MyTools.Search do
@behaviour Nous.Tool.Behaviour
@impl true
def metadata do
%{
name: "search",
description: "Search the web",
parameters: %{
"type" => "object",
"properties" => %{
"query" => %{"type" => "string"}
},
"required" => ["query"]
}
}
end
@impl true
def execute(ctx, %{"query" => query}) do
http = ctx.deps[:http_client] || MyApp.HTTP
{:ok, http.search(query)}
end
end
agent = Nous.new("openai:gpt-4",
tools: [Nous.Tool.from_module(MyTools.Search)]
)
```
### Tool Context Updates
Tools can modify context state for subsequent calls:
```elixir
alias Nous.Tool.ContextUpdate
add_item = fn ctx, %{"item" => item} ->
items = ctx.deps[:cart] || []
{:ok, %{added: item}, ContextUpdate.set(ContextUpdate.new(), :cart, items ++ [item])}
end
```
### Prompt Templates
Build prompts with EEx variable substitution:
```elixir
alias Nous.PromptTemplate
template = PromptTemplate.from_template(
"You are a <%= @role %> who speaks <%= @language %>.",
role: :system
)
message = PromptTemplate.to_message(template, %{role: "teacher", language: "Spanish"})
{:ok, result} = Nous.run(agent, messages: [message, Message.user("Hello")])
```
### ReActAgent
For complex multi-step reasoning with planning:
```elixir
agent = Nous.ReActAgent.new("openai:gpt-4",
tools: [&search/2, &calculate/2]
)
{:ok, result} = Nous.run(agent,
"Research the population of Tokyo and calculate its density"
)
```
### Plugin System
Extend agents with composable plugins for cross-cutting concerns:
```elixir
agent = Nous.new("openai:gpt-4",
instructions: "You are an assistant.",
plugins: [Nous.Plugins.Summarization, Nous.Plugins.HumanInTheLoop],
tools: [&MyTools.send_email/2]
)
{:ok, result} = Nous.run(agent, "Send a welcome email to alice@example.com")
```
### Human-in-the-Loop
Add approval workflows for sensitive tool calls:
```elixir
agent = Nous.new("openai:gpt-4",
plugins: [Nous.Plugins.HumanInTheLoop],
tools: [&MyTools.delete_record/2]
)
{:ok, result} = Nous.run(agent, "Delete user 42",
approval_handler: fn tool_call ->
IO.puts("Approve #{tool_call.name}? [y/n]")
if IO.gets("") |> String.trim() == "y", do: :approve, else: :reject
end
)
```
#### Async Approval via PubSub
For LiveView or other async approval workflows:
```elixir
# Configure PubSub once in config/config.exs
config :nous, pubsub: MyApp.PubSub
# Use async approval handler
deps = %{hitl_config: %{
tools: ["send_email"],
handler: Nous.PubSub.Approval.handler(session_id: session_id, timeout: :timer.minutes(5))
}}
# In LiveView: handle {:approval_required, info} and call
# Nous.PubSub.Approval.respond(MyApp.PubSub, session_id, tool_call_id, :approve)
```
### Sub-Agent Delegation
Enable agents to delegate tasks to specialized child agents:
```elixir
agent = Nous.new("openai:gpt-4",
plugins: [Nous.Plugins.SubAgent],
deps: %{sub_agent_templates: %{
"researcher" => Agent.new("openai:gpt-4o-mini",
instructions: "Research topics thoroughly"
),
"coder" => Agent.new("openai:gpt-4",
instructions: "Write clean Elixir code"
)
}}
)
# delegate_task — single sub-agent for focused work
{:ok, result} = Nous.run(agent, "Research Elixir GenServers, then write an example")
# spawn_agents — multiple sub-agents in parallel
{:ok, result} = Nous.run(agent,
"Compare GenServer vs Agent vs ETS for caching. Research each in parallel."
)
```
The `SubAgent` plugin provides two tools:
- `delegate_task` — run a single sub-agent for sequential delegation
- `spawn_agents` — run multiple sub-agents concurrently via `Task.Supervisor`
Each sub-agent runs in its own isolated context. Configure concurrency
limits and timeouts via deps:
```elixir
deps: %{
sub_agent_templates: templates,
parallel_max_concurrency: 3, # Max concurrent sub-agents (default: 5)
parallel_timeout: 60_000 # Per-task timeout in ms (default: 120_000)
}
```
### Agent Memory
Persistent memory across conversations with hybrid text + vector search:
```elixir
# Minimal setup — ETS store, keyword-only search, zero deps
agent = Nous.new("openai:gpt-4",
plugins: [Nous.Plugins.Memory],
deps: %{memory_config: %{store: Nous.Memory.Store.ETS}}
)
# Agent can now use remember/recall/forget tools
{:ok, r1} = Nous.run(agent, "Remember that my favorite color is blue")
{:ok, r2} = Nous.run(agent, "What is my favorite color?", context: r1.context)
# => Recalls "blue" from memory
```
Add semantic search with embeddings:
```elixir
agent = Nous.new("openai:gpt-4",
plugins: [Nous.Plugins.Memory],
deps: %{
memory_config: %{
store: Nous.Memory.Store.ETS,
embedding: Nous.Memory.Embedding.OpenAI,
embedding_opts: %{api_key: System.get_env("OPENAI_API_KEY")},
auto_inject: true # Auto-retrieves relevant memories before each request
}
}
)
```
**Store backends:** ETS (zero deps), SQLite (FTS5), DuckDB (FTS + vector), Muninn (Tantivy BM25), Zvec (HNSW), Hybrid (Muninn + Zvec).
**Embedding providers:** Bumblebee (local, offline), OpenAI, Local (Ollama/vLLM).
**Features:** Memory scoping (agent/user/session/global), temporal decay, importance weighting, RRF scoring, configurable auto-injection.
See the [Memory Examples](#memory-examples) section below for complete examples.
### Deep Research
Autonomous multi-step research with citations:
```elixir
{:ok, report} = Nous.Research.run(
"Best practices for Elixir deployment",
model: "openai:gpt-4o",
search_tool: &Nous.Tools.TavilySearch.search/2
)
IO.puts(report.content) # Markdown report with inline citations
```
### Agent Supervision & Persistence
Production lifecycle management with state persistence:
```elixir
# Start a supervised agent with persistence
{:ok, pid} = Nous.AgentDynamicSupervisor.start_agent(
agent, session_id: "user-123",
persistence: Nous.Persistence.ETS,
name: {:via, Registry, {Nous.AgentRegistry, "user-123"}}
)
# Agent state auto-saves; restore later
{:ok, context} = Nous.Persistence.ETS.load("user-123")
{:ok, result} = Nous.run(agent, "Continue our conversation", context: context)
```
### LiveView Integration
```elixir
defmodule MyAppWeb.ChatLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
agent = Nous.new("lmstudio:qwen3", instructions: "Be helpful.")
{:ok, assign(socket, agent: agent, messages: [], streaming: false)}
end
def handle_event("send", %{"message" => msg}, socket) do
Task.start(fn ->
Nous.run(socket.assigns.agent, msg, notify_pid: socket.root_pid)
end)
{:noreply, assign(socket, streaming: true)}
end
def handle_info({:agent_delta, text}, socket) do
{:noreply, update(socket, :current, &(&1 <> text))}
end
def handle_info({:agent_complete, result}, socket) do
messages = socket.assigns.messages ++ [%{role: :assistant, content: result.output}]
{:noreply, assign(socket, messages: messages, streaming: false)}
end
end
```
See [examples/advanced/liveview_integration.exs](examples/advanced/liveview_integration.exs) for complete patterns.
## Examples
**[Full Examples Collection](examples/README.md)** - 21 focused examples from basics to production.
### Core Examples (01-10)
| Example | Description |
|---------|-------------|
| [01_hello_world.exs](examples/01_hello_world.exs) | Minimal example |
| [02_with_tools.exs](examples/02_with_tools.exs) | Tool calling |
| [03_streaming.exs](examples/03_streaming.exs) | Streaming responses |
| [04_conversation.exs](examples/04_conversation.exs) | Multi-turn with context |
| [05_callbacks.exs](examples/05_callbacks.exs) | Callbacks + LiveView |
| [06_prompt_templates.exs](examples/06_prompt_templates.exs) | EEx templates |
| [07_module_tools.exs](examples/07_module_tools.exs) | Module-based tools |
| [08_tool_testing.exs](examples/08_tool_testing.exs) | Test helpers |
| [09_agent_server.exs](examples/09_agent_server.exs) | GenServer agent |
| [10_react_agent.exs](examples/10_react_agent.exs) | ReAct pattern |
| [13_sub_agents.exs](examples/13_sub_agents.exs) | Sub-agents (single + parallel) |
### Provider Examples
- [providers/anthropic.exs](examples/providers/anthropic.exs) - Claude, extended thinking
- [providers/openai.exs](examples/providers/openai.exs) - GPT models
- [providers/lmstudio.exs](examples/providers/lmstudio.exs) - Local AI
- [providers/switching_providers.exs](examples/providers/switching_providers.exs) - Provider comparison
### Memory Examples
- [memory/basic_ets.exs](examples/memory/basic_ets.exs) - Simplest setup, ETS + keyword search
- [memory/local_bumblebee.exs](examples/memory/local_bumblebee.exs) - Local semantic search, no API keys
- [memory/sqlite_full.exs](examples/memory/sqlite_full.exs) - SQLite + FTS5 production setup
- [memory/duckdb_full.exs](examples/memory/duckdb_full.exs) - DuckDB analytics-friendly setup
- [memory/hybrid_full.exs](examples/memory/hybrid_full.exs) - Muninn + Zvec maximum quality
- [memory/cross_agent.exs](examples/memory/cross_agent.exs) - Multi-agent shared memory with scoping
### Advanced Examples
- [advanced/context_updates.exs](examples/advanced/context_updates.exs) - Tool state management
- [advanced/error_handling.exs](examples/advanced/error_handling.exs) - Retries, fallbacks
- [advanced/telemetry.exs](examples/advanced/telemetry.exs) - Metrics, cost tracking
- [advanced/cancellation.exs](examples/advanced/cancellation.exs) - Task cancellation
- [advanced/liveview_integration.exs](examples/advanced/liveview_integration.exs) - LiveView patterns
## Telemetry
Attach handlers for monitoring:
```elixir
Nous.Telemetry.attach_default_handler()
```
**Events:**
- `[:nous, :agent, :run, :start/stop/exception]`
- `[:nous, :agent, :iteration, :start/stop]`
- `[:nous, :provider, :request, :start/stop/exception]`
- `[:nous, :tool, :execute, :start/stop/exception]`
- `[:nous, :tool, :timeout]`
- `[:nous, :context, :update]`
## Evaluation Framework
Test, benchmark, and optimize your agents:
```elixir
# Define tests
suite = Nous.Eval.Suite.new(
name: "my_tests",
default_model: "lmstudio:qwen3",
test_cases: [
Nous.Eval.TestCase.new(
id: "greeting",
input: "Say hello",
expected: %{contains: ["hello"]},
eval_type: :contains
)
]
)
# Run evaluation
{:ok, result} = Nous.Eval.run(suite)
Nous.Eval.Reporter.print(result)
```
**Features:**
- Six built-in evaluators (exact_match, fuzzy_match, contains, tool_usage, schema, llm_judge)
- Metrics collection (latency, tokens, cost)
- A/B testing with `Nous.Eval.run_ab/2`
- Parameter optimization with Bayesian, grid, or random search
- YAML test suite definitions
**CLI:**
```bash
mix nous.eval --suite test/eval/suites/basic.yaml
mix nous.optimize --suite suite.yaml --strategy bayesian --trials 20
```
See [Evaluation Guide](docs/guides/evaluation.md) for complete documentation.
## Architecture
```
Nous.new/2 → Agent struct
↓
Nous.run/3 → AgentRunner
↓
├─→ Context (messages, deps, callbacks, pubsub)
├─→ Behaviour (BasicAgent | ReActAgent | custom)
├─→ Plugins (HITL, Summarization, SubAgent, Memory, ...)
├─→ Memory (Store → Search → Scoring → Embedding)
├─→ ModelDispatcher → Provider → HTTP
├─→ ToolExecutor (timeout, validation, approval)
├─→ Callbacks (map | notify_pid | PubSub)
├─→ PubSub (Nous.PubSub → Phoenix.PubSub, optional)
├─→ Persistence (ETS | custom backend)
└─→ Research (Planner → Searcher → Synthesizer → Reporter)
```
## Development
### Prerequisites
- Erlang/OTP 26+
- Elixir 1.15+
### Setup
```bash
git clone https://github.com/nyo16/nous.git
cd nous
mix deps.get
mix compile
```
### Running Tests
```bash
# Run all tests
mix test
# Run a specific test file
mix test test/nous/decisions_test.exs
# Run tests with verbose output
mix test --trace
```
### Code Quality
```bash
# Check formatting
mix format --check-formatted
# Run credo linter
mix credo --strict
# Run dialyzer (first run builds PLT, takes a few minutes)
mix dialyzer
# All checks at once
mix compile --warnings-as-errors && mix format --check-formatted && mix credo --strict && mix test
```
### Configuration
API keys are configured via environment variables:
```bash
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
export GROQ_API_KEY="gsk_..."
# See config/config.exs for all supported providers
```
For local models (no API key needed):
```bash
# LM Studio — start the server, then:
agent = Nous.new("lmstudio:qwen3")
# Ollama — start the server, then:
agent = Nous.new("ollama:llama2")
```
### Running Examples
```bash
# Run any example script
mix run examples/01_hello_world.exs
# Run with a specific provider
OPENAI_API_KEY=sk-... mix run examples/02_with_tools.exs
```
### Generating Docs
```bash
mix docs
open doc/index.html
```
### Project Structure
```
lib/nous/
├── agent.ex # Agent struct and builder
├── agent_runner.ex # Core execution loop
├── agent_server.ex # GenServer wrapper for supervised agents
├── decisions/ # Decision graph (goals, decisions, outcomes)
│ ├── store/ # Store backends (ETS, DuckDB)
│ ├── node.ex # Node struct
│ ├── edge.ex # Edge struct
│ ├── tools.ex # LLM-callable decision tools
│ └── context_builder.ex
├── memory/ # Persistent memory with hybrid search
│ ├── store/ # Store backends (ETS, SQLite, DuckDB, etc.)
│ ├── embedding/ # Embedding providers
│ └── tools.ex # LLM-callable memory tools
├── plugins/ # Agent plugins
│ ├── decisions.ex # Decision graph plugin
│ ├── memory.ex # Memory plugin
│ ├── team_tools.ex # Team communication plugin
│ ├── sub_agent.ex # Sub-agent delegation
│ └── human_in_the_loop.ex
├── providers/ # LLM provider adapters
├── teams/ # Multi-agent team orchestration
│ ├── coordinator.ex # Team lifecycle management
│ ├── shared_state.ex # Per-team shared state (ETS)
│ ├── rate_limiter.ex # Budget and rate limiting
│ ├── role.ex # Role-based tool scoping
│ └── comms.ex # PubSub topic helpers
├── tool/ # Tool system
│ ├── behaviour.ex # Tool behaviour
│ ├── schema.ex # Declarative tool DSL
│ └── registry.ex # Tool collection and filtering
├── research/ # Deep research system
└── eval/ # Evaluation framework
```
## Contributing
Contributions welcome! See [CHANGELOG.md](CHANGELOG.md) for recent changes.
```bash
# Fork, clone, then:
mix deps.get
mix test # Make sure tests pass
mix format # Format your code
mix credo --strict # Check for issues
# Open a PR against master
```
## License
Apache 2.0 - see [LICENSE](https://github.com/nyo16/nous/blob/master/LICENSE)
## Credits
- Inspired by [Pydantic AI](https://ai.pydantic.dev/)
- HTTP: [Req](https://github.com/wojtekmach/req) + [Finch](https://github.com/sneako/finch)