Skip to main content

Introduction

Arcana’s Agent module provides a composable, pipeline-based approach to agentic RAG (Retrieval-Augmented Generation). Build sophisticated question-answering systems by chaining modular steps together.

Basic Pipeline

Arcana.Agent.new("What is Elixir?")
|> Arcana.Agent.search()
|> Arcana.Agent.answer()

Architecture

Context Flow

The Arcana.Agent.Context struct flows through the pipeline, accumulating results at each step:
%Arcana.Agent.Context{
  question: "original question",
  rewritten_query: nil,      # Set by rewrite/2
  expanded_query: nil,       # Set by expand/2
  sub_questions: nil,        # Set by decompose/2
  collections: nil,          # Set by select/2
  skip_retrieval: false,     # Set by gate/2
  results: [],               # Set by search/2
  answer: nil,               # Set by answer/2
  # ... configuration and metadata
}

Pipeline Steps

Each step transforms the context and passes it forward:
  1. gate/2 - Decide if retrieval is needed
  2. rewrite/2 - Clean conversational input
  3. select/2 - Choose which collections to search
  4. expand/2 - Add synonyms and related terms
  5. decompose/2 - Break complex questions into sub-questions
  6. search/2 - Execute retrieval
  7. reason/2 - Multi-hop reasoning with additional searches
  8. rerank/2 - Re-score and re-order results
  9. answer/2 - Generate final answer

Complete Pipeline Example

# Advanced pipeline with all steps
ctx =
  Arcana.Agent.new("How do Elixir processes communicate?")
  |> Arcana.Agent.gate()                          # Skip retrieval for general knowledge?
  |> Arcana.Agent.rewrite()                       # Clean input
  |> Arcana.Agent.select(collections: ["docs", "guides", "api"])
  |> Arcana.Agent.expand()                        # Add related terms
  |> Arcana.Agent.decompose()                     # Break into sub-questions
  |> Arcana.Agent.search()                        # Execute searches
  |> Arcana.Agent.reason(max_iterations: 2)       # Multi-hop if needed
  |> Arcana.Agent.rerank(threshold: 7)            # Re-score results
  |> Arcana.Agent.answer()

ctx.answer
# => "Elixir processes communicate through message passing..."

Configuration

Set defaults in your application config:
config :arcana,
  repo: MyApp.Repo,
  llm: &MyApp.LLM.complete/1
This avoids passing :repo and :llm to every pipeline.

Query Transformation Chain

Queries are transformed through the pipeline in this priority order:
  1. expanded_query (from expand/2) - highest priority
  2. rewritten_query (from rewrite/2)
  3. question (original) - fallback
Each transformation step uses the effective query from previous steps.

Error Handling

If any step sets ctx.error, subsequent steps are skipped:
ctx
|> Arcana.Agent.search()  # Sets error on failure
|> Arcana.Agent.answer()  # Skipped if error is set

if ctx.error do
  Logger.error("Pipeline failed: #{ctx.error}")
else
  render_answer(ctx.answer)
end

Telemetry Events

Every step emits telemetry events for observability:
  • [:arcana, :agent, :gate]
  • [:arcana, :agent, :rewrite]
  • [:arcana, :agent, :select]
  • [:arcana, :agent, :expand]
  • [:arcana, :agent, :decompose]
  • [:arcana, :agent, :search]
  • [:arcana, :agent, :reason]
  • [:arcana, :agent, :rerank]
  • [:arcana, :agent, :answer]
  • [:arcana, :agent, :self_correct]
:telemetry.attach(
  "my-handler",
  [:arcana, :agent, :search],
  &MyApp.Telemetry.handle_event/4,
  nil
)

Next Steps

gate/2

Decide if retrieval is needed

rewrite/2

Clean conversational input

select/2

Choose collections to search

search/2

Execute retrieval

Build docs developers (and LLMs) love