README.md

# OpenAI Agents for Elixir

Build powerful AI agents in Elixir using OpenAI's Responses API. This library provides an idiomatic Elixir framework for creating agents that can use tools, delegate tasks to specialized agents, and maintain conversations with full type safety and fault tolerance.

## Table of Contents
- [Installation](#installation)
- [Setup](#setup)
- [Basic Usage](#basic-usage)
- [Core Concepts](#core-concepts)
- [Examples](#examples)
- [Elixir-Specific Features](#elixir-specific-features)
- [API Reference](#api-reference)
- [Configuration](#configuration)

## Installation

Add `openai_agents` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:openai_agents, "~> 0.1.0"}
  ]
end
```

Then run:
```bash
mix deps.get
```

## Publishing to Hex

This package is published to [hex.pm](https://hex.pm/packages/openai_agents) for easy installation in Elixir projects.

### For Maintainers

To publish a new version to hex.pm:

1. Ensure all tests pass and the working directory is clean:
   ```bash
   mix test
   mix lint
   git status  # should show no uncommitted changes
   ```

2. Update the version in `mix.exs`

3. Commit and tag the release:
   ```bash
   git add mix.exs
   git commit -m "Bump version to X.Y.Z"
   git tag -a vX.Y.Z -m "Release X.Y.Z"
   ```

4. Build and publish:
   ```bash
   mix hex.build
   mix hex.publish
   ```

5. Push changes:
   ```bash
   git push origin main --tags
   ```

### Prerequisites

- You must be authenticated with hex.pm: `mix hex.user auth`
- You must be added as a package owner (for subsequent releases)

## Setup

### 1. Configure your OpenAI API key

#### For Development/Test

Create a `.env` file in your project root (copy from `.env.example`):
```bash
cp .env.example .env
```

Then edit `.env` and add your API key:
```bash
OPENAI_API_KEY=your-api-key-here
```

The library will automatically load this file in development and test environments.

#### For Production

Set your API key as an environment variable:
```bash
export OPENAI_API_KEY="your-api-key"
```

Or configure it in your `config/runtime.exs`:
```elixir
config :openai_agents,
  api_key: System.get_env("OPENAI_API_KEY"),
  default_model: "gpt-4o"
```

### 2. Start the application

If you're using Phoenix or another OTP application, the library will start automatically. For standalone usage, add to your application's supervision tree:

```elixir
def start(_type, _args) do
  children = [
    # ... your other children
    {OpenAI.Agents.Application, []}
  ]
  
  Supervisor.start_link(children, strategy: :one_for_one)
end
```

## Basic Usage

### Creating Your First Agent

```elixir
defmodule MyApp.Assistant do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "assistant",
      instructions: "You are a helpful assistant that responds concisely."
    }
  end
end

# Run the agent
{:ok, result} = OpenAI.Agents.run(MyApp.Assistant, "What is the capital of France?")
IO.puts(result.output)
# => "The capital of France is Paris."
```

### Core Functions

The library provides three main ways to interact with agents:

```elixir
# 1. Synchronous execution (blocking)
{:ok, result} = OpenAI.Agents.run(agent_module, input, opts \\ [])

# 2. Asynchronous execution (returns a Task)
task = OpenAI.Agents.run_async(agent_module, input, opts \\ [])
{:ok, result} = Task.await(task)

# 3. Streaming execution (returns a Stream)
stream = OpenAI.Agents.stream(agent_module, input, opts \\ [])
```

## Core Concepts

### Agents

Agents are modules that implement the `OpenAI.Agent` behaviour:

```elixir
defmodule MyApp.HaikuAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "haiku_bot",
      instructions: "You only respond in haikus.",
      model: "gpt-4o",
      model_settings: %{
        temperature: 0.7,
        max_tokens: 100
      }
    }
  end
  
  # Optional lifecycle callbacks
  @impl true
  def on_start(context, state) do
    IO.puts("Agent starting...")
    {:ok, state}
  end
end
```

### Tools

Tools are functions that agents can call:

```elixir
defmodule MyApp.Tools.Weather do
  use OpenAI.Agents.Tool
  
  @impl true
  def schema do
    %{
      name: "get_weather",
      description: "Get the current weather for a city",
      parameters: %{
        type: "object",
        properties: %{
          city: %{type: "string", description: "City name"}
        },
        required: ["city"]
      }
    }
  end
  
  @impl true
  def execute(%{"city" => city}, _context) do
    # In real usage, call a weather API
    {:ok, %{temperature: 72, conditions: "Sunny", city: city}}
  end
end
```

### Context

Pass application state through the execution:

```elixir
# Define your context
context = %{
  user_id: "123",
  session_id: "abc",
  preferences: %{language: "en"}
}

# Pass it to the agent
{:ok, result} = OpenAI.Agents.run(MyApp.Assistant, "Hello", context: context)

# Access it in tools
defmodule MyApp.Tools.UserPreferences do
  use OpenAI.Agents.Tool
  
  @impl true
  def execute(_params, context) do
    user_id = context.user_context.user_id
    # Use the context data
    {:ok, %{user_id: user_id}}
  end
end
```

### Guardrails

Guardrails validate inputs and outputs to ensure safe operation:

```elixir
defmodule MyApp.Guardrails.NoPersonalInfo do
  use OpenAI.Agents.Guardrail
  
  @impl true
  def validate_input(input, _context) do
    if String.match?(input, ~r/ssn|social security|credit card/i) do
      {:error, "Cannot process personal information", %{type: "privacy_violation"}}
    else
      :ok
    end
  end
  
  @impl true
  def validate_output(output, _context) do
    if String.match?(output, ~r/password|secret/i) do
      {:error, "Output contains sensitive information", %{type: "data_leak"}}
    else
      :ok
    end
  end
end

defmodule MyApp.SecureAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "secure_agent",
      instructions: "You are a helpful assistant.",
      input_guardrails: [MyApp.Guardrails.NoPersonalInfo],
      output_guardrails: [MyApp.Guardrails.NoPersonalInfo]
    }
  end
end
```

### Handoffs

Agents can transfer conversations to specialized agents:

```elixir
defmodule MyApp.Tools.TransferToSupport do
  use OpenAI.Agents.Tool
  
  @impl true
  def schema do
    %{
      name: "transfer_to_support",
      description: "Transfer the conversation to a support specialist",
      parameters: %{
        type: "object",
        properties: %{
          reason: %{type: "string", description: "Reason for transfer"}
        },
        required: ["reason"]
      }
    }
  end
  
  @impl true
  def execute(%{"reason" => reason}, _context) do
    {:handoff, MyApp.SupportAgent, %{transfer_reason: reason}}
  end
end

defmodule MyApp.SupportAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "support_agent",
      instructions: "You are a technical support specialist."
    }
  end
end

defmodule MyApp.TriageAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "triage_agent",
      instructions: "Route users to appropriate specialists.",
      tools: [MyApp.Tools.TransferToSupport],
      handoffs: [MyApp.SupportAgent]
    }
  end
end
```

## Examples

### Example 1: Simple Q&A Agent

```elixir
defmodule MyApp.QAAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "qa_agent",
      instructions: """
      You are a helpful Q&A assistant. 
      Provide clear, accurate answers to questions.
      If you don't know something, say so.
      """
    }
  end
