Imp.Adapter.Chat (Imp v0.12.1)

Copy Markdown View Source

Plain chat adapter: instructions plus field-labelled user content.

format/3 renders a signature and its inputs as a system message, one user/assistant pair per demo, the conversation history, and a final user message. Fields are labelled with [[ ## name ## ]] markers.

parse/3 accepts an Imp.Prediction, a map of output fields, or completion text. Text is split on those markers and the first section for each output field wins. An output with no section takes its declared default, or nil when optional; a required output with no section is a parse error, and so is a completion with no section for any output, even when every output is optional or defaulted, so the JSON fallback or a retry runs.

One signature-declared exception: signature.metadata[:text_field] names an output field that takes a completion carrying no marker at all. A native tool loop asks for a thought and tool calls, and a model that answers a step in plain text with no tool call has said something and called nothing — the plain reading of that completion, not a format failure worth a second LM call through Imp.Adapter.JSON. Remaining outputs take their declared defaults, so the signature says what an unanswered field means. The exception is narrow on purpose: the completion must carry no [[ ## field ## ]] line anywhere, no opening tag for an output, and no JSON object with an output's key, and it must not be blank. A completion in one of those formats tried to answer in fields, so it is parsed by markers, and one with no output section is a parse error that the JSON fallback reads. A signature without that metadata parses by markers alone. A blank completion is a step that said nothing. The text a model writes beside native tool calls is read the same way. Imp.Predict.ReActV2 sets it on its internal step signature; see that module.

Options to format/3: :demos, :response_instruction, :guidance, :agent_line (the guidance's "You are an Agent..." line: a string replaces it, nil leaves it out, and when absent it is DSPy's), :omit_empty_request, and the renderer seams :output_renderer, :input_section_renderer, :system_renderer, :tool_result_renderer and :history_note_renderer, which let another adapter reuse this message assembly with its own dialect and let a host bound what a tool result costs in the prompt without changing what the loop records. :history_note_renderer is the one seam for saying something about a stored turn rather than re-rendering it: it is consulted for every history turn, native tool turns included, after that turn's own messages, and its text becomes one user message right after them — the next thing the model reads. A note is data about the turn (the answer was not delivered, the account's allowance ran out), not a rewrite of what happened, so the record the loop keeps is unchanged. Options outside that list are ignored; anything that is not a keyword list raises ArgumentError.

Extending the default rendering

A renderer runs for whichever adapter formats the request, which is not always the one a program names: a Chat reply that cannot be parsed is asked again through Imp.Adapter.JSON with the same renderers. So the options a renderer receives carry that adapter's own default rendering, and a renderer that adds to the default calls it rather than naming an adapter:

system_renderer = fn signature, opts ->
  "Answer tersely.

" <> opts[:default_system].(signature, opts)

end

opts[:default_system] takes (signature, opts). An :output_renderer of arity 4 receives the options as its fourth argument, with opts[:default_outputs] taking (signature, outputs, missing_message). The JSON and XML adapters pass the same two, for their formats. A renderer that writes a format of its own writes it whatever the adapter.

Summary

Functions

Formats a model-facing tool result, preserving explicit typed image content.

Renders a tool result as the text a reader should see, whether that reader is the model or a person looking at a host's tool card.

The words for a failed tool call, without the Error: that format_tool_result/1 puts before them: the text an MCP error result carries, or one sentence for any other reason.

Functions

format_tool_content(value)

@spec format_tool_content(term()) ::
  String.t() | [String.t() | Imp.Adapter.Types.Image.t()]

Formats a model-facing tool result, preserving explicit typed image content.

Lists containing only strings and Imp.Adapter.Types.Image values, with at least one image, retain their ordered content. All other values use format_tool_result/1, whose textual contract is unchanged. Custom :tool_result_renderer functions may bound the strings while retaining the images. Chat associates those images with the originating tool call and places them in user messages after all tool responses in the turn.

format_tool_result(value)

@spec format_tool_result(term()) :: String.t()

Renders a tool result as the text a reader should see, whether that reader is the model or a person looking at a host's tool card.

Successful results format like any other value. A failed result renders as plain text instead of an Elixir term. An MCP error result is the text its tool wrote, after Error: unless that text already begins with "error", since the model is given no other sign that the call failed. Every other failure is one sentence after Error:: a JSON-RPC error is the server's message; a call that got no answer says why in words, and unless it was never sent, that it may have been carried out; a denied call says who declined it; a crashed tool names itself and its message, and one that exited says it may have been carried out; an unknown tool, a rejected argument or a rejected submit says what is wrong with the call; an atom reason is spelled out; and a structured :reason map reads as its reason and limit. Only the rendering is plain: the result the loop records keeps the whole structured error. Hosts that relay Imp tool results over a protocol boundary should use this so the same words reach the person that reached the model.

iex> Imp.Adapter.Chat.format_tool_result({:error, {:tool_denied, :post, :client_denied}})
"Error: post was not allowed; the person declined it."

tool_error_text(reason)

@spec tool_error_text(term()) :: String.t()

The words for a failed tool call, without the Error: that format_tool_result/1 puts before them: the text an MCP error result carries, or one sentence for any other reason.

iex> Imp.Adapter.Chat.tool_error_text({:unknown_tool, "frobnicate"})
"there is no tool named frobnicate."