PhoenixKitAgent.Prompt (PhoenixKitAgent v0.1.0)

Copy Markdown View Source

The system prompt of a turn (spec §7.1), put together in the order of priority, each part separated by a blank line:

  1. the module's prompt (agent_system_prompt, PhoenixKitAgent.Config), then the module's own rule for writes, always there: a tool that changes data only proposes the change, which waits for the user's confirmation in the chat, and the model never says it is done before a confirmation message says so, nor asks again for a change still waiting (PhoenixKitAgent.Confirmations); then, when the AI guard has the agent.chat task flag off-purpose requests (PhoenixKitAI.Guard.self_flag?/1), the guard's instruction (PhoenixKitAI.Guard.self_flag_instruction/1) and how to flag: the marker ⟦off_purpose: <reason>⟧ at the very end of the answer, which the turn cuts off (PhoenixKitAgent.Turns.Runner). It stands above everything the admin and the user write, so their instructions cannot undo it, and above the date, so the prompt's cached prefix holds;
  2. the host's addendum — config :phoenix_kit_agent, prompt_addendum: {Mod, :fun}: what an order is, the statuses, the language of the answer. Mod.fun(ctx) is called with %{user_uuid:, locale:} when it is exported with one argument, Mod.fun() otherwise; the result is a string or nil. An exception, an exit, a result that is not a string or a function that does not exist skips the addendum with a warning that names the reason only — never the content;
  3. the admin's prompt for this user (UserSettings.system_prompt);
  4. the user's personal instructions (personal_instructions), under a line that says they rank below the parts above and never override them;
  5. the context of the session: the user's name (never the e-mail), the role the session acts as, the locale, the date in the site time zone (not the time of day: the system message opens every request, and a prefix that changes by the minute defeats the providers' prompt caching), the page's path when the host passed it to PhoenixKitAgent.Turns.start/6, and what the host says of that page (below).

The page and the locale come from a browser, so they are not trusted: a value with a control or line-separator character, or one that is not shaped like a path / a locale, is left out whole; a long one is cut. The page is reduced to its path (no query, no fragment: those may hold what the user typed into a search).

The host's page context

config :phoenix_kit_agent, page_context: {Mod, :fun} lets the host say what the page is — the number of the order open there, say (spec §7.1 point 5). With a page path, Mod.fun(path, scope) is called: path is the page's path as above, scope the scope of the user's browser session (PhoenixKit.Users.Auth.SessionRef.scope_for_session/2: the role the session acts as), not one built from the user alone. The host decides what to tell, and tells only what that scope may see. The result is a string — put on one line (control and line-separator characters become spaces), trimmed and cut to 200 characters — or nil. An exception, an exit, a result that is not a string (or not valid UTF-8) or a function that does not exist skips it with a warning that names the reason only — never the path or the content. No hook, or no page: the context has the path only.

The hook is called as the turn starts, in the process that starts it (the chat panel's), while the turn holds its locks, and with no timeout of its own: keep it fast — one indexed lookup, not a report. Until it returns, the turn has not started and the chat and the user stay busy.

Summary

Types

The page the user is on, as the browser says it is: a path.

Functions

The system prompt for ctx (see the moduledoc); never empty.

Types

ctx()

@type ctx() :: %{
  :user_uuid => String.t(),
  :scope => PhoenixKit.Users.Auth.Scope.t(),
  optional(:settings) => PhoenixKitAgent.UserConfig.effective(),
  optional(:locale) => String.t() | nil,
  optional(:page) => page() | nil,
  optional(:now) => DateTime.t()
}

page()

@type page() :: String.t()

The page the user is on, as the browser says it is: a path.

Functions

build(ctx)

@spec build(ctx()) :: String.t()

The system prompt for ctx (see the moduledoc); never empty.