Prompts
Change a prompt without a deploy, and see which version wrote each answer.
Versions and labels
A prompt is a name and a list of versions. A version is a text template or a
list of chat messages, with {{variable}} placeholders and
the model settings you want stored beside it. A version never changes once
stored; a new body is a new version, numbered by the daemon. Posting a body
identical to the latest version stores nothing, so a deploy that posts its
prompts on every start is a no-op when nothing changed.
A label points at one version. Your code fetches by label, usually
production, so releasing a prompt is moving the label to the
new version and rolling it back is moving it to the old one. Neither needs
a deploy. latest is reserved and always means the newest
version.
production, and gets the
latest version while no version carries it. The last step moves production back to
v2 and no code is deployed.
store a version, then release it
curl -X POST localhost:4317/v1/api/prompts \
-H 'content-type: application/json' \
-d '{"name": "support/reply", "messages": [{"role": "system", "content": "Answer {{customer}} briefly."}]}'
curl -X PUT localhost:4317/v1/api/prompts/support%2Freply/labels/production \
-H 'content-type: application/json' -d '{"version": 1}' Fetching and rendering
Each SDK fetches a prompt by name and label and renders it with your
variables in the process; the daemon never renders one. With no label and
no version the SDK asks for production, and falls back to the
latest version when no version carries it. A variable the prompt names and
you did not pass is an error naming it.
Fetched prompts are cached for 60 seconds by default. After that the SDK fetches again, and if the daemon does not answer it keeps serving the copy it has, so an outage does not stop an agent that fetched once. Pass a fallback template for the first start, before anything was fetched.
fetch, render, link
/**
* A model generation. Its prompt is stored in mcpd, so a new version ships by
* moving the `production` label, with no deploy.
*/
async function draftAnswer(
ticket: Ticket,
order: { status: string; eta: string },
): Promise<string> {
// Cached for a minute. When the daemon cannot answer, the last copy is
// used, and with no copy the fallback, so an outage never stops an answer.
const prompt = await mcpd.getPrompt('support-answer', {
label: 'production',
fallback: ANSWER_MESSAGES,
})
const messages = renderPrompt(prompt, {
question: ticket.question,
order: JSON.stringify(order),
})
return await mcpd.withSpan(
'draft_answer',
async (generation) => {
// Links the call to its prompt version, so spans can be read per version.
generation.setPrompt(prompt)
generation.setInput(messages)
const response = await callModel(generation, order)
const answer = response.content[0]?.text ?? ''
generation.setOutput(answer)
// The model, the token counts mcpd prices, the response id and the
// finish reason, read from the response in one call.
generation.recordResponse(response)
return answer
},
{ kind: 'model_generation', entityKind: 'model', entityName: 'answer_writer' },
)
} from mcpd import render_prompt
def plan(question: str) -> str:
"""Draft a plan with the system prompt the team released as `production`."""
# Served from memory for 60 s, and from the last copy fetched while the daemon
# is down. The fallback covers a first start with no daemon and no copy.
prompt = mcpd.get_prompt(
"support-system",
label="production",
fallback="You are a support agent for {{company}}. Reply in {{language}}.",
)
system = render_prompt(prompt, {"company": "Acme", "language": "English"})
with mcpd.span("draft_plan", kind=SpanKind.MODEL_GENERATION) as generation:
# Links the span to this prompt version: `list_spans(prompt=...)` finds it.
generation.set_prompt(prompt)
generation.set_input([
{"role": "system", "content": system},
{"role": "user", "content": question},
])
time.sleep(0.05) # stand-in for the provider round trip
draft = "Look the order up, then give a delivery window."
generation.set_output(draft)
return draft // The version labelled `production`: moving the label releases another
// version with no deploy. A copy fetched once outlives a daemon outage, and
// the fallback covers a process that has never reached mcpd.
let options = GetPrompt::new()
.label("production")
.fallback(PromptFallback::Template("Answer this question: {{question}}".into()));
match mcpd.get_prompt("support/answer", &options).await {
Ok(prompt) => match render_prompt(&prompt, [("customer", "Ada"), ("question", question)]) {
Ok(rendered) => {
let call = run.child_of("draft with the prompt", SpanKind::ModelGeneration);
// Links the span to this version, so list_spans can find it.
call.set_prompt(&prompt);
call.set_input(rendered.text());
call.end();
}
Err(missing) => eprintln!("{missing}"),
},
Err(e) => eprintln!("could not fetch the prompt: {e}"),
} // The version labelled production, cached for a minute. If the daemon is
// down before the first fetch, the fallback keeps the agent working.
prompt, err := m.GetPrompt(ctx, "support/answer", mcpd.PromptOptions{
Label: "production",
FallbackMessages: []map[string]any{
{"role": "system", "content": "You answer billing questions for {{company}}."},
{"role": "user", "content": "{{question}}"},
},
})
if err != nil {
log.Fatalf("read the prompt: %v", err)
}
rendered, err := mcpd.RenderPrompt(prompt, map[string]string{
"company": "Acme", "question": question,
})
if err != nil {
log.Fatalf("render the prompt: %v", err)
}
ctx, span := m.Start(ctx, "draft_answer",
mcpd.WithSpanKind(mcpd.SpanKindModelGeneration),
mcpd.WithInput(map[string]any{"messages": rendered.Messages}),
)
defer span.End()
// The run now shows which prompt version wrote the answer.
span.SetPrompt(prompt) // The version labelled production. The client caches it for a minute and
// serves its last copy if mcpd is down; the fallback covers a first start
// with no daemon at all.
val prompt = mcpd.getPrompt(
"support/answer",
label = "production",
fallback = PromptFallback.Template("Answer in a {{ tone }} tone: {{question}}"),
)
val text = prompt.renderText(mapOf("tone" to "friendly", "question" to "where is order 41?"))
mcpd.span("draft_reply", SpanKind.MODEL_GENERATION) { generation ->
// Links the call to the prompt version, so the trace shows which
// version wrote the answer and listSpans(prompt = ...) finds it.
generation.setPrompt(prompt)
generation.setInput(listOf(mapOf("role" to "user", "content" to text)))
generation.setOutput(callModel(text))
} # The version `production` points at, cached for a minute. The fallback is
# used only when the daemon cannot be reached and nothing was fetched yet.
{:ok, prompt} =
Mcpd.get_prompt(mcpd, "support-reply",
label: "production",
fallback: "Reply in two sentences: {{question}}"
)
# A variable the template names and the map lacks raises, rather than sending
# a prompt with a hole in it. Extra variables are ignored.
text = Mcpd.Prompts.render!(prompt, %{question: question, customer: "Ada"})
# The prompt's name and version go on the span, so a trace shows which version
# produced the answer, and `Mcpd.list_spans(mcpd, prompt: "support-reply")`
# finds every call that used it.
Mcpd.span mcpd, "reply", kind: :model_generation, input: text,
attributes: Mcpd.Prompts.attributes(prompt) do
"Your order 41 shipped on Tuesday."
end Linking a step to its prompt
Link the model span to the prompt it ran and the span carries
mcpd.prompt.name and mcpd.prompt.version.
GET /v1/api/spans?prompt=support/reply&prompt_version=2
lists every step that ran version 2, which is how a regression after a
release is traced back to the change that caused it.
In the app
The Prompts screen lists every prompt at its latest version. Open one to read each version, what changed from the one before, the labels on it and the steps that ran it, to move a label, and to store a new version.
Endpoints
GET /v1/api/prompts: every prompt at its latest version.POST /v1/api/prompts: store a version.GET /v1/api/prompts/{name}: one version, bylabelorversion.GET /v1/api/prompts/{name}/versions: every version, newest first.PUTandDELETE /v1/api/prompts/{name}/labels/{label}: move or remove a label.