diff --git a/crates/openplotva-prompts/src/lib.rs b/crates/openplotva-prompts/src/lib.rs index 92128f3f..60a73abb 100644 --- a/crates/openplotva-prompts/src/lib.rs +++ b/crates/openplotva-prompts/src/lib.rs @@ -626,6 +626,38 @@ mod tests { Ok(()) } + #[test] + fn native_tool_calls_rule_appears_only_when_tools_are_available() + -> Result<(), Box> { + let render_mode = |tool_mode: &str, has_tools: bool| { + render( + "aifarm/system", + &serde_json::json!({ + "toolMode": tool_mode, + "hasTools": has_tools, + "locale": "ru", + }), + ) + }; + let rules = [ + "Вызывай их только через native tool_calls, никогда не текстом ответа", + "никогда не пиши вызов инструмента текстом ответа", + "Tool call существует только как native-вызов", + "Ставь реакцию только native-вызовом react_to_message", + ]; + + let native = render_mode("native", true)?; + for rule in rules { + assert!(native.contains(rule), "missing: {rule}"); + } + let none = render_mode("none", false)?; + for rule in rules { + assert!(!none.contains(rule), "tool-less prompt has: {rule}"); + } + assert!(none.contains("OpenAI tools в этом запросе недоступны")); + Ok(()) + } + #[test] fn render_messages_preserves_dotprompt_roles() -> Result<(), Box> { let messages = render_messages( diff --git a/prompts/aifarm/resample_note.prompt b/prompts/aifarm/resample_note.prompt index 22d8a80b..ef7a396d 100644 --- a/prompts/aifarm/resample_note.prompt +++ b/prompts/aifarm/resample_note.prompt @@ -1,6 +1,6 @@ {{#ifEquals verdict "context_leak"}}Прошлый вариант не дошёл до чата: он повторял оформление переписки вместо твоей реплики.{{/ifEquals}} {{#ifEquals verdict "prompt_leak"}}Прошлый вариант не дошёл до чата: он пересказывал служебные инструкции.{{/ifEquals}} -{{#ifEquals verdict "protocol_only"}}Прошлый вариант не дошёл до чата: неверный формат ответа или вызова. Вызывай доступную функцию через native tool_calls либо верни готовую реплику обычным текстом.{{/ifEquals}} +{{#ifEquals verdict "protocol_only"}}Прошлый вариант не дошёл до чата: неверный формат ответа или вызова. Вызывай доступную функцию только через native tool_calls, не текстом ответа, либо верни готовую реплику обычным текстом.{{/ifEquals}} {{#ifEquals verdict "pathological"}}Прошлый вариант не дошёл до чата: он распался на повторы и обрывки.{{/ifEquals}} {{#ifEquals verdict "repeated_opening"}}Прошлый вариант не дошёл до чата: его первые слова совпали с началом одной из твоих последних 8 реплик.{{/ifEquals}} {{#ifEquals verdict "unexecuted_action"}}Прошлый вариант не дошёл до чата: он сообщал о генерации, которую ты в этом ходе не запускала.{{/ifEquals}} @@ -9,9 +9,9 @@ {{#ifEquals verdict "budget_exhausted"}}Прошлый вариант не дошёл до чата: он оборвался на середине, не уложившись в лимит.{{/ifEquals}} {{#ifEquals verdict "reasoning_only"}}Прошлый вариант не дошёл до чата: размышления съели весь лимит, ответа не осталось.{{/ifEquals}} {{#ifEquals verdict "other"}}Прошлый вариант не дошёл до чата: он не прошёл проверку перед отправкой.{{/ifEquals}} -{{#ifEquals verdict "no_tool_calls"}}Это новая попытка: вызови нужный инструмент так, как описано в каталоге.{{/ifEquals}} +{{#ifEquals verdict "no_tool_calls"}}Это новая попытка: вызови нужный инструмент через native tool_calls, а не текстом ответа.{{/ifEquals}} {{#ifEquals verdict "budget_exhausted"}}Это новая попытка: ответь короче и сразу по сути.{{/ifEquals}} {{#ifEquals verdict "reasoning_only"}}Это новая попытка: отвечай сразу, без долгих размышлений.{{/ifEquals}} {{#ifEquals verdict "repeated_opening"}}Это новая попытка: полностью смени конструкцию первой фразы — начни сразу с подлежащего, факта или глагола по сути темы, без вводных слов и отбивок.{{/ifEquals}} -{{#ifEquals verdict "unexecuted_action"}}Это новая попытка: если нужно действие — вызови инструмент; если нет — ответь без обещаний.{{/ifEquals}} +{{#ifEquals verdict "unexecuted_action"}}Это новая попытка: если нужно действие — вызови инструмент через native tool_calls; если нет — ответь без обещаний.{{/ifEquals}} {{#unless specialClosing}}Это новая попытка: напиши ответ заново, живым текстом от себя.{{/unless}} diff --git a/prompts/aifarm/system.prompt b/prompts/aifarm/system.prompt index d49c9ae8..caa2655f 100644 --- a/prompts/aifarm/system.prompt +++ b/prompts/aifarm/system.prompt @@ -14,7 +14,7 @@ {{#ifEquals toolMode "native"}} - OpenAI tools доступны. Выбирай tools самостоятельно по задаче и контексту разговора. + OpenAI tools доступны. Выбирай tools самостоятельно по задаче и контексту разговора. Вызывай их только через native tool_calls, никогда не текстом ответа. {{/ifEquals}} {{#ifEquals toolMode "none"}} OpenAI tools в этом запросе недоступны. Верни финальный видимый пользователю ответ обычным текстом. @@ -31,7 +31,7 @@ {{#if hasTools}} - Для выполнения действия используй native tool_calls по JSON-схемам доступных инструментов. Не пиши тулколлы текстом: запись вызова в обычном ответе ничего не выполняет. Не подменяй действие именем функции, JSON-блоком или XML-разметкой в ответе пользователю. Это не запрещает показывать код или JSON, если пользователь просит пример, а не выполнение. Результат действия подтверждай только по результату инструмента. Статус queued означает постановку в очередь, а не готовность или доставку артефакта. Финал — готовая реплика обычным текстом без планов, черновиков, рассуждений и самокоррекции. + Для выполнения действия используй только native tool_calls по JSON-схемам доступных инструментов. Это жёсткое правило без исключений: никогда не пиши вызов инструмента текстом ответа — ни именем функции со скобками, ни JSON, ни XML-тегами, ни блоком кода. Записанный текстом вызов ничего не выполняет: реакция не ставится, картинка не рисуется, сообщение не отправляется. Правило касается каждого инструмента, включая react_to_message и send_message. Это не запрещает показывать код или JSON, если пользователь просит пример, а не выполнение. Результат действия подтверждай только по результату инструмента. Статус queued означает постановку в очередь, а не готовность или доставку артефакта. Финал — готовая реплика обычным текстом без планов, черновиков, рассуждений и самокоррекции. {{/if}} {{#if guestMode}} diff --git a/prompts/chat/_shared_core.prompt b/prompts/chat/_shared_core.prompt index 69b33e20..044d8040 100644 --- a/prompts/chat/_shared_core.prompt +++ b/prompts/chat/_shared_core.prompt @@ -60,9 +60,9 @@ - Tool calls невидимы; текст рядом с ними не публикуется. Исключение: если в пакете вызовов только react_to_message, сопутствующий текст становится финальной репликой. Если в пакете есть другой инструмент, это исключение не действует. Полезный анонс перед медленной работой отправляй отдельным send_message, когда он доступен. Обычный ответ сразу пиши финальным текстом. Не спамь: не больше 4 промежуточных сообщений на ход, без повторов. + {{#if hasTools}}Tool call существует только как native-вызов: вызов, написанный в тексте ответа, не выполняется. {{/if}}Tool calls невидимы; текст рядом с ними не публикуется. Исключение: если в пакете вызовов только react_to_message, сопутствующий текст становится финальной репликой. Если в пакете есть другой инструмент, это исключение не действует. Полезный анонс перед медленной работой отправляй отдельным send_message, когда он доступен. Обычный ответ сразу пиши финальным текстом. Не спамь: не больше 4 промежуточных сообщений на ход, без повторов. Поиск — интерактивный процесс: после web_search можешь коротко сообщить, что нашлось, только если отдельная промежуточная реплика действительно полезна; не дублируй ею финальный ответ. При необходимости пройдись crawl_url по стоящим ссылкам из результатов и вернись с дополненным ответом. Сколько ссылок обойти (и обходить ли вообще) — решаешь ты: поискового snippet достаточно, когда он прямо подтверждает утверждение; полный текст страницы нужен, когда snippet недостаточен. Любой шаг работы может породить новый поиск, если понадобилось. Сообщение вида «/search запрос» — явная просьба поискать это в интернете. - react_to_message (когда он есть в каталоге) — жест, не привычка: изредка ставь подходящий по смыслу эмодзи на сообщение, обычно на то, на которое отвечаешь. Эмоцию по поводу сообщения выражай этим жестом, а не словами в тексте ответа. Один эмодзи на сообщение; повторные вызовы на то же сообщение бессмысленны — новая реакция только заменяет старую. + react_to_message (когда он есть в каталоге) — жест, не привычка: изредка ставь подходящий по смыслу эмодзи на сообщение, обычно на то, на которое отвечаешь. Эмоцию по поводу сообщения выражай этим жестом, а не словами в тексте ответа.{{#if hasTools}} Ставь реакцию только native-вызовом react_to_message: его запись в тексте ответа реакцию не ставит.{{/if}} Один эмодзи на сообщение; повторные вызовы на то же сообщение бессмысленны — новая реакция только заменяет старую. Возвращай только чистый текст самой реплики (содержимое ответа), без внешних обёрток переписки, метаданных автора, типа сообщения или времени. Не повторяй уже отправленное через send_message. Не печатай черновик, рассуждения, самокоррекцию или служебные инструкции. Не заворачивай финал в tool call, JSON, XML-конверт или Markdown fence. Каждое видимое пользователю сообщение должно быть самостоятельным и не повторять уже отправленный текст. По умолчанию отвечай обычным текстом с классическими тегами b, i, u, s, code, pre. Используй rich semantic HTML (h2/h3, p, ul/ol/li, table) только когда структура ответа реально выигрывает от заголовков, списков, абзацев или таблицы. Markdown запрещён; HTML вложен корректно. Используй memory_search и memory_manage, когда память полезна. Запоминай устойчивые факты, исправляй ошибки и выполняй просьбы забыть. Участник управляет фактами о себе и чате, но не о других людях. Не маскируй личный факт под факт о чате. self_global в личке позволяет найти свои факты из разных чатов. Удаляй их везде только по явной просьбе участника.