PhoenixKitAI (PhoenixKitAI v0.3.0)

Copy Markdown View Source

Main context for PhoenixKit AI system.

Provides AI endpoint management and usage tracking for AI API requests.

Architecture

Each Endpoint is a unified configuration that combines:

  • Provider credentials (api_key, base_url, provider_settings)
  • Model selection (single model per endpoint)
  • Generation parameters (temperature, max_tokens, etc.)

Users create as many endpoints as needed, each representing one complete AI configuration ready for making API requests.

Core Functions

System Management

Endpoint CRUD

Completion API

Usage Tracking

Usage Examples

# Enable the module
PhoenixKitAI.enable_system()

# Create an endpoint
{:ok, endpoint} = PhoenixKitAI.create_endpoint(%{
  name: "Claude Fast",
  provider: "openrouter",
  api_key: "sk-or-v1-...",
  model: "anthropic/claude-3-haiku",
  temperature: 0.7
})

# Use the endpoint
{:ok, response} = PhoenixKitAI.ask(endpoint.uuid, "Hello!")

# Extract the response text
{:ok, text} = PhoenixKitAI.extract_content(response)

Configuration

# Persist message + response content in request metadata (default: true).
# Disable for deployments with PII / data-retention obligations — token
# counts, latency, model, and cost are still recorded.
config :phoenix_kit_ai, capture_request_content: false

# Capture process memory in request caller_context (default: false).
config :phoenix_kit_ai, capture_request_memory: true

# Allow endpoint base_url to point at private/loopback IPs (default: false).
# Required for self-hosted Ollama / intranet inference.
config :phoenix_kit_ai, allow_internal_endpoint_urls: true

Summary

Functions

Simple helper for single-turn chat completion.

Makes an AI completion using a prompt template.

Returns an endpoint changeset for use in forms.

Returns a prompt changeset for use in forms.

Makes a chat completion request using a configured endpoint.

Makes an AI completion with a prompt template as the system message.

Counts the number of enabled endpoints.

Counts the number of enabled prompts.

Counts the total number of endpoints.

Counts the total number of prompts.

Counts the total number of requests.

Creates a new AI endpoint.

Creates a new AI prompt.

Creates a new AI request record.

Deletes an AI endpoint.

Deletes an AI prompt.

Disables a prompt.

Disables the AI module.

Duplicates a prompt with a new name.

Makes an embeddings request using a configured endpoint.

Enables a prompt.

Enables the AI module.

Checks if the AI module is enabled.

Returns the PubSub topic for AI endpoints. Subscribe to this topic to receive real-time updates.

Extracts the text content from a completion response.

Extracts usage information from a response.

Gets the AI module configuration with statistics.

Gets dashboard statistics for display.

Gets a single endpoint by UUID.

Gets a single endpoint by UUID.

Returns usage statistics for each endpoint.

Gets a single prompt by UUID.

Gets a single prompt by UUID.

Gets a prompt by slug.

Gets usage statistics for all prompts.

Gets the variables defined in a prompt.

Finds all prompts that use a specific variable.

Gets a single request by UUID.

Gets a single request by UUID.

Returns filter options for requests (distinct endpoints, models, and sources).

Gets request counts grouped by day.

Gets token usage grouped by model.

Gets aggregated usage statistics.

Increments the usage count for a prompt and updates last_used_at.

Lists only enabled prompts.

Lists all AI endpoints.

Lists all AI prompts.

Lists AI requests with pagination and filters.

Marks an endpoint as validated by updating its last_validated_at timestamp.

Combined boot-time migration entry point.

Previews a rendered prompt without making an AI call.

Returns the PubSub topic for AI prompts.

Increments the usage count for a prompt and updates last_used_at.

Renders a prompt by replacing variables with provided values.

Reorders endpoints based on a list of UUIDs in their new display order.

Reorders prompts based on a list of UUIDs in their new display order.

Returns the PubSub topic for AI requests/usage.

Resets the usage statistics for a prompt.

Resolves an endpoint from an ID (UUID string) or Endpoint struct.

Resolves a prompt from various input types.

One-shot auto-migrator for legacy endpoint.api_key values into PhoenixKit.Integrations connections.

