From 17b47ce20640f3ef2ea704e8853534cc8b82e567 Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 31 Jul 2026 02:10:37 +0400 Subject: [PATCH] Route questions to query, not fact The router prompt tested "reports current state -> fact" before "wants information -> query", so a question naming a fact key was written as a fact. Query now comes first, plus an explicit question test. Reviewers: the prompt block in llmrouter.go, and the note about the training-side copy of the prompt that needs the same edit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ --- internal/router/llmrouter.go | 22 +++++++++++++++++----- internal/router/llmrouter_test.go | 22 +++++++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/internal/router/llmrouter.go b/internal/router/llmrouter.go index 3a3f4e2..40aeb18 100644 --- a/internal/router/llmrouter.go +++ b/internal/router/llmrouter.go @@ -34,6 +34,14 @@ string ::= "\"" ([^"\\] | "\\" .)* "\"" ws ::= [ \t\n]* ` +// routeSystem — the router prompt. Changed 31-07-2026: the query test now sits +// above the fact test and there is an explicit question test. Before that, a +// question naming a fact key ("сколько воды я выпил с утра") matched the fact +// rule first and was stored as an assertion — 15 of 76 fixture cases. +// +// The training workspace keeps its own copy of this prompt for relabelling, and +// `llm/check_prompt_parity.py` there compares the two. That copy is in another +// repo and was not touched, so parity will fail until it gets the same edit. const routeSystem = `Классифицируй ровно одно сообщение пользователя. Верни ОДИН JSON-массив действий. Ровно одно намерение: fact, reminder, note, query, act, chat, system. @@ -41,22 +49,26 @@ const routeSystem = `Классифицируй ровно одно сообще Классифицируй по цели пользователя. Порядок решения: 1. Хочет напоминание в будущем → reminder 2. Явно просит сохранить информацию → note -3. Сообщает или обновляет текущее состояние/событие → fact -4. Хочет получить информацию → query -5. Просит выполнить работу → act -6. Про ассистента, настройки или память → system -7. Иначе → chat +3. Задаёт вопрос: есть вопросительное слово (сколько, что, какой, когда, где, кто, почему, как) или знак «?» → query +4. Хочет получить информацию, в том числе о своих же данных → query +5. Утверждает: сообщает или обновляет текущее состояние/событие → fact +6. Просит выполнить работу → act +7. Про ассистента, настройки или память → system +8. Иначе → chat Различия: - note — сохранить информацию, без напоминания. text = суть. - reminder — уведомить позже. text = что напомнить. - fact — неявное обновление: пользователь сообщает, что что-то в мире изменилось (текущее/изменённое состояние, случившееся событие). key/value. +- query против fact — решает форма реплики, а не тема. Вопрос о состоянии — это query, даже если названо то же самое, что бывает в fact. Только утверждение — это fact. Примеры: "запиши пароль" → {"intent":"note","text":"пароль"} "напомни купить молоко" → {"intent":"reminder","text":"купить молоко"} "запиши купить молоко" → {"intent":"note","text":"купить молоко"} "я выпил воду" → {"intent":"fact","key":"water","value":"выпил"} +"сколько воды я выпил с утра" → {"intent":"query","text":"сколько воды я выпил с утра"} +"сколько раз я ел вчера?" → {"intent":"query","text":"сколько раз я ел вчера"} "мой любимый фильм — Интерстеллар" → {"intent":"note","text":"любимый фильм — Интерстеллар"} "что такое docker?" → {"intent":"query","text":"что такое docker"} "напиши письмо" → {"intent":"act","verb":"написать письмо"} diff --git a/internal/router/llmrouter_test.go b/internal/router/llmrouter_test.go index 9a736b7..de8cde5 100644 --- a/internal/router/llmrouter_test.go +++ b/internal/router/llmrouter_test.go @@ -3,16 +3,36 @@ package router import ( "context" "fmt" + "strings" "testing" "time" "github.com/kami/maven/internal/llm" ) -type mockLLM struct{ out string; err error } +type mockLLM struct { + out string + err error +} func (m mockLLM) Complete(_ context.Context, _ llm.Req) (string, error) { return m.out, m.err } +// A question naming a fact key used to be stored as a fact because the fact rule +// was tested first. Keep the query rule above it. +func TestRoutePromptTestsQueryBeforeFact(t *testing.T) { + query := strings.Index(routeSystem, "→ query") + fact := strings.Index(routeSystem, "состояние/событие → fact") + if query < 0 || fact < 0 { + t.Fatalf("prompt lost a rule: query=%d fact=%d", query, fact) + } + if query > fact { + t.Fatal("query rule must come before the fact rule") + } + if !strings.Contains(routeSystem, "Задаёт вопрос") { + t.Fatal("prompt lost the explicit question test") + } +} + func TestLLMRouterFactMapping(t *testing.T) { lr := NewLLMRouter(mockLLM{out: `{"intent":"fact","key":"water","value":"выпил"}`}) d, ok, err := lr.Route(context.Background(), "я выпил воду", time.Now())