end

# Usage
{:ok, result} = OpenAI.Agents.run(MyApp.QAAgent, "What is Elixir?")
```

### Example 2: Agent with Tools

```elixir
defmodule MyApp.MathTools do
  use OpenAI.Agents.Tool
  
  @impl true
  def schema do
    %{
      name: "calculate",
      description: "Perform mathematical calculations",
      parameters: %{
        type: "object",
        properties: %{
          expression: %{type: "string"}
        },
        required: ["expression"]
      }
    }
  end
  
  @impl true
  def execute(%{"expression" => expr}, _context) do
    # BE CAREFUL: In production, use a safe math parser!
    try do
      {result, _} = Code.eval_string(expr)
      {:ok, %{result: result}}
    rescue
      _ -> {:error, "Invalid expression"}
    end
  end
end

defmodule MyApp.MathTutor do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "math_tutor",
      instructions: "You are a helpful math tutor. Use the calculate tool for computations.",
      tools: [MyApp.MathTools]
    }
  end
end

# Usage
{:ok, result} = OpenAI.Agents.run(MyApp.MathTutor, "What is 25 * 4 + 10?")
```

### Example 3: Multi-Agent System with Handoffs

```elixir
defmodule MyApp.SpanishAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "spanish_agent",
      instructions: "You only speak Spanish. Respond to all queries in Spanish."
    }
  end
end

defmodule MyApp.FrenchAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "french_agent", 
      instructions: "You only speak French. Respond to all queries in French."
    }
  end
end

defmodule MyApp.TriageAgent do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "triage_agent",
      instructions: """
      You are a triage agent that routes conversations to the right language expert.
      If the user wants Spanish, transfer to the Spanish agent.
      If the user wants French, transfer to the French agent.
      Otherwise, respond in English.
      """,
      handoffs: [MyApp.SpanishAgent, MyApp.FrenchAgent]
    }
  end