Searches prompts by name, description, or content.

Subscribes the current process to AI endpoint changes.

Subscribes the current process to AI prompt changes.

Subscribes the current process to AI request/usage changes.

Sums the total tokens used across all requests.

Updates an existing AI endpoint.

Updates an existing AI prompt.

Validates that a prompt is ready for use.

Validates that the content has valid variable syntax.

Validates that all required variables are provided for a prompt.

Functions

ask(endpoint_uuid, prompt, opts \\ [])

@spec ask(String.t() | PhoenixKitAI.Endpoint.t(), String.t(), keyword()) ::
  {:ok, map()} | {:error, term()}

Simple helper for single-turn chat completion.

Parameters

  • endpoint_uuid - Endpoint UUID string or Endpoint struct
  • prompt - User prompt string
  • opts - Optional parameter overrides and system message

Options

All options from complete/3 plus:

  • :system - System message string
  • :source - Override auto-detected source for request tracking

Examples

# Simple question
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "What is the capital of France?")

# With system message
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Translate: Hello",
  system: "You are a translator. Translate to French."
)

# With custom source for tracking
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Hello!",
  source: "Languages"
)

# Extract just the text content
{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Hello!")
{:ok, text} = PhoenixKitAI.extract_content(response)

Returns

Same as complete/3

ask_with_prompt(endpoint_uuid, prompt_uuid, variables \\ %{}, opts \\ [])

Makes an AI completion using a prompt template.

The prompt content is rendered with the provided variables and sent as the user message.

change_endpoint(endpoint, attrs \\ %{})

@spec change_endpoint(PhoenixKitAI.Endpoint.t(), map()) :: Ecto.Changeset.t()

Returns an endpoint changeset for use in forms.

change_prompt(prompt, attrs \\ %{})

@spec change_prompt(PhoenixKitAI.Prompt.t(), map()) :: Ecto.Changeset.t()

Returns a prompt changeset for use in forms.

complete(endpoint_uuid, messages, opts \\ [])

@spec complete(String.t() | PhoenixKitAI.Endpoint.t(), [map()], keyword()) ::
  {:ok, map()} | {:error, term()}

Makes a chat completion request using a configured endpoint.

Parameters

  • endpoint_uuid - Endpoint UUID string or Endpoint struct
  • messages - List of message maps with :role and :content
  • opts - Optional parameter overrides

Options

All standard completion parameters plus:

  • :source - Override auto-detected source for request tracking

Examples

{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, [
  %{role: "user", content: "Hello!"}
])

# With system message
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, [
  %{role: "system", content: "You are a helpful assistant."},
  %{role: "user", content: "What is 2+2?"}
])

# With parameter overrides
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, messages,
  temperature: 0.5,
  max_tokens: 500
)

# With custom source for tracking
{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, messages,
  source: "MyModule"
)

Returns

  • {:ok, response} - Full API response including usage stats
  • {:error, reason} - Error atom or tagged tuple. See PhoenixKitAI.Errors for the vocabulary and translation.

complete_with_system_prompt(endpoint_uuid, prompt_uuid, variables, user_message, opts \\ [])

Makes an AI completion with a prompt template as the system message.

The prompt is rendered and used as the system message, with the user_message as the user message.

count_enabled_endpoints()

@spec count_enabled_endpoints() :: non_neg_integer()

Counts the number of enabled endpoints.

count_enabled_prompts()

@spec count_enabled_prompts() :: non_neg_integer()

Counts the number of enabled prompts.

count_endpoints()

@spec count_endpoints() :: non_neg_integer()

Counts the total number of endpoints.

count_prompts()

@spec count_prompts() :: non_neg_integer()

Counts the total number of prompts.

count_requests()

@spec count_requests() :: non_neg_integer()

Counts the total number of requests.

create_endpoint(attrs, opts \\ [])

@spec create_endpoint(
  map(),
  keyword()
) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}

Creates a new AI endpoint.

Examples

{:ok, endpoint} = PhoenixKitAI.create_endpoint(%{
  name: "Claude Fast",
  provider: "openrouter",
  api_key: "sk-or-v1-...",
  model: "anthropic/claude-3-haiku",
  temperature: 0.7
})

