Промпты
Model Context Protocol (MCP) предоставляет стандартизированный способ для серверов предоставлять клиентам шаблоны промптов. Промпты позволяют серверам передавать структурированные сообщения и инструкции для взаимодействия с языковыми моделями. Клиенты могут получать список доступных промптов, извлекать их содержимое и передавать аргументы для их настройки.
В главе Основы мы научились объявлять простой промпт:
#[prompt(descr = "Generates a user message requesting a hello world code generation.")]
async fn hello_world_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}"))
}
Того же результата можно добиться без использования процедурного макроса:
use neva::prelude::*;
async fn hello_world_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}"))
}
#[tokio::main]
async fn main() {
let mut mcp_server = App::new()
.with_options(|opt| opt
.with_stdio()
.with_name("Sample MCP Server")
.with_version("1.0.0"));
mcp_server
.map_prompt("hello_world_code", hello_world_code)
.with_description("Generates a user message requesting a hello world code generation.");
mcp_server.run().await;
}
В примере выше имя промпта должно быть задано явно.
При использовании атрибутного макроса #[prompt] имя промпта автоматически выводится из имени функции.
Все остальные параметры промпта, доступные в атрибутном макросе, можно настроить с помощью методов with_* (например, with_description()).
Метод map_prompt() регистрирует обработчик промпта под указанным именем и возвращает изменяемую ссылку на зарегистрированный промпт.
Входные аргументы
Для промпта можно явно задать входные аргументы. Если аргументы не указаны, Neva автоматически генерирует их на основе сигнатуры функции-обработчика.
Для переопределения сгенерированной схемы укажите её в виде JSON-строки:
#[prompt(
descr = "Generates a user message requesting a hello world code generation.",
args = r#"[
{
"name": "lang",
"description": "A language to use",
"required": true
}
]"#
)]
async fn hello_world_code(lang: String) -> PromptMessage {
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}"))
}
Необязательные аргументы
Аргумент, объявленный как Option<T>, публикуется с "required": false, а
prompts/get, который его не передал, отдаёт обработчику None:
#[prompt(descr = "Generates a user message requesting a hello world code generation.")]
async fn hello_world_code(lang: String, tone: Option<String>) -> PromptMessage {
let tone = tone.unwrap_or_else(|| "neutral".into());
PromptMessage::user()
.with(format!("Write a hello-world function on {lang}, tone: {tone}"))
}
Если вы собираете список аргументов вручную,
PromptArgument::named(name, required) —
это форма без описания; PromptArgument::required и
PromptArgument::optional — то же самое с описанием.
Имена аргументов
Аргументы промпта читаются из prompts/get по имени, поэтому имена, по
которым читает обработчик, обязаны совпадать с теми, что публикует
prompts/list.
#[prompt] берёт имена параметров самой функции. У «голого» замыкания их
нет — Rust их не сохраняет, — и оно откатывается к позиционным arg0,
arg1, … Макрос map_prompt! считывает их с замыкания:
use neva::{App, map_prompt, types::Role};
#[tokio::main]
async fn main() {
let mut app = App::new();
map_prompt!(app, "analyze", |lang: String, code: String| async move {
(format!("Analyze this {lang} code: {code}"), Role::User)
})
.with_description("Analyzes a code snippet");
app.run().await;
}
Prompt::with_args() —
явная форма: она задаёт публикуемые аргументы и имена для извлечения одним
вызовом, так что разойтись они не могут.
Промпт, публикующий аргументы, которые его обработчик не читает, роняет
App::run при старте — см.
Инструменты → Проверка при старте: к промптам
применяется то же правило.
MCP-контекст
В более сложных сценариях — например, когда промпту нужен доступ к ресурсам, объявленным на том же MCP-сервере, — можно внедрить Context в обработчик промпта:
#[prompt(descr = "Generates a user message requesting a translate a text using the glossary.")]
async fn translate_with_glossary(ctx: Context, text: String) -> PromptMessage {
let glossary = ctx.resource("res://glossary").await?;
let resource = result.contents
.into_iter()
.next()
.expect("No resource contents");
PromptMessage::user()
.with(format!("Translate using this glossary:\n{glossary}\n\nText: {text}"))
}
Обучение на примерах
Полный пример доступен здесь.