# `Lotus.AI`
[🔗](https://github.com/elixir-lotus/lotus/blob/v1.0.0/lib/lotus/ai.ex#L1)

AI-powered query generation for Lotus.

## Configuration

Configure AI in your application config:

    config :lotus,
      ai: [
        enabled: true,
        model: "anthropic:claude-opus-4",
        api_key: {:system, "ANTHROPIC_API_KEY"}
      ]

The `model` key accepts any model string supported by ReqLLM, e.g.:

- `"openai:gpt-4o"` (default)
- `"anthropic:claude-opus-4"`
- `"google:gemini-2.0-flash"`
- `"groq:llama-3.3-70b-versatile"`
- Any other provider supported by ReqLLM

## Usage

    {:ok, result} = Lotus.AI.generate_query(
      prompt: "Show me all users who signed up last month",
      data_source: "postgres"
    )

    # Returns:
    # %{
    #   statement: "SELECT * FROM users WHERE created_at >= ...",
    #   variables: [],
    #   model: "openai:gpt-4o",
    #   usage: %{total_tokens: 150}
    # }

## Error Handling

The `generate_query/1` function returns structured error tuples that clients can
pattern match on for custom handling and internationalization (i18n):

- `{:ok, result}` - Successfully generated statement
- `{:error, :not_configured}` - AI features not enabled in config
- `{:error, :api_key_not_configured}` - API key missing or invalid
- `{:error, {:unable_to_generate, reason}}` - LLM refused (not a data question)
- `{:error, term}` - Other errors (API failures, network issues, etc.)

# `feature`

```elixir
@type feature() :: :generation | :optimization | :explanation
```

The set of AI features an adapter may support per-source.

# `enabled?`

```elixir
@spec enabled?() :: boolean()
```

Check if AI features are enabled and configured.

Returns `true` if AI is properly configured, `false` otherwise.

## Examples

    Lotus.AI.enabled?()
    # => true

# `explain_query`

```elixir
@spec explain_query(keyword()) :: {:ok, map()} | {:error, term()}
```

Get an AI-powered plain-language explanation of a query.

Supports explaining a full query or a selected fragment. When a fragment
is provided, the full query is sent as context so the AI can explain even
isolated terms accurately.

## Options

- `:statement` (required) - The full query statement
- `:fragment` (optional) - A selected portion of the query to explain
- `:data_source` (required) - Name of the data source to resolve schema context

## Returns

- `{:ok, result}` - Map with `:explanation`, `:model`, and `:usage`
- `{:error, term}` - Structured error tuple

## Examples

    # Explain a full query
    {:ok, result} = Lotus.AI.explain_query(
      statement: "SELECT d.name, COUNT(o.id) FROM departments d LEFT JOIN orders o ...",
      data_source: "postgres"
    )

    result.explanation
    # => "This query shows departments ranked by total order count..."

    # Explain a selected fragment
    {:ok, result} = Lotus.AI.explain_query(
      statement: "SELECT d.name FROM departments d LEFT JOIN employees e ON e.department_id = d.id",
      fragment: "LEFT JOIN employees e ON e.department_id = d.id",
      data_source: "postgres"
    )

# `generate_query`

```elixir
@spec generate_query(keyword()) :: {:ok, map()} | {:error, term()}
```

Generate a query statement from a natural language prompt.

Uses the globally configured AI provider from application config.

## Options

- `:prompt` (required) - Natural language description of desired query
- `:data_source` (required) - Name of the data source to query against
- `:read_only` (optional) - When `true` (default), the AI only generates read-only
  queries. Set to `false` to allow the AI to generate write queries.

## Returns

- `{:ok, result}` - Successfully generated statement with metadata
- `{:error, term}` - Structured error tuple (see module docs for error types)

## Examples

    {:ok, result} = Lotus.AI.generate_query(
      prompt: "Count active users by signup month",
      data_source: "postgres"
    )

    result.statement
    # => "SELECT DATE_TRUNC('month', created_at) as month, COUNT(*) FROM users WHERE status = 'active' GROUP BY month"

    result.model
    # => "openai:gpt-4o"

    result.usage
    # => %{prompt_tokens: 150, completion_tokens: 50, total_tokens: 200}

    # Error handling with pattern matching
    {:error, :not_configured} = Lotus.AI.generate_query(
      prompt: "some query",
      data_source: "postgres"
    )

# `generate_query_with_context`

```elixir
@spec generate_query_with_context(keyword()) :: {:ok, map()} | {:error, term()}
```

Generate a query statement from a natural language prompt, with conversation context.

Enables multi-turn conversations by accepting conversation history. The AI
can refine queries, fix errors, and provide iterative improvements.

## Options

- `:prompt` (required) - Natural language description of desired query
- `:data_source` (required) - Name of the data source to query against
- `:conversation` (optional) - Conversation struct with message history
- `:read_only` (optional) - When `true` (default), the AI only generates read-only
  queries. Set to `false` to allow the AI to generate write queries.

## Returns

- `{:ok, result}` - Successfully generated statement with metadata
- `{:error, term}` - Structured error tuple (see module docs for error types)

## Examples

    # Simple single-turn (same as generate_query/1)
    {:ok, result} = Lotus.AI.generate_query_with_context(
      prompt: "Show active users",
      data_source: "postgres"
    )

    # Multi-turn with conversation history
    conversation = Conversation.new()
    conversation = Conversation.add_user_message(conversation, "Show active users")

    {:ok, result} = Lotus.AI.generate_query_with_context(
      prompt: "Show active users",
      data_source: "postgres",
      conversation: conversation
    )

    # If query fails, add error to conversation
    conversation = Conversation.add_query_result(conversation, {:error, "column 'status' not found"})

    # AI can now fix the error with full context
    {:ok, fixed_result} = Lotus.AI.generate_query_with_context(
      prompt: "Fix the error",
      data_source: "postgres",
      conversation: conversation
    )

# `model`

```elixir
@spec model() :: {:ok, String.t()} | {:error, :not_configured}
```

Get the configured AI model string.

Returns the full model string (e.g. `"openai:gpt-4o"`) if configured.

## Examples

    Lotus.AI.model()
    # => {:ok, "openai:gpt-4o"}

# `suggest_optimizations`

```elixir
@spec suggest_optimizations(keyword()) :: {:ok, map()} | {:error, term()}
```

Get AI-powered optimization suggestions for a statement.

Runs the adapter's `prepare_for_analysis/2` + `query_plan/3` to get an
execution plan (when the engine exposes one), then asks the AI to
review the statement and the plan for potential improvements. Adapters
that can't produce a plan still get structural suggestions.

## Options

  * `:statement` (required) — a `%Lotus.Query.Statement{}` to review.
  * `:data_source` (required) — name of the data source.
  * `:search_path` (optional) — Postgres search path.

## Returns

  * `{:ok, result}` — map with `:suggestions`, `:model`, `:usage`.
  * `{:error, :ai_not_supported_for_source}` — the adapter opted out
    of AI via its `ai_context/1` callback.
  * `{:error, term}` — other failure.

# `supports?`

```elixir
@spec supports?(String.t(), feature()) :: boolean()
```

Check whether a specific AI feature is supported for the given source.

Reads `ai_context.capabilities[feature]` via
`Lotus.Source.Adapter.ai_context/1`. Returns `false` if the adapter
opts out of AI entirely (returns `{:error, _}` from `ai_context/1`).

`feature` is one of `:generation`, `:optimization`, `:explanation`.

UIs should call this before rendering each AI button to hide or
disable options the data source doesn't support.

# `unsupported_reason`

```elixir
@spec unsupported_reason(String.t(), feature()) :: String.t() | nil
```

Return the adapter-declared (or generic fallback) reason a feature is
unsupported for this source, or `nil` when supported.

Reasons from untrusted adapters are replaced with a generic fallback
at the dispatch layer — UIs can surface this string verbatim.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