create_prompt(attrs, opts \\ [])

@spec create_prompt(
  map(),
  keyword()
) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}

Creates a new AI prompt.

Examples

{:ok, prompt} = PhoenixKitAI.create_prompt(%{
  name: "Translator",
  content: "Translate the following text to {{Language}}:\n\n{{Text}}"
})

create_request(attrs)

Creates a new AI request record.

Used to log every AI API call for tracking and statistics.

delete_endpoint(endpoint, opts \\ [])

@spec delete_endpoint(
  PhoenixKitAI.Endpoint.t(),
  keyword()
) :: {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}

Deletes an AI endpoint.

delete_prompt(prompt, opts \\ [])

@spec delete_prompt(
  PhoenixKitAI.Prompt.t(),
  keyword()
) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}

Deletes an AI prompt.

disable_prompt(prompt_uuid)

@spec disable_prompt(String.t()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, term()}

Disables a prompt.

disable_system()

@spec disable_system() :: {:ok, term()} | {:error, term()}

Disables the AI module.

duplicate_prompt(prompt_uuid, new_name)

@spec duplicate_prompt(String.t(), String.t()) ::
  {:ok, PhoenixKitAI.Prompt.t()} | {:error, term()}

Duplicates a prompt with a new name.

embed(endpoint_uuid, input, opts \\ [])

@spec embed(
  String.t() | PhoenixKitAI.Endpoint.t(),
  String.t() | [String.t()],
  keyword()
) ::
  {:ok, map()} | {:error, term()}

Makes an embeddings request using a configured endpoint.

Parameters

  • endpoint_uuid - Endpoint UUID string or Endpoint struct
  • input - Text or list of texts to embed
  • opts - Optional parameter overrides

Options

  • :dimensions - Override embedding dimensions
  • :source - Override auto-detected source for request tracking

Examples

# Single text
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, "Hello, world!")

# Multiple texts
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, ["Hello", "World"])

# With dimension override
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, "Hello", dimensions: 512)

# With custom source for tracking
{:ok, response} = PhoenixKitAI.embed(endpoint_uuid, "Hello",
  source: "SemanticSearch"
)

Returns

  • {:ok, response} - Response with embeddings
  • {:error, reason} - Error atom or tagged tuple.

enable_prompt(prompt_uuid)

@spec enable_prompt(String.t()) :: {:ok, PhoenixKitAI.Prompt.t()} | {:error, term()}

Enables a prompt.

enable_system()

@spec enable_system() :: {:ok, term()} | {:error, term()}

Enables the AI module.

enabled?()

@spec enabled?() :: boolean()

Checks if the AI module is enabled.

endpoints_topic()

@spec endpoints_topic() :: String.t()

Returns the PubSub topic for AI endpoints. Subscribe to this topic to receive real-time updates.

extract_content(response)

Extracts the text content from a completion response.

Examples

{:ok, response} = PhoenixKitAI.ask(endpoint_uuid, "Hello!")
{:ok, text} = PhoenixKitAI.extract_content(response)
# => "Hello! How can I help you today?"

extract_usage(response)

Extracts usage information from a response.

Examples

{:ok, response} = PhoenixKitAI.complete(endpoint_uuid, messages)
usage = PhoenixKitAI.extract_usage(response)
# => %{prompt_tokens: 10, completion_tokens: 15, total_tokens: 25}

get_config()

@spec get_config() :: %{
  enabled: boolean(),
  endpoints_count: non_neg_integer(),
  total_requests: non_neg_integer(),
  total_tokens: non_neg_integer()
}

Gets the AI module configuration with statistics.

Stat queries are wrapped in a try/rescue so that environments without a live Repo connection (early boot, test cases without sandbox checkout) still get a well-formed map — matching the defensive pattern in enabled?/0.

get_dashboard_stats()

Gets dashboard statistics for display.

Returns stats for the last 30 days plus all-time totals.

get_endpoint(id)

@spec get_endpoint(term()) :: PhoenixKitAI.Endpoint.t() | nil

