diff --git a/cmd/mavend/voicewire.go b/cmd/mavend/voicewire.go index 7bc4ccd..23229b1 100644 --- a/cmd/mavend/voicewire.go +++ b/cmd/mavend/voicewire.go @@ -385,6 +385,9 @@ func buildRouter(emb router.Embedder, acts router.ActMatcher, threshold float64, // Same reason as the agenda rules, for the feeds: "что нового в лентах?" // routed system and answered "пока не умею" (Vikunja #474). grammars = append(grammars, router.FeedQueryGrammar()) + // The list side of the same exposure: a phrasing with no possessive in it + // ("список дел") routed system and never reached queryTasks (Vikunja #467). + grammars = append(grammars, router.TaskListGrammar()) grammars = append(grammars, router.ReminderGrammar()) // Last, and it matches any utterance shape — its Build is the filter. An // explicit capture marker beats the model, which called it an act and diff --git a/internal/router/eval/eval_test.go b/internal/router/eval/eval_test.go index 89fad15..f8f0571 100644 --- a/internal/router/eval/eval_test.go +++ b/internal/router/eval/eval_test.go @@ -237,7 +237,11 @@ func newBaselineRouter(t *testing.T, emb router.Embedder, llmR *router.LLMRouter // anything while its grammar set is the daemon's grammar set. grammars = append(grammars, router.AgendaQueryGrammars()...) grammars = append(grammars, router.FeedQueryGrammar()) + // The list side of the same exposure: a phrasing with no possessive in it + // ("список дел") routed system and never reached queryTasks (Vikunja #467). + grammars = append(grammars, router.TaskListGrammar()) grammars = append(grammars, router.ReminderGrammar()) + grammars = append(grammars, router.TaskCaptureGrammar()) return router.New(router.Config{ Grammars: grammars, Classifier: cls, diff --git a/internal/router/task.go b/internal/router/task.go index 96b0e2e..160cc1d 100644 --- a/internal/router/task.go +++ b/internal/router/task.go @@ -248,3 +248,32 @@ func TaskCaptureGrammar() Grammar { }, } } + +// TaskListGrammar — stage 0 for "какие у меня задачи", "список дел", "что мне +// нужно сделать" (Vikunja #467). +// +// The same exposure the capture marker had, pointed the other way. IsTaskListQuery +// is a deterministic lookup that lives inside a query source, so it is only +// consulted once the turn is already IntentQuery. A phrasing the model calls +// system or note never reaches it, and "пока не умею" is what he hears — the +// failure the agenda and feed rules were written for. +// +// Placed after the agenda rules, which already send "какие у меня задачи" to +// query. What this adds is the phrasings with no possessive in them. +func TaskListGrammar() Grammar { + return Grammar{ + Name: "task-list-query", + Pattern: regexp.MustCompile(`(?s)^\s*(.+)$`), + Build: func(m []string) (Decision, bool) { + if !IsTaskListQuery(m[1]) { + return Decision{}, false + } + return Decision{ + Stage: 0, + Intent: IntentQuery, + Confidence: 1.0, + Slots: Slots{Text: strings.TrimSpace(m[1])}, + }, true + }, + } +} diff --git a/internal/router/task_test.go b/internal/router/task_test.go index 83e5ffd..ead243b 100644 --- a/internal/router/task_test.go +++ b/internal/router/task_test.go @@ -119,3 +119,27 @@ func TestTaskCaptureGrammarClaimsTheMarker(t *testing.T) { } } } + +// TestTaskListGrammarClaimsTheAsk — a list question answered before the model, +// including the phrasings with no possessive that used to route elsewhere. +func TestTaskListGrammarClaimsTheAsk(t *testing.T) { + g := TaskListGrammar() + claimed := []string{"какие у меня задачи", "список дел", "что мне нужно сделать"} + for _, u := range claimed { + m := g.Pattern.FindStringSubmatch(u) + if m == nil { + t.Fatalf("%q did not match the grammar pattern", u) + } + d, ok := g.Build(m) + if !ok || d.Intent != IntentQuery { + t.Errorf("%q built %+v ok=%v; want a query", u, d, ok) + } + } + passed := []string{"как дела", "напомни купить хлеб", "что docker делает"} + for _, u := range passed { + m := g.Pattern.FindStringSubmatch(u) + if _, ok := g.Build(m); ok { + t.Errorf("%q was claimed as a task list", u) + } + } +}