# Agent Framework The Agent Framework enables building tool-using agents with conversation memory and multi-turn interactions. ## Overview The framework consists of four components: | Component | Purpose | |-----------|---------| | **Agent** | Core orchestrator for LLM + tool execution | | **Session** | Conversation memory and context | | **Registry** | Tool registration and dispatch | | **Tool** | Behaviour for custom tools | ## Creating an Agent ```elixir alias Rag.Agent.Agent alias Rag.Agent.Tools.{SearchRepos, ReadFile, AnalyzeCode} # Simple agent agent = Agent.new() # Agent with tools agent = Agent.new( tools: [SearchRepos, ReadFile, AnalyzeCode], max_iterations: 10 ) # With existing session agent = Agent.new( tools: [SearchRepos, ReadFile], session: existing_session, provider: :gemini ) ``` ### Options | Option | Default | Description | |--------|---------|-------------| | `tools` | `[]` | Tool modules to register | | `session` | new Session | Session for memory | | `provider` | Gemini | LLM provider | | `max_iterations` | 10 | Max tool calling rounds | ## Processing Queries ### Simple Processing (No Tools) ```elixir {:ok, response, agent} = Agent.process(agent, "What is Elixir?") IO.puts(response) ``` ### Processing with Tools ```elixir {:ok, response, agent} = Agent.process_with_tools(agent, "Find all GenServer modules in the codebase" ) ``` The agent will: 1. Send query to LLM with available tools 2. If LLM requests a tool, execute it 3. Send tool result back to LLM 4. Repeat until final answer or max_iterations ## Context Management ```elixir # Add context for tools agent = agent |> Agent.with_context(:repo, MyApp.Repo) |> Agent.with_context(:user_id, 123) |> Agent.with_context(:search_fn, &vector_store_search/2) # Access history history = Agent.get_history(agent) # Clear history agent = Agent.clear_history(agent) ``` ## Session Sessions maintain conversation state: ```elixir alias Rag.Agent.Session # Create session session = Session.new() session = Session.new(id: "session-123", metadata: %{user: "alice"}) # Add messages session = session |> Session.add_message(:user, "Hello") |> Session.add_message(:assistant, "Hi there!") |> Session.add_message(:system, "Context info") # Add tool result session = Session.add_tool_result(session, "search", {:ok, results}) # Query session Session.messages(session) # All messages Session.last_messages(session, 5) # Last 5 Session.message_count(session) # Count Session.token_estimate(session) # Approximate tokens # Context management session = Session.set_context(session, :repo, repo) session = Session.merge_context(session, %{repo: repo, user_id: 123}) context = Session.context(session) repo = Session.get_context(session, :repo, nil) # Format for LLM llm_messages = Session.to_llm_messages(session) ``` ## Registry Manage tool registration: ```elixir alias Rag.Agent.Registry # Create registry registry = Registry.new() registry = Registry.new(tools: [SearchRepos, ReadFile]) # Register tools registry = Registry.register(registry, CustomTool) registry = Registry.register_all(registry, [Tool1, Tool2]) # Query tools Registry.get(registry, "search") # {:ok, module} | {:error, :not_found} Registry.list(registry) # [module1, module2, ...] Registry.names(registry) # ["search", "read_file", ...] Registry.count(registry) # 3 Registry.has_tool?(registry, "search") # true # Unregister registry = Registry.unregister(registry, "old_tool") # Execute tool {:ok, result} = Registry.execute(registry, "search", %{"query" => "authentication"}, %{repo: repo, user_id: 123} ) # Format for LLM tool_definitions = Registry.format_for_llm(registry) ``` ## Creating Custom Tools Implement the `Rag.Agent.Tool` behaviour: ```elixir defmodule MyApp.Tools.CustomSearch do @behaviour Rag.Agent.Tool @impl true def name, do: "custom_search" @impl true def description do "Search for information in the knowledge base" end @impl true def parameters do %{ type: "object", properties: %{ query: %{ type: "string", description: "The search query" }, limit: %{ type: "integer", description: "Maximum results to return" } }, required: ["query"] } end @impl true def execute(args, context) do query = Map.get(args, "query") limit = Map.get(args, "limit", 10) # Access context values repo = Map.get(context, :repo) search_fn = Map.get(context, :search_fn) case search_fn.(query, limit: limit) do {:ok, results} -> {:ok, results} {:error, reason} -> {:error, reason} end end end ``` ### Tool Callbacks | Callback | Return | Description | |----------|--------|-------------| | `name/0` | `String.t()` | Unique tool identifier | | `description/0` | `String.t()` | Description for LLM | | `parameters/0` | `map()` | JSON Schema for args | | `execute/2` | `{:ok, term} \| {:error, term}` | Execute the tool | ### Context Values The context map passed to `execute/2` contains: - `:session_id` - Current session ID - `:user_id` - User identifier (if set) - `:repo` - Ecto repo (if set) - `:router` - Router for LLM calls (if set) - Any custom context from `Rag.Agent.Agent.with_context/3` ## Built-in Tools ### ReadFile Read file contents with optional line ranges: ```elixir Registry.execute(registry, "read_file", %{"path" => "lib/my_module.ex", "start_line" => 10, "end_line" => 30}, %{read_fn: &File.read/1} ) ``` **Parameters:** - `path` (string, required) - File path - `start_line` (integer, optional) - Start line (1-indexed) - `end_line` (integer, optional) - End line (inclusive) ### AnalyzeCode Analyze code structure: ```elixir Registry.execute(registry, "analyze_code", %{"code" => code_string, "language" => "elixir"}, %{} ) # Returns: %{modules: [...], functions: [...], module_count: N, function_count: N} ``` **Parameters:** - `code` (string, required) - Code to analyze - `language` (string, optional) - Language (default: "elixir") ### SearchRepos Semantic search over repositories: ```elixir Registry.execute(registry, "search_repos", %{"query" => "authentication", "limit" => 5}, %{search_fn: &vector_store_search/2} ) ``` **Parameters:** - `query` (string, required) - Search query - `limit` (integer, optional) - Max results - `source_filter` (string, optional) - Filter by source ### GetRepoContext Get repository metadata: ```elixir Registry.execute(registry, "get_repo_context", %{"repo_name" => "my_project"}, %{context_fn: &get_repo_info/1} ) ``` **Parameters:** - `repo_name` (string, required) - Repository name - `include_files` (boolean, optional) - Include file contents ## Tool Calling Workflow ``` User Query | v Build prompt with tools (Registry.format_for_llm) | v Send to LLM | v Parse response (Agent.parse_tool_call) | +---> Tool call: {"tool": "name", "args": {...}} | | | v | Execute tool (Registry.execute) | | | v | Record result (Session.add_tool_result) | | | v | Loop back to LLM (until max_iterations) | +---> Final answer | v Return response ``` ## Complete Example ```elixir alias Rag.Agent.{Agent, Registry} alias Rag.Agent.Tools.{SearchRepos, ReadFile, AnalyzeCode} # 1. Create agent with tools agent = Agent.new( tools: [SearchRepos, ReadFile, AnalyzeCode], max_iterations: 5 ) # 2. Add context for tools agent = agent |> Agent.with_context(:repo, MyApp.Repo) |> Agent.with_context(:search_fn, fn query, opts -> # Vector store search implementation {:ok, results} end) |> Agent.with_context(:read_fn, &File.read/1) # 3. Process query with tools case Agent.process_with_tools(agent, "Find and explain the authentication module") do {:ok, response, updated_agent} -> IO.puts("Response: #{response}") # Check history history = Agent.get_history(updated_agent) IO.puts("Messages: #{length(history)}") {:error, :max_iterations_exceeded} -> IO.puts("Too many tool calls") {:error, reason} -> IO.puts("Error: #{inspect(reason)}") end ``` ## Direct Tool Execution Use tools without the full agent loop: ```elixir alias Rag.Agent.Registry alias Rag.Agent.Tools.AnalyzeCode # Create registry registry = Registry.new(tools: [AnalyzeCode]) # Execute directly code = """ defmodule Calculator do def add(a, b), do: a + b defp validate(n), do: n > 0 end """ {:ok, result} = Registry.execute(registry, "analyze_code", %{"code" => code}, %{} ) IO.puts("Modules: #{inspect(result.modules)}") IO.puts("Functions: #{length(result.functions)}") ``` ## Best Practices 1. **Set max_iterations** - Prevent infinite loops 2. **Provide necessary context** - Tools need access to resources 3. **Handle errors** - Tools can fail, handle gracefully 4. **Use descriptive tool names** - LLM selects based on name/description 5. **Clear parameter schemas** - Help LLM provide correct args ## Next Steps - [Pipeline](pipelines.md) - Integrate agents in pipelines - [GraphRAG](graph_rag.md) - Use agents with knowledge graphs