Gets a single endpoint by UUID.

Accepts a UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000").

Returns nil if the endpoint does not exist.

get_endpoint!(id)

@spec get_endpoint!(String.t()) :: PhoenixKitAI.Endpoint.t()

Gets a single endpoint by UUID.

Raises Ecto.NoResultsError if the endpoint does not exist.

get_endpoint_usage_stats()

Returns usage statistics for each endpoint.

Returns a map of endpoint_uuid => %{request_count, total_tokens, total_cost, last_used_at}

get_prompt(id)

@spec get_prompt(term()) :: PhoenixKitAI.Prompt.t() | nil

Gets a single prompt by UUID.

Accepts a UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000").

Returns nil if the prompt does not exist.

get_prompt!(id)

@spec get_prompt!(String.t()) :: PhoenixKitAI.Prompt.t()

Gets a single prompt by UUID.

Raises Ecto.NoResultsError if the prompt does not exist.

get_prompt_by_slug(slug)

@spec get_prompt_by_slug(String.t()) :: PhoenixKitAI.Prompt.t() | nil

Gets a prompt by slug.

Returns nil if the prompt does not exist.

get_prompt_usage_stats(opts \\ [])

Gets usage statistics for all prompts.

get_prompt_variables(prompt_uuid)

Gets the variables defined in a prompt.

get_prompts_with_variable(variable_name)

Finds all prompts that use a specific variable.

get_request(id)

Gets a single request by UUID.

Accepts a UUID string (e.g., "550e8400-e29b-41d4-a716-446655440000").

Returns nil if the request does not exist.

get_request!(id)

Gets a single request by UUID.

get_request_filter_options()

Returns filter options for requests (distinct endpoints, models, and sources).

get_requests_by_day(opts \\ [])

Gets request counts grouped by day.

get_tokens_by_model(opts \\ [])

Gets token usage grouped by model.

get_usage_stats(opts \\ [])

Gets aggregated usage statistics.

Options

  • :since - Start date for statistics
  • :until - End date for statistics
  • :endpoint_uuid - Filter by endpoint

Returns

Map with statistics including total_requests, total_tokens, success_rate, etc.

increment_prompt_usage(prompt_uuid)

Increments the usage count for a prompt and updates last_used_at.

list_enabled_prompts()

@spec list_enabled_prompts() ::
  [PhoenixKitAI.Prompt.t()] | {[PhoenixKitAI.Prompt.t()], non_neg_integer()}

Lists only enabled prompts.

Convenience wrapper for list_prompts(enabled: true).

Examples

PhoenixKitAI.list_enabled_prompts()

list_endpoints(opts \\ [])

@spec list_endpoints(keyword()) :: {[PhoenixKitAI.Endpoint.t()], non_neg_integer()}

Lists all AI endpoints.

Options

  • :provider - Filter by provider type
  • :enabled - Filter by enabled status
  • :preload - Associations to preload

Examples

PhoenixKitAI.list_endpoints()
PhoenixKitAI.list_endpoints(provider: "openrouter", enabled: true)

list_prompts(opts \\ [])

@spec list_prompts(keyword()) ::
  [PhoenixKitAI.Prompt.t()] | {[PhoenixKitAI.Prompt.t()], non_neg_integer()}

Lists all AI prompts.

Options

  • :sort_by - Field to sort by (default: :sort_order)
  • :sort_dir - Sort direction, :asc or :desc (default: :asc)
  • :enabled - Filter by enabled status

Examples

PhoenixKitAI.list_prompts()
PhoenixKitAI.list_prompts(sort_by: :name, sort_dir: :asc)
PhoenixKitAI.list_prompts(enabled: true)

list_requests(opts \\ [])

Lists AI requests with pagination and filters.

Options

  • :page - Page number (default: 1)
  • :page_size - Results per page (default: 20)
  • :endpoint_uuid - Filter by endpoint
  • :user_uuid - Filter by user
  • :status - Filter by status
  • :model - Filter by model
  • :source - Filter by source (from metadata)
  • :since - Filter by date (requests after this date)
  • :preload - Associations to preload

Returns

{requests, total_count}

mark_endpoint_validated(endpoint)

