PhoenixKit.Modules.AI.Translation (phoenix_kit v1.7.118)

Copy Markdown View Source

Generic AI translation helper. Translates a %{field_name => text} map from source_lang to target_lang using the optional PhoenixKitAI plugin, and parses the structured response back into the same shape.

Designed to be the single orchestration layer shared by every feature module that wants AI translation — phoenix_kit_publishing, phoenix_kit_projects, future consumers. Each module wraps this helper in its own Oban worker (or controller action) and owns the per-module storage write, broadcasts, caching, and per-resource activity log entry.

What lives here

  • Prompt rendering with {{SourceLanguage}} / {{TargetLanguage}} / arbitrary field-name variables.
  • The PhoenixKitAI.ask_with_prompt/4 call, guarded so absence of the plugin returns {:error, :ai_not_installed} instead of raising.
  • A structured-response parser for the ---FIELD_NAME--- shape that publishing's prompt template ships with. Generalised — any list of field names is accepted, ordering follows the input.
  • Error normalisation: every failure path returns {:error, atom_or_tuple} so callers can pattern-match without knowing whether the failure came from the plugin, the parser, or a network blip.
  • A single core activity-log entry (core.ai_translation.requested) on every dispatched request — gives Max a unified audit trail of AI token spend regardless of which feature module triggered it. Modules still log their own per-resource action (e.g. publishing.translation.added, projects.translation.added).

What does NOT live here

  • Storage of the translation result — each consumer owns its own write path (publishing's publishing_content rows, projects' Project.translations JSONB, etc.).
  • Broadcasts — each consumer has its own PubSub topic shape.
  • Per-language status (in-flight, completed, errored) — that's the consumer's worker and the host UI.
  • Retry policy / queue choice — each consumer's Oban worker picks its own queue, max-attempts, and uniqueness constraints. This module is a synchronous function-call shape; consumers wrap it.

Usage

Translation.translate_fields(
  endpoint_uuid,
  prompt_uuid,
  "en",
  "es",
  %{"title" => "Hello", "body" => "World"}
)
# => {:ok, %{"title" => "Hola", "body" => "Mundo"}}
# |  {:error, :ai_not_installed}
# |  {:error, :no_endpoint}
# |  {:error, :missing_prompt}
# |  {:error, {:ai_error, reason}}
# |  {:error, {:parse_error, reason}}

Summary

Functions

Parses a structured ---FIELD_NAME--- response into a field map.

Types

field_map()

@type field_map() :: %{required(String.t()) => String.t()}

translation_result()

@type translation_result() ::
  {:ok, field_map()}
  | {:error,
     :ai_not_installed
     | :no_endpoint
     | :missing_prompt
     | {:ai_error, term()}
     | {:parse_error, term()}}

Functions

parse_response(response, field_names)

@spec parse_response(String.t(), [String.t()]) ::
  {:ok, field_map()} | {:error, {:parse_error, term()}}

Parses a structured ---FIELD_NAME--- response into a field map.

Public for testing — consumers normally hit translate_fields/6, which calls this internally. Useful when a caller already has the raw AI response (e.g. a previously cached completion) and just needs to extract field values.

Field names are matched case-insensitively against the markers but the returned map preserves the input casing of fields so callers can round-trip with their original field-name strings.

All requested fields must be present in the response. When the model returns a partial response (e.g. it forgot the ---SLUG--- marker), this function returns {:error, {:parse_error, {:missing_fields, [...]}}} so the caller can decide whether to retry, fall back to the source value, or surface an error to the user — rather than silently persisting a half-translated row.

iex> Translation.parse_response(
...>   "---TITLE---\nHola\n---BODY---\nMundo",
...>   ["title", "body"]
...> )
{:ok, %{"title" => "Hola", "body" => "Mundo"}}

iex> Translation.parse_response("---TITLE---\nonly", ["title", "body"])
{:error, {:parse_error, {:missing_fields, ["body"]}}}

iex> Translation.parse_response(":shrug:", ["title"])
{:error, {:parse_error, :no_markers}}

translate_fields(endpoint_uuid, prompt_uuid, source_lang, target_lang, fields, opts \\ [])

@spec translate_fields(
  String.t(),
  String.t(),
  String.t(),
  String.t(),
  field_map(),
  keyword()
) :: translation_result()

Translate fields from source_lang to target_lang.

  • endpoint_uuid — UUID of a configured PhoenixKitAI endpoint. Required even when the plugin is installed; the plugin's prompt machinery binds to a specific endpoint at call time.
  • prompt_uuid — UUID of a PhoenixKitAI.Prompt whose template references {{SourceLanguage}}, {{TargetLanguage}}, and one {{<FieldName>}} placeholder per key in fields.
  • source_lang / target_lang — language codes (base or dialect; the helper passes them through unchanged).
  • fields%{field_name => text} map. Field names become the structured-response markers (---<FIELD_NAME>---, uppercased).

Options

  • :actor_uuid — included in the core.ai_translation.requested activity log entry. When omitted the log is still written with actor_uuid: nil.
  • :resource_type / :resource_uuid — let the audit log point at the row being translated.
  • :source — string identifier for the calling module (e.g. "Publishing.TranslatePostWorker"); included in the PhoenixKitAI request log so usage reports break down by caller.