end

# Usage
{:ok, result} = OpenAI.Agents.run(MyApp.TriageAgent, "I need help in Spanish")
# Agent will hand off to SpanishAgent
```

### Example 4: Streaming Responses

```elixir
defmodule MyApp.StoryTeller do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "storyteller",
      instructions: "You are a creative storyteller."
    }
  end
end

# Stream the response
MyApp.StoryTeller
|> OpenAI.Agents.stream("Tell me a story about a brave rabbit")
|> Stream.each(fn event ->
  case event do
    %OpenAI.Agents.Events.TextDelta{text: text} -> 
      # Print text as it arrives
      IO.write(text)
      
    %OpenAI.Agents.Events.ToolCall{name: name} -> 
      IO.puts("\n[Calling tool: #{name}]")
      
    %OpenAI.Agents.Events.ResponseCompleted{usage: usage} ->
      IO.puts("\n\nTokens used: #{usage.total_tokens}")
      
    _ -> 
      :ok
  end
end)
|> Stream.run()
```

### Example 5: Agents with Guardrails

```elixir
defmodule MyApp.Guardrails.MathOnly do
  use OpenAI.Agents.Guardrail
  
  @impl true
  def validate_input(input, _context) do
    if String.match?(input, ~r/math|calculate|number|equation/i) do
      :ok
    else
      {:error, "I only help with math questions", %{reason: "off_topic"}}
    end
  end
end

defmodule MyApp.MathHelper do
  use OpenAI.Agent
  
  @impl true
  def configure do
    %{
      name: "math_helper",
      instructions: "You are a math helper. Only answer math-related questions.",
      input_guardrails: [MyApp.Guardrails.MathOnly]
    }
  end
end

# This will work
{:ok, result} = OpenAI.Agents.run(MyApp.MathHelper, "What is 2+2?")

# This will be rejected by the guardrail
{:error, {:guardrail_triggered, _}} = OpenAI.Agents.run(MyApp.MathHelper, "Tell me about dogs")
```

## Elixir-Specific Features

### 1. Process-Based Isolation

Each agent run is isolated in its own process, providing fault tolerance:

```elixir
# If an agent crashes, it doesn't affect your application
task1 = OpenAI.Agents.run_async(MyAgent, "Query 1")
task2 = OpenAI.Agents.run_async(MyAgent, "Query 2") 

# Even if task1 crashes, task2 continues
```

### 2. Concurrent Tool Execution

Tools are executed in parallel automatically:

```elixir
defmodule MyApp.SlowTool do
  use OpenAI.Agents.Tool
  
  @impl true
  def execute(_params, _context) do
    # These will run concurrently if the agent calls multiple tools
    Process.sleep(1000)
    {:ok, %{data: "result"}}
  end
end
```

### 3. GenStage-Based Streaming

The streaming implementation uses GenStage for proper backpressure handling:

```elixir
# The stream automatically handles backpressure
stream = OpenAI.Agents.stream(MyAgent, "Generate a long response")

# Process in batches with Flow (optional)
stream
|> Flow.from_enumerable(max_demand: 10)
|> Flow.map(&process_event/1)
|> Flow.run()
```

### 4. Telemetry Integration

Built-in telemetry for monitoring:

```elixir
# Attach to telemetry events
:telemetry.attach_many(
  "my-app-handler",
  [
    [:openai_agents, :run, :start],
    [:openai_agents, :run, :stop],
    [:openai_agents, :tool, :start],
    [:openai_agents, :tool, :stop]
  ],
  &MyApp.TelemetryHandler.handle_event/4,
  nil
)
```

### 5. Supervision and Fault Tolerance

The library uses OTP supervision for reliability:

```elixir
# Agents run under a DynamicSupervisor
# If a run fails, it's isolated and won't crash your system

# You can also configure restart strategies
config :openai_agents,
  runner_restart_strategy: :transient,
  max_restarts: 3
```

### 6. Registry-Based Agent Discovery

Agents can be discovered dynamically:

```elixir
# Register agents with custom names
defmodule MyApp.DynamicAgent do
  use OpenAI.Agent
  
  def start_link(opts) do
    name = Keyword.get(opts, :name)
    Registry.register(MyApp.AgentRegistry, name, __MODULE__)
    {:ok, self()}
  end
end