@spec mark_endpoint_validated(PhoenixKitAI.Endpoint.t()) ::
  {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}

Marks an endpoint as validated by updating its last_validated_at timestamp.

migrate_legacy()

@spec migrate_legacy() :: {:ok, map()} | {:error, term()}

Combined boot-time migration entry point.

Runs both legacy data transitions for AI:

  1. Local api_key → Integrations row + integration_uuid (delegates to run_legacy_api_key_migration/0). Endpoints with bare provider == "openrouter" and a non-empty api_key get grouped, get an Integration row created, and have both provider AND integration_uuid updated to point at it.

  2. provider-string → integration_uuid (sweep). Endpoints with integration_uuid IS NULL whose provider field resolves to a real integration uuid (either bare provider, provider:name shape, or a uuid stuffed in the string column from pre-V107 form saves) get their integration_uuid populated. V107's migration backfilled most of these at install time; this pass catches stragglers (e.g., endpoints created post-form-update but pre-V107).

Both kinds log to PhoenixKit.Activity per migrated record / group with mode: "auto" and module "ai". PII-safe: never logs api_key values.

Idempotent — run on every host-app boot. Designed to be invoked via the orchestrator (PhoenixKit.ModuleRegistry.run_all_legacy_migrations/0), but can be called directly for ad-hoc migration runs.

preview_prompt(prompt_uuid, variables \\ %{})

Previews a rendered prompt without making an AI call.

prompts_topic()

@spec prompts_topic() :: String.t()

Returns the PubSub topic for AI prompts.

record_prompt_usage(prompt)

@spec record_prompt_usage(PhoenixKitAI.Prompt.t()) ::
  {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}

Increments the usage count for a prompt and updates last_used_at.

render_prompt(prompt_uuid, variables \\ %{})

Renders a prompt by replacing variables with provided values.

Returns {:ok, rendered_text} or {:error, reason}.

reorder_endpoints(ordered_ids, opts \\ [])

@spec reorder_endpoints(
  [String.t()],
  keyword()
) :: :ok | {:error, :too_many_uuids}

Reorders endpoints based on a list of UUIDs in their new display order.

Delegates to PhoenixKit.Utils.Reorder.reorder/4 for the shared two-phase index-rewrite primitive. Returns :ok on success or {:error, :too_many_uuids} when the payload exceeds the cap.

On success, logs one endpoint.reordered activity row with the actual updated count + first uuid so the audit feed records the drag-to-reorder action without per-row noise. opts is forwarded to log_activity/5 so callers can thread actor_uuid / mode.

reorder_prompts(ordered_ids, opts \\ [])

@spec reorder_prompts(
  [String.t()],
  keyword()
) :: :ok | {:error, :too_many_uuids}

Reorders prompts based on a list of UUIDs in their new display order.

Delegates to PhoenixKit.Utils.Reorder.reorder/4 — the same shared two-phase index-rewrite primitive reorder_endpoints/2 uses, so the two reorder paths share the malformed-uuid filtering, dedup, and payload-cap guard. Returns :ok on success or {:error, :too_many_uuids} when the payload exceeds the cap.

On success, logs one prompt.reordered activity row with the actual updated count + first uuid so the audit feed records the drag-to-reorder action without per-row noise. opts is forwarded to log_activity/5 so callers can thread actor_uuid / actor_role.

requests_topic()

@spec requests_topic() :: String.t()

Returns the PubSub topic for AI requests/usage.

reset_prompt_usage(prompt_uuid)

Resets the usage statistics for a prompt.

resolve_endpoint(id)

@spec resolve_endpoint(term()) ::
  {:ok, PhoenixKitAI.Endpoint.t()}
  | {:error, :endpoint_not_found | :invalid_endpoint_identifier}

Resolves an endpoint from an ID (UUID string) or Endpoint struct.

Examples

{:ok, endpoint} = PhoenixKitAI.resolve_endpoint("019abc12-3456-7def-8901-234567890abc")
{:ok, endpoint} = PhoenixKitAI.resolve_endpoint(endpoint)

resolve_prompt(prompt)

Resolves a prompt from various input types.

