defmodule Phantom.Prompt do @moduledoc """ The Model Context Protocol (MCP) provides a standardized way for servers to expose prompt templates to clients. Prompts allow servers to provide structured messages and instructions for interacting with language models. Clients can discover available prompts, retrieve their contents, and provide arguments to customize them. ```mermaid sequenceDiagram participant Client participant Server Note over Client,Server: Discovery Client->>Server: prompts/list Server-->>Client: List of prompts Note over Client,Server: Usage Client->>Server: prompts/get Server-->>Client: Prompt content opt listChanged Note over Client,Server: Changes Server--)Client: prompts/list_changed Client->>Server: prompts/list Server-->>Client: Updated prompts end ``` https://modelcontextprotocol.io/specification/2025-03-26/server/prompts """ import Phantom.Utils alias Phantom.Prompt.Argument @enforce_keys ~w[name handler function]a defstruct [ :name, :description, :handler, :completion_function, :function, meta: %{}, arguments: [] ] @type t :: %__MODULE__{ name: String.t(), handler: module(), function: atom(), completion_function: atom(), description: String.t(), meta: map(), arguments: [Argument.t()] } @type json :: %{ required(:name) => String.t(), optional(:description) => String.t(), optional(:arguments) => %{ String.t() => String.t() } } @type text_content :: %{ type: :text, data: String.t() } @type image_content :: %{ type: :image, data: base64_encoded :: String.t(), mimeType: String.t() } @type audio_content :: %{ type: :audio, data: base64_encoded :: String.t(), mimeType: String.t() } @type embedded_resource_content :: %{ type: :resource, resource: Phantom.Resource.response() } @type message :: %{ role: :assistant | :user, content: text_content() | image_content() | audio_content() | embedded_resource_content() } @type response :: %{ description: String.t(), messages: [message()] } @spec build(map() | Keyword.t()) :: t() @doc """ Build a prompt spec The `Phantom.Router.prompt/3` macro will build these specs. """ def build(attrs) do attrs = attrs |> Map.new() |> Map.update(:name, to_string(attrs[:function]), &to_string/1) struct!( __MODULE__, Map.put(attrs, :arguments, Enum.map(attrs[:arguments] || [], &Argument.build/1)) ) end @spec to_json(t()) :: json() @doc """ Represent a Prompt spec as json when listing the available prompts to clients. """ def to_json(%__MODULE__{} = prompt) do remove_nils(%{ name: prompt.name, description: prompt.description, arguments: Enum.map(prompt.arguments, &Argument.to_json/1) }) end @doc """ Formats the response from an MCP Router to the MCP specification Provide a keyword list of messages with a keyword list. The key should contain the role, and the value contain the message. For example: require Phantom.Prompt, as: Prompt {:ok, uri, resource} = MyApp.MCP.Router.read_resource(session, :my_resource, 123) Prompt.response([ assistant: Prompt.audio(File.read!("foo.wav"), "audio/wav"), user: Prompt.text("Wow that was interesting"), assistant: Prompt.image(File.read!("bar.png"), "image/png"), user: Prompt.text("amazing"), assistant: Prompt.embedded_resource(uri, resource) ]) """ defmacro response(%{messages: _} = response), do: response defmacro response(messages) when is_list(messages) do if not Macro.Env.has_var?(__CALLER__, {:session, nil}) do raise "session was not supplied to the response. Phantom requires the variable named `session` to exist, or use response/2." end quote do prompt = var!(session, nil).request.spec Phantom.Prompt.response(unquote(messages), prompt) end end def response(%{messages: _} = response, _prompt), do: response @doc """ Construct a prompt response with the provided messages for the given prompt See `response/1` macro version that do the same thing but will fetch the prompt spec from the current session. """ @spec response([message()], Phantom.Prompt.t()) :: response() def response(messages, prompt) when is_list(messages) do %{ description: prompt.description, messages: Enum.map(messages, fn {role, content} -> %{role: role, content: content} end) } end @spec text(String.t()) :: text_content() @doc """ Build a text message for the prompt """ def text(data), do: %{type: :text, text: data || ""} @spec audio(binary(), String.t()) :: audio_content() @doc """ Build an audio message for the prompt The provided binary will be base64-encoded. """ def audio(data, mime_type) do %{type: :audio, data: Base.encode64(data || <<>>), mimeType: mime_type} end @spec image(binary(), String.t()) :: image_content() @doc """ Build an image message for the prompt The provided binary will be base64-encoded. """ def image(binary, mime_type) do %{type: :image, data: Base.encode64(binary || <<>>), mimeType: mime_type} end @spec embedded_resource(string_uri :: String.t(), map()) :: embedded_resource_content() @doc """ Embedded resource reponse. """ def embedded_resource(uri, resource) do %{type: :resource, resource: Map.put(resource, :uri, uri)} end end