# Find and run agents dynamically
[{pid, module}] = Registry.lookup(MyApp.AgentRegistry, "custom_agent")
OpenAI.Agents.run(module, "Hello")
```

## API Reference

### Main Functions

- `OpenAI.Agents.run/3` - Run an agent synchronously
- `OpenAI.Agents.run_async/3` - Run an agent asynchronously  
- `OpenAI.Agents.stream/3` - Stream an agent's response

### Options

All functions accept these options:

- `:context` - Application-specific context
- `:timeout` - Maximum execution time (default: 60000ms)
- `:max_turns` - Maximum conversation turns (default: 10)
- `:trace_id` - Custom trace ID for debugging

### Behaviors

- `OpenAI.Agent` - Define an agent
- `OpenAI.Agents.Tool` - Define a tool
- `OpenAI.Agents.Guardrail` - Define a guardrail

## Configuration

Full configuration options in `config/config.exs`:

```elixir
config :openai_agents,
  # Required
  api_key: System.get_env("OPENAI_API_KEY"),
  
  # Optional
  base_url: "https://api.openai.com/v1",
  default_model: "gpt-4o",
  max_turns: 10,
  timeout: 60_000,
  
  # Telemetry and tracing
  trace_processors: [
    OpenAI.Agents.Tracing.ConsoleProcessor,
    {OpenAI.Agents.Tracing.FileProcessor, path: "/tmp/traces"}
  ],
  
  # Pool configuration for HTTP client
  pool_size: 10,
  pool_timeout: 5_000
```

## Common Patterns

### Error Handling

```elixir
case OpenAI.Agents.run(MyAgent, "Hello") do
  {:ok, result} -> 
    IO.puts(result.output)
    
  {:error, {:guardrail_triggered, {guardrail, reason, metadata}}} ->
    IO.puts("Guardrail #{guardrail} blocked: #{reason}")
    
  {:error, {:max_turns_exceeded, turns}} ->
    IO.puts("Agent exceeded #{turns} turns")
    
  {:error, {:api_error, status, body}} ->
    IO.puts("API error #{status}: #{body}")
    
  {:error, reason} ->
    IO.puts("Error: #{inspect(reason)}")
end
```

## Examples

### Travel Booking Example

A comprehensive example demonstrating all major framework features through a realistic travel booking workflow:

```bash
cd examples/travel_booking
mix deps.get
mix run -e "TravelBooking.main()"
```

See [`examples/travel_booking/README.md`](examples/travel_booking/README.md) for detailed setup and usage instructions.

This example showcases:
- Multi-agent handoffs between specialized agents
- Dynamic instructions based on user context
- Tool integration for external functions
- Guardrails for input/output validation
- Context management across agents

### Testing Agents

The library includes both unit tests and integration tests:

#### Running Tests

```bash
# Run unit tests only (default)
mix test

# Run integration tests that call OpenAI API
mix test --include remote

# Run only remote/integration tests
mix test --only remote
```

**Note:** Integration tests require a valid `OPENAI_API_KEY` environment variable and will make real API calls. They are excluded by default to prevent accidental API usage and costs.

#### Writing Tests

For unit tests, mock the API calls:

```elixir
# In your tests
defmodule MyApp.AgentTest do
  use ExUnit.Case
  
  test "agent responds correctly" do
    # Use Bypass for mocking API calls
    bypass = Bypass.open()
    
    Application.put_env(:openai_agents, :base_url, "http://localhost:#{bypass.port}")
    
    Bypass.expect_once(bypass, "POST", "/responses", fn conn ->
      Plug.Conn.resp(conn, 200, Jason.encode!(%{
        output: [%{type: "message", content: [%{type: "output_text", text: "Hello!"}]}],
        usage: %{total_tokens: 10}
      }))
    end)
    
    assert {:ok, result} = OpenAI.Agents.run(MyApp.Assistant, "Hi")
    assert result.output == "Hello!"
  end
end
```

For integration tests that need the real API:

```elixir
defmodule MyApp.AgentIntegrationTest do
  use ExUnit.Case
  
  # Tag the entire module as remote
  @moduletag :remote
  
  test "agent handles real conversations" do
    {:ok, result} = OpenAI.Agents.run(MyApp.Assistant, "Hello")
    assert is_binary(result.output)
    assert result.usage.total_tokens > 0
  end
end
```

## Learn More

- [Full API Documentation](https://hexdocs.pm/openai_agents)
- [GitHub Repository](https://github.com/yourusername/openai_agents)
- [OpenAI Responses API Docs](https://platform.openai.com/docs)

## License

MIT License - see LICENSE file for details.