From 43f2c37538f5c8002d43f8df8d8bbb12bbbd8558 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 4 Aug 2026 02:52:51 +0400 Subject: [PATCH] router: stage 0 claims the other days and the named event (V-471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "какие планы на сегодня" worked and "какие планы на завтра" answered "пока не умею": the agenda rule needs "у меня" or a calendar noun, and that phrasing carries neither. "когда планёрка?" had the same shape. Two rules. One takes a plan noun aimed at a named day, one takes a closed list of event nouns after "когда"/"во сколько". Both route intent only, so the query chain still decides which source answers. classifier+onnx over the fixture: 55/79, 69.6% full, with the two new cases passing and no case moving the other way. --- internal/router/agenda_test.go | 44 +++++++++++++++++++++++++ internal/router/eval/ru_routing_v1.json | 2 ++ internal/router/stage0.go | 29 ++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/internal/router/agenda_test.go b/internal/router/agenda_test.go index 4ef3407..9789c90 100644 --- a/internal/router/agenda_test.go +++ b/internal/router/agenda_test.go @@ -75,3 +75,47 @@ func TestAgendaGrammarSparesStatements(t *testing.T) { } } } + +// The tomorrow form and the bare event noun. Both were measured answering +// "пока не умею" on the deployed daemon, 02-08-2026, while the same question +// about today worked — the first rule set needed "у меня" or a calendar noun +// and these phrasings carry neither (Vikunja #471). +func TestAgendaCoversOtherDaysAndNamedEvents(t *testing.T) { + r := agendaRouter(t) + for _, u := range []string{ + "какие планы на завтра?", + "какие планы на послезавтра", + "что по делам в среду", + "какие планы на выходные", + "когда планёрка?", + "во сколько созвон", + "когда будет совещание", + } { + d, err := r.Route(context.Background(), u, refNow()) + if err != nil { + t.Fatalf("route(%q): %v", u, err) + } + if d.Intent != IntentQuery { + t.Errorf("route(%q) = %s, want query", u, d.Intent) + } + } +} + +// The two new rules are narrow on purpose. A world question that opens with +// "когда" is not an agenda question, and telling her about a plan is not +// asking about one. +func TestAgendaGrammarsLeaveTheWorldAlone(t *testing.T) { + r := agendaRouter(t) + for _, u := range []string{ + "когда была битва при ватерлоо", + "когда изобрели телефон", + } { + d, err := r.Route(context.Background(), u, refNow()) + if err != nil { + t.Fatalf("route(%q): %v", u, err) + } + if d.Stage == 0 { + t.Errorf("route(%q) was claimed at stage 0 as %s", u, d.Intent) + } + } +} diff --git a/internal/router/eval/ru_routing_v1.json b/internal/router/eval/ru_routing_v1.json index 6c765f0..2f6004e 100644 --- a/internal/router/eval/ru_routing_v1.json +++ b/internal/router/eval/ru_routing_v1.json @@ -23,6 +23,8 @@ { "id": "ru-query-012", "utterance": "какие заметки я оставил про полив", "lang": "ru", "intent": "query", "tags": ["recall"] }, { "id": "ru-query-013", "utterance": "во сколько у меня встреча", "lang": "ru", "intent": "query", "tags": ["calendar"] }, { "id": "ru-query-019", "utterance": "что у меня стоит в календаре на послезавтра", "lang": "ru", "intent": "query", "tags": ["calendar", "hard"], "note": "agenda, not the clock: the daemon answers this from CalendarEvents inside the query branch, so the clock/date system rule must not swallow it" }, + { "id": "ru-query-022", "utterance": "какие планы на завтра?", "lang": "ru", "intent": "query", "tags": ["calendar"], "note": "the same agenda question as ru-query-019 aimed at another day; it answered \u043f\u043e\u043a\u0430 \u043d\u0435 \u0443\u043c\u0435\u044e on the deployed daemon while the today form worked (Vikunja #471)" }, + { "id": "ru-query-023", "utterance": "\u043a\u043e\u0433\u0434\u0430 \u043f\u043b\u0430\u043d\u0451\u0440\u043a\u0430?", "lang": "ru", "intent": "query", "tags": ["calendar", "hard"], "note": "a named event with no calendar word — the noun is the only signal that this is a question about his day" }, { "id": "ru-query-014", "utterance": "я успеваю до дедлайна", "lang": "ru", "intent": "query", "tags": ["hard", "no-question-word"] }, { "id": "ru-query-015", "utterance": "сколько я прошёл шагов", "lang": "ru", "intent": "query", "tags": ["aggregate"] }, { "id": "ru-query-016", "utterance": "покажи давление за неделю", "lang": "ru", "intent": "query", "tags": ["hard", "imperative"], "note": "imperative form but a read — must not route to act" }, diff --git a/internal/router/stage0.go b/internal/router/stage0.go index 615a4fc..eafc89a 100644 --- a/internal/router/stage0.go +++ b/internal/router/stage0.go @@ -182,9 +182,38 @@ func AgendaQueryGrammars() []Grammar { Pattern: regexp.MustCompile(`(?i)^\s*(что|чего|какие|сколько|во\s+сколько|когда)\s+у\s+меня(\s|[?!.]|$)`), Build: agendaQueryBuild, }, + { + // A plan noun aimed at a named day, with no possessive to anchor + // on: "какие планы на завтра", "что по делам в среду". The rule + // above wants "у меня" and this phrasing never has it, so + // "какие планы на завтра" answered "пока не умею" while "какие + // планы на сегодня" worked (Vikunja #471). The day word is what + // makes it an agenda question rather than a topic. + Name: "plan-day-query", + // Only "план" and "дел". A verb stem like "встреч" would take + // "встречаемся в среду", which is him telling her something, not + // asking. + Pattern: regexp.MustCompile(`(?i)(^|\s)(план|дел)[а-я]*\s+(на|в|во|по)\s+` + dayWordPattern + `(\s|[?!.]|$)`), + Build: agendaQueryBuild, + }, + { + // A named event with no calendar word at all: "когда планёрка?", + // "во сколько созвон". He is asking when something on his calendar + // happens, and the noun is the only signal. Closed list, so "когда + // битва при Ватерлоо" is still a world question. + Name: "event-time-query", + Pattern: regexp.MustCompile(`(?i)^\s*(когда|во\s+сколько|в\s+котором\s+часу)\s+(будет\s+|у\s+нас\s+)?(планёрк|планерк|встреч|созвон|митинг|совещани|звонок|созвон|приём|прием|интервью|собеседовани|тренировк|урок|занятие|пара)[а-я]*(\s|[?!.]|$)`), + Build: agendaQueryBuild, + }, } } +// dayWordPattern — the day words an agenda question can name. Weekdays appear +// in the accusative and prepositional forms the questions actually use ("в +// среду", "на среде"), which is why the stems carry an inflection tail rather +// than a fixed ending. +const dayWordPattern = `(сегодня|завтра|послезавтра|выходн[а-я]+|недел[а-я]+|понедельник[а-я]*|вторник[а-я]*|сред[ауые][а-я]*|четверг[а-я]*|пятниц[ауые][а-я]*|суббот[ауые][а-я]*|воскресень[ея][а-я]*)` + // agendaQueryBuild — shared Build for the agenda grammars. Confidence 1.0 on // the intent only: the utterance travels intact and the query chain's own // matchers decide the rest.