Accepts:

  • UUID string (e.g., "019abc12-3456-7def-8901-234567890abc")
  • String slug (e.g., "my-prompt")
  • Prompt struct (returned as-is)

Returns {:ok, prompt} or {:error, reason}.

run_legacy_api_key_migration()

@spec run_legacy_api_key_migration() :: :ok

One-shot auto-migrator for legacy endpoint.api_key values into PhoenixKit.Integrations connections.

Mirrors the pattern of PhoenixKit.Integrations.run_legacy_migrations/0 — call it at host-app boot to fold pre-Integrations endpoint api_keys into the named-connection model. Safe to call multiple times: multiple idempotency guards short-circuit on already-migrated state.

What it does

For each phoenix_kit_ai_endpoints row whose provider is the bare string "openrouter" (i.e., NOT already pointing at a named Integrations connection like "openrouter:my-key") AND whose api_key is non-empty:

  1. Group by api_key value — endpoints sharing a key share one connection (dedup).
  2. Create a PhoenixKit.Integrations connection per distinct key. Naming: "openrouter:default" if there's exactly one key in the deployment; "openrouter:imported-1", "openrouter:imported-2" (1-indexed by first-seen order) if there are multiple.
  3. Update each endpoint's provider field to point at the new connection key (e.g., "openrouter:default").

The legacy api_key column is NEVER cleared — it stays on each row as a safety net. OpenRouterClient.resolve_api_key/2 prefers Integrations, so post-migration endpoints stop firing the legacy warning; if Integrations later breaks for any reason, the column still has the value and the fallback path keeps working.

Idempotency guards (any one short-circuits)

  • The ai_legacy_api_key_migration_completed_at setting is set → already ran, skip.
  • ANY integration:openrouter:* key already exists in phoenix_kit_settings (operator already set up Integrations manually) → mark completed and skip.
  • NO endpoints have provider == "openrouter" with a non-empty api_key → nothing to migrate, mark completed.

Failure modes

Top-level try/rescue/catch :exit so DB outages, race conditions, or any unexpected exception NEVER crashes the host app's boot. Per-key-group operations are isolated — one bad group doesn't abort the others. Partial migration is safe because un-migrated endpoints still resolve via the legacy fallback path.

Configuration

No options. Disable by simply not calling the function.

search_prompts(query, opts \\ [])

Searches prompts by name, description, or content.

subscribe_endpoints()

@spec subscribe_endpoints() :: :ok | {:error, term()}

Subscribes the current process to AI endpoint changes.

subscribe_prompts()

@spec subscribe_prompts() :: :ok | {:error, term()}

Subscribes the current process to AI prompt changes.

subscribe_requests()

@spec subscribe_requests() :: :ok | {:error, term()}

Subscribes the current process to AI request/usage changes.

sum_tokens()

@spec sum_tokens() :: non_neg_integer()

Sums the total tokens used across all requests.

update_endpoint(endpoint, attrs, opts \\ [])

@spec update_endpoint(PhoenixKitAI.Endpoint.t(), map(), keyword()) ::
  {:ok, PhoenixKitAI.Endpoint.t()} | {:error, Ecto.Changeset.t()}

Updates an existing AI endpoint.

Accepts an :actor_uuid option so the mutation can be attributed in the activity feed. If the change toggles the enabled flag an additional endpoint.enabled / endpoint.disabled entry is logged.

update_prompt(prompt, attrs, opts \\ [])

@spec update_prompt(PhoenixKitAI.Prompt.t(), map(), keyword()) ::
  {:ok, PhoenixKitAI.Prompt.t()} | {:error, Ecto.Changeset.t()}

Updates an existing AI prompt.

Accepts an :actor_uuid option so the mutation can be attributed in the activity feed. If the change toggles the enabled flag an additional prompt.enabled / prompt.disabled entry is logged.

validate_prompt(prompt)

Validates that a prompt is ready for use.

Returns {:ok, prompt} if valid, or {:error, reason} if not.

validate_prompt_content(content)

Validates that the content has valid variable syntax.

validate_prompt_variables(prompt_uuid, variables)

Validates that all required variables are provided for a prompt.