From e481ad493000210005c846c8303d282433a49755 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 4 Aug 2026 03:44:48 +0400 Subject: [PATCH] mavend: a question about attention reaches Praxis (V-475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The capability was built, wired and degrading correctly, and no utterance could reach it. Its aliases sit on the act dispatch, "что требует внимания" routes to a query, and every query source passed — so the turn fell to the web search and came back with an article about the concept of attention. That reads as an answer, which is worse than silence. queryAttention sits next to "tasks", above the recall sources and well above the personal boundary: it is operational state about his things, and a notes pass would otherwise answer from whatever he once wrote about a server. It calls the same handler the act path calls, so the outage string comes free. An absent or unconfigured Praxis falls through instead of claiming the turn, like queryHome and queryNetwork. A configured Praxis that is down claims it and names the gap. "что нового" is left to the feeds source. --- cmd/mavend/actions_query.go | 5 +++ cmd/mavend/attentionq.go | 68 +++++++++++++++++++++++++++++++ cmd/mavend/attentionq_test.go | 77 +++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 cmd/mavend/attentionq.go create mode 100644 cmd/mavend/attentionq_test.go diff --git a/cmd/mavend/actions_query.go b/cmd/mavend/actions_query.go index 9629303..7d8b039 100644 --- a/cmd/mavend/actions_query.go +++ b/cmd/mavend/actions_query.go @@ -81,6 +81,11 @@ var querySources = []querySource{ // matcher requires a task noun or an explicit "что … сделать", so a // date-bearing question still reaches the calendar. {name: "tasks", answer: (*reactiveHandler).queryTasks}, + // Next to "tasks" and for the same reason: "что требует внимания?" is a + // question about the operational state Praxis holds, and it used to fall + // through every source to the web search (Vikunja #475). Its matcher needs + // an attention marker, and it falls through when Praxis is not configured. + {name: "attention", answer: (*reactiveHandler).queryAttention}, // Before the recall sources too: "сколько я потратил?" is a question about // the money facts the poller wrote, and the notes pass would otherwise // answer it from whatever he once said about spending. Its matcher needs a diff --git a/cmd/mavend/attentionq.go b/cmd/mavend/attentionq.go new file mode 100644 index 0000000..bb5befa --- /dev/null +++ b/cmd/mavend/attentionq.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "strings" + + "github.com/kami/maven/internal/router" +) + +// attentionMarkers — the ways he asks what Praxis is holding. Substrings on a +// stem, because "внимание", "внимания" and "вниманию" are one word to him. +// +// "что нового" is deliberately absent: the feeds source claims it, and it +// still should — a question about news is a question about the feeds she +// reads. This list is about the operational state of his things. +var attentionMarkers = []string{ + "внимани", "что требует", "что не так", "что важн", "что срочн", + "needs attention", "what needs looking", +} + +// isAttentionQuery reports whether the utterance asks what needs looking at. +func isAttentionQuery(u string) bool { + s := strings.ToLower(strings.TrimSpace(u)) + if s == "" { + return false + } + for _, m := range attentionMarkers { + if strings.Contains(s, m) { + return true + } + } + return false +} + +// queryAttention answers "что требует внимания?" from Praxis. +// +// The capability was already built and already degraded correctly, and no +// utterance could reach it (Vikunja #475). Its aliases live on the act +// dispatch, and the question routes to IntentQuery, so it fell through every +// source to the web search and came back with an encyclopedia article about +// the concept of attention — worse than silence, because it reads as an +// answer. +// +// Placed above the recall sources and well above the personal boundary: this +// is operational state about his things, and a notes pass would otherwise +// answer it from whatever he once wrote about a server. An unconfigured or +// absent Praxis falls through rather than claiming the turn, the same +// convention queryHome and queryNetwork follow. A Praxis that is configured +// and down does claim it, and says it cannot reach the service — that is the +// degradation the ecosystem contract asks for, and it comes from the same +// handler the act path uses. +func (h *reactiveHandler) queryAttention(ctx context.Context, t *queryTurn) (string, bool) { + if !isAttentionQuery(t.dec.Utterance) { + return "", false + } + if h.ecosystem == nil || h.ecosystem.praxis == nil { + return "", false + } + reply := h.handlePraxisAct(ctx, router.Decision{ + Utterance: t.dec.Utterance, + Intent: router.IntentAct, + Slots: router.Slots{Fn: "list_attention", HasFn: true}, + }) + if reply == "" { + return "", false + } + return reply, true +} diff --git a/cmd/mavend/attentionq_test.go b/cmd/mavend/attentionq_test.go new file mode 100644 index 0000000..ee95411 --- /dev/null +++ b/cmd/mavend/attentionq_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/kami/maven/internal/router" +) + +func TestIsAttentionQuery(t *testing.T) { + for _, tc := range []struct { + text string + want bool + }{ + {"что требует внимания", true}, + {"на что обратить внимание?", true}, + {"что не так?", true}, + {"что важного?", true}, + // The feeds source owns this one, and should keep owning it. + {"что нового?", false}, + {"какая погода?", false}, + {"", false}, + } { + if got := isAttentionQuery(tc.text); got != tc.want { + t.Errorf("isAttentionQuery(%q) = %v, want %v", tc.text, got, tc.want) + } + } +} + +// TestAttentionQuestionReachesPraxis — the defect (Vikunja #475). The question +// routes to IntentQuery, and every source used to pass, so a web search about +// the concept of attention answered it. +func TestAttentionQuestionReachesPraxis(t *testing.T) { + ctx := context.Background() + praxis := newFakePraxis(t, fixturePraxisAttentionItems(map[string]any{ + "id": "item_1", "title": "disk almost full", "importance": 3.0, + })) + h := ecoHandler(t, nil, praxis, nil) + + reply, ok := h.queryAttention(ctx, &queryTurn{dec: router.Decision{ + Intent: router.IntentQuery, Utterance: "что требует внимания", + }}) + if !ok { + t.Fatal("the attention question must be claimed before the world sources") + } + if !strings.Contains(reply, "disk almost full") { + t.Fatalf("reply = %q, want the praxis item", reply) + } +} + +// A configured Praxis that is down claims the turn and says so. Falling +// through here would answer an outage with an encyclopedia article. +func TestAttentionQuestionSaysWhenPraxisIsDown(t *testing.T) { + ctx := context.Background() + praxis := newFakePraxis(t, fixturePraxisAttentionItems()) + h := ecoHandler(t, nil, praxis, nil) + praxis.SetFault(503) + + reply, ok := h.queryAttention(ctx, &queryTurn{dec: router.Decision{ + Intent: router.IntentQuery, Utterance: "что требует внимания", + }}) + if !ok || !strings.Contains(reply, "не могу") { + t.Fatalf("an outage must name the gap, got ok=%v reply=%q", ok, reply) + } +} + +// No Praxis configured means no claim: the rest of the chain still runs. +func TestAttentionQuestionFallsThroughWithoutPraxis(t *testing.T) { + h, _ := newFactGateHandler(t, time.Now()) + if _, ok := h.queryAttention(context.Background(), &queryTurn{dec: router.Decision{ + Intent: router.IntentQuery, Utterance: "что требует внимания", + }}); ok { + t.Fatal("an unconfigured praxis must not claim the turn") + } +} -- 2.52.0