# OpEx
An agentic LLM toolkit for Elixir.
## Overview
- **OpenAI-compatible API Client**: HTTP client with automatic retry logic and error handling
- **MCP Support**: Full support for Model Context Protocol servers via stdio and HTTP transports
- **Flexible Chat Loop**: Tool calling with customizable hooks for integration
- **Session Management**: Automatic health monitoring and reconnection for MCP servers
## Architecture
### Core Modules
- **`OpEx.Client`** - HTTP client for OpenAI-compatible API with exponential backoff retry logic
- **`OpEx.Chat`** - Chat conversation loop with tool calling and hook support
- **`OpEx.MCP.StdioClient`** - MCP client for stdio transport (local process spawning)
- **`OpEx.MCP.HttpClient`** - MCP client for HTTP transport (remote MCP servers)
- **`OpEx.MCP.SessionManager`** - Manages multiple MCP server sessions with health checks
- **`OpEx.MCP.Tools`** - Utilities for converting between MCP and OpenAI tool formats
### Hooks System
OpEx uses a hooks-based architecture to avoid hard-coded dependencies. You can customize behavior via:
- **`custom_tool_executor`** - Execute application-specific tools
- **`on_assistant_message`** - Handle assistant messages (e.g., save to database)
- **`on_tool_result`** - Handle tool results (e.g., logging, metrics)
See `OpEx.Hooks` for detailed documentation.
## Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:opex, path: "opex"} # Or publish to Hex
]
end
```
## Quick Start
```elixir
# 1. Create OpenRouter client
client = OpEx.Client.new(System.get_env("OPENROUTER_KEY"))
# 2. Start MCP session manager
{:ok, _pid} = OpEx.MCP.SessionManager.start_link(name: MyApp.MCPManager)
# 3. Add MCP servers
{:ok, server_id} = OpEx.MCP.SessionManager.add_server(MyApp.MCPManager, %{
"command" => "npx",
"args" => ["-y", "@modelcontextprotocol/server-filesystem"],
"env" => []
})
# 4. Create chat session with hooks
session = OpEx.Chat.new(client,
mcp_clients: [{:ok, server_id}],
custom_tool_executor: &MyApp.execute_custom_tool/3,
on_assistant_message: &MyApp.save_message/2
)
# 5. Have a conversation
{:ok, response} = OpEx.Chat.chat(session,
model: "anthropic/claude-3.5-sonnet",
messages: [%{"role" => "user", "content" => "List files in /tmp"}],
system_prompt: "You are a helpful assistant",
context: %{conversation_id: 123}
)
```
## MCP Server Examples
### Stdio Transport (Local)
```elixir
# Filesystem server
%{
"command" => "npx",
"args" => ["-y", "@modelcontextprotocol/server-filesystem"],
"env" => []
}
# Brave Search server
%{
"command" => "npx",
"args" => ["-y", "@modelcontextprotocol/server-brave-search"],
"env" => [{"BRAVE_API_KEY", api_key}]
}
```
### HTTP Transport (Remote)
```elixir
%{
"url" => "https://api.example.com/mcp",
"auth_token" => "your-token",
"execution_id" => "exec-123" # Optional
}
```
## Custom Tools
Define custom tools and provide an executor function:
```elixir
custom_tools = [
%{
"type" => "function",
"function" => %{
"name" => "search_database",
"description" => "Search the internal database",
"parameters" => %{
"type" => "object",
"properties" => %{
"query" => %{"type" => "string", "description" => "Search query"}
},
"required" => ["query"]
}
}
}
]
def execute_custom_tool("search_database", args, _context) do
results = MyApp.Database.search(args["query"])
{:ok, %{"results" => results}}
end
def execute_custom_tool(_, _, _), do: {:error, :tool_not_found}
session = OpEx.Chat.new(client,
custom_tools: custom_tools,
custom_tool_executor: &execute_custom_tool/3
)
```
## Configuration
### OpenRouter Client Options
```elixir
client = OpEx.Client.new(api_key,
base_url: "https://openrouter.ai/api/v1", # Default
user_agent: "my-app/1.0.0",
app_title: "My Application" # For X-Title header
)
```
### Chat Options
```elixir
OpEx.Chat.chat(session,
model: "anthropic/claude-3.5-sonnet", # Required
messages: messages, # Required
system_prompt: "You are helpful", # Optional
execute_tools: true, # Auto-execute tools (default: true)
context: %{} # Passed to all hooks (default: %{})
)
```
## Error Handling
OpEx automatically retries transient errors:
- **HTTP 429**: Rate limits (retry with 5s+ backoff)
- **HTTP 500-504, 508**: Server errors (retry with 2s+ backoff)
- **Transport errors**: Closed connections, timeouts (retry with 1s+ backoff)
Maximum 3 retries with exponential backoff.
## Future Enhancements
Potential additions for OpEx:
- [ ] Streaming support (SSE from OpenRouter)
- [ ] Telemetry events
- [ ] Supervision tree helpers
## License
MIT