Packages
nous
0.7.1
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)
Nous AI is an AI agent framework for Elixir with support for any OpenAI-compatible API. Features include tool calling, multi-provider support, local LLM execution, and built-in utilities for dates, strings, web search, and task tracking.
## Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:nous, "~> 0.5.0"},
{:openai_ex, "~> 0.9.17"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
```elixir
# Create an agent
agent = Nous.new("lmstudio:qwen/qwen3-30b",
instructions: "Be helpful and concise"
)
# Run it
{:ok, result} = Nous.run(agent, "What is 2+2?")
IO.puts(result.output) # "4"
IO.puts("Tokens: #{result.usage.total_tokens}")
```
## Supported Providers
| Provider | Model String | HTTP Client | Streaming |
|----------|-------------|-------------|-----------|
| LM Studio | `lmstudio:qwen/qwen3-30b` | Custom SSE | Tested |
| OpenAI | `openai:gpt-4` | OpenaiEx | Tested |
| Anthropic | `anthropic:claude-sonnet-4-5-20250929` | Native | Tested |
| Google Gemini | `gemini:gemini-2.0-flash-exp` | Native | Tested |
| Mistral AI | `mistral:ministral-3-14b-instruct-2512` | Req | Tested |
| Groq | `groq:llama-3.1-70b-versatile` | OpenaiEx | Supported |
| Ollama | `ollama:llama2` | Custom SSE | Supported |
| vLLM | `vllm:model` + `:base_url` | Custom SSE | Tested |
| SGLang | `sglang:model` + `:base_url` | Custom SSE | Supported |
| OpenRouter | `openrouter:anthropic/claude-3.5-sonnet` | OpenaiEx | Supported |
| Together AI | `together:meta-llama/Llama-3-70b-chat-hf` | Custom SSE | Supported |
| Custom | `custom:model` + `:base_url` | Custom SSE | Supported |
**Local (Zero Cost):** LM Studio, Ollama, vLLM, SGLang
**Cloud:** OpenAI, Anthropic, Mistral AI, Groq, OpenRouter, Together AI
```elixir
# Switch providers with one line change
agent = Nous.new("lmstudio:qwen/qwen3-30b") # Local
agent = Nous.new("openai:gpt-4") # Cloud
agent = Nous.new("anthropic:claude-sonnet-4-5-20250929") # Claude
agent = Nous.new("mistral:ministral-3-14b-instruct-2512") # Mistral
```
## Agent Types
| Feature | `ReActAgent` | Standard `Agent` |
|---------|--------------|------------------|
| **Planning** | Built-in structured planning | No planning (you control flow) |
| **Todo tracking** | Automatic task breakdown | Manual (via custom tools) |
| **Completion** | Explicit `final_answer` required | Stops naturally |
| **Best for** | Complex multi-step problems | Simple tasks, custom workflows |
| **Token usage** | Higher (planning + tracking) | Lower (direct execution) |
### ReActAgent (Enhanced Planning)
Use for complex, multi-step problems with structured planning. Agent creates plans, tracks todos, and documents findings.
```elixir
agent = Nous.ReActAgent.new("lmstudio:qwen/qwen3-30b",
tools: [&search/2, &calculate/2]
)
{:ok, result} = Nous.ReActAgent.run(agent,
"Who is the oldest F1 driver and when did they win their first championship?"
)
```
See [examples/react_agent_enhanced_demo.exs](examples/react_agent_enhanced_demo.exs) or [by_feature/patterns/](examples/by_feature/README.md#-patterns-agent-reasoning--architecture)
### Standard Agent (Flexible & Simple)
Use for simple Q&A, single-step tasks, or custom workflows where you control behavior.
```elixir
agent = Nous.new("openai:gpt-4",
tools: [&calculate/2]
)
{:ok, result} = Nous.run(agent, "Calculate 15 * 23")
```
## Built-in Tools
### DateTime Tools
Date/time operations with timezone support, date arithmetic, and formatting.
```elixir
alias Nous.Tools.DateTimeTools
tools: [&DateTimeTools.current_date/2, &DateTimeTools.date_difference/2, &DateTimeTools.add_days/2]
```
See [examples/datetime_tools_demo.exs](examples/datetime_tools_demo.exs) or [by_feature/tools/](examples/by_feature/README.md#-tools-function-calling--actions)
### String Tools
Text manipulation: uppercase, replace, split, palindrome detection, number extraction.
```elixir
alias Nous.Tools.StringTools
tools: [&StringTools.to_uppercase/2, &StringTools.replace_text/2, &StringTools.extract_numbers/2]
```
See [examples/string_tools_demo.exs](examples/string_tools_demo.exs) or [by_feature/tools/](examples/by_feature/README.md#-tools-function-calling--actions)
### Todo Tools
Automatic task breakdown and progress tracking for multi-step workflows.
```elixir
alias Nous.Tools.TodoTools
agent = Nous.new("lmstudio:qwen/qwen3-30b",
enable_todos: true,
tools: [&TodoTools.add_todo/2, &TodoTools.complete_todo/2]
)
```
See [examples/todo_tools_demo.exs](examples/todo_tools_demo.exs) or [by_feature/tools/](examples/by_feature/README.md#-tools-function-calling--actions)
### Brave Search (Web Search)
Search the web for current information. Requires `BRAVE_API_KEY` ([get free key](https://brave.com/search/api/)).
```elixir
alias Nous.Tools.BraveSearch
tools: [&BraveSearch.web_search/2, &BraveSearch.news_search/2]
```
See [examples/brave_search_demo.exs](examples/brave_search_demo.exs) or [by_feature/tools/](examples/by_feature/README.md#-tools-function-calling--actions)
## Features
### Tool Calling
Define Elixir functions as tools. AI calls them automatically when needed.
```elixir
def search_database(_ctx, %{"query" => q}), do: DB.search(q)
agent = Nous.new("openai:gpt-4",
tools: [&search_database/2]
)
```
### Tools with Context
Pass dependencies (user, database, API keys) via context:
```elixir
def get_balance(ctx, _args), do: DB.get_balance(ctx.deps.database, ctx.deps.user.id)
{:ok, result} = Nous.run(agent, "What's my balance?",
deps: %{user: %{id: 123}, database: MyApp.DB}
)
```
See [examples/tools_with_context.exs](examples/tools_with_context.exs) or [custom_tools_guide.exs](examples/custom_tools_guide.exs)
### Conversations
```elixir
{:ok, r1} = Nous.run(agent, "Tell me a joke")
{:ok, r2} = Nous.run(agent, "Explain it", message_history: r1.new_messages)
```
### Streaming
```elixir
{:ok, stream} = Nous.run_stream(agent, "Write a poem")
stream |> Stream.each(fn {:text_delta, t} -> IO.write(t) end) |> Stream.run()
```
#### Stream Events
| Event | Description |
|-------|-------------|
| `{:text_delta, text}` | Incremental text content |
| `{:thinking_delta, text}` | Reasoning/thinking content (vLLM, DeepSeek, SGLang) |
| `{:tool_call_delta, calls}` | Tool call information |
| `{:finish, reason}` | Stream completion |
#### Thinking/Reasoning Streams
For models with reasoning capabilities (DeepSeek-R1, QwQ, etc.):
```elixir
stream |> Stream.each(fn
{:thinking_delta, t} -> IO.write("[thinking] #{t}")
{:text_delta, t} -> IO.write(t)
_ -> :ok
end) |> Stream.run()
```
### Anthropic Extended Features
```elixir
# 1M token context window
agent = Nous.new("anthropic:claude-sonnet-4-5-20250929",
model_settings: %{enable_long_context: true}
)
# Extended thinking mode
agent = Nous.new("anthropic:claude-sonnet-4-5-20250929",
model_settings: %{thinking: %{type: "enabled", budget_tokens: 5000}}
)
```
### LiveView Integration
```elixir
# Spawn linked streaming process from LiveView
spawn_link(fn ->
agent = Nous.new("anthropic:claude-sonnet-4-5-20250929")
{:ok, stream} = Nous.run_stream(agent, message)
stream
|> Stream.each(fn
{:text_delta, text} -> send(parent, {:stream_chunk, text})
{:finish, result} -> send(parent, {:stream_complete, result})
_ -> :ok
end)
|> Stream.run()
end)
```
See [liveview_streaming_example.ex](examples/liveview_streaming_example.ex) for real-time streaming or [LiveView Integration Guide](docs/guides/liveview-integration.md) for patterns
## Logging & Telemetry
Configure log level in `config/config.exs`:
```elixir
config :logger, level: :info # or :debug, :warning, :error
```
Attach telemetry handlers:
```elixir
Nous.Telemetry.attach_default_handler()
```
Events: `[:nous, :agent, :run, :*]`, `[:nous, :model, :request, :*]`, `[:nous, :tool, :execute, :*]`
## Examples
**🚀 [Get Started in 5 Minutes](docs/getting-started.md)** - Quick setup guide with local or cloud options.
**📚 [Full Examples Collection](examples/README.md)** - Comprehensive learning path from beginner to production.
### Learning Path Overview
**🟢 Beginner** (5-15 minutes each)
- [basic_hello_world.exs](examples/basic_hello_world.exs) - 30-second minimal example
- [test_lm_studio.exs](examples/test_lm_studio.exs) - Local LM Studio setup
- [tools_simple.exs](examples/tools_simple.exs) - Basic tool calling
- [calculator_demo.exs](examples/calculator_demo.exs) - Multi-tool chaining
**🟡 Intermediate** (15-45 minutes each)
- [streaming_example.exs](examples/streaming_example.exs) - Real-time responses
- [conversation_history_example.exs](examples/conversation_history_example.exs) - Multi-turn chat
- [error_handling_example.exs](examples/error_handling_example.exs) - Graceful failure handling
- [cost_tracking_example.exs](examples/cost_tracking_example.exs) - Monitor token usage
**🔴 Advanced** (45+ minutes each)
- [Trading Desk](examples/trading_desk/README.md) - Production multi-agent system
- [Council](examples/council/README.md) - Multi-LLM deliberation
- [Coderex](examples/coderex/README.md) - AI code editor with SEARCH/REPLACE
**Browse by:**
- [Skill Level](examples/by_level/README.md) - Beginner → Intermediate → Advanced
- [Provider](examples/by_provider/README.md) - Anthropic, OpenAI, Local, etc.
- [Feature](examples/by_feature/README.md) - Tools, Streaming, Patterns, etc.
**Quick Templates:**
- [templates/](examples/templates/README.md) - Copy-paste starter files
## Architecture
```
User Code
↓
Nous.Agent (config)
↓
Nous.AgentRunner (execution loop)
↓
├─→ OpenAICompatible (model adapter)
│ ↓
│ ┌─────────────────────────────────────────┐
│ │ Cloud Providers Local Providers │
│ │ (OpenAI, Groq, (vLLM, SGLang, │
│ │ OpenRouter) LM Studio, Ollama) │
│ │ ↓ ↓ │
│ │ OpenaiEx Custom SSE Client │
│ │ (HTTP) (Finch + Req) │
│ └─────────────────────────────────────────┘
│ ↓
│ StreamNormalizer (behaviour)
│ ↓
│ Normalized stream events
│
├─→ ToolExecutor (run functions)
├─→ Messages (format conversion)
└─→ Usage (track tokens)
```
### Stream Normalizer
Nous uses an extensible behaviour pattern for normalizing streaming responses from different providers:
```elixir
# Default normalizer handles most OpenAI-compatible providers
Nous.StreamNormalizer.OpenAI
# Mistral has its own normalizer
Nous.StreamNormalizer.Mistral
```
#### Custom Normalizer
For providers with unique formats, implement the `Nous.StreamNormalizer` behaviour:
```elixir
defmodule MyApp.CustomNormalizer do
@behaviour Nous.StreamNormalizer
@impl true
def normalize_chunk(chunk) do
# Transform provider-specific format to stream events
[{:text_delta, chunk["custom_field"]}]
end
@impl true
def complete_response?(chunk), do: false
@impl true
def convert_complete_response(_chunk), do: []
end
# Use it with your model
agent = Nous.new("openai_compatible:custom-model",
base_url: "http://custom-server/v1",
stream_normalizer: MyApp.CustomNormalizer
)
```
## Stats
- **Lines:** ~2,500 | **Modules:** 20 | **Tests:** 18 passing | **Providers:** 10+
## Contributing
Contributions welcome! Areas for improvement:
- More comprehensive tests
- Structured output validation (Ecto integration)
- Performance benchmarks
- More examples
## License
Apache 2.0 License - see [LICENSE](https://github.com/nyo16/nous/blob/master/LICENSE)
## Credits
- Inspired by [Pydantic AI](https://ai.pydantic.dev/)
- Built with [openai_ex](https://github.com/cyberchitta/openai_ex) (cloud providers)
- Custom SSE streaming with [Finch](https://github.com/sneako/finch) (local providers)
- HTTP client [Req](https://github.com/wojtekmach/req) (Mistral, non-streaming)
- Validation with [ecto](https://github.com/elixir-ecto/ecto)