Risk tiers: take Hexis's tier for a Hexis capability, derive only for local tool rows #162
+3
-2
@@ -16,6 +16,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/kami/maven/internal/loop"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
@@ -35,10 +36,10 @@ func (h *reactiveHandler) resolveAck(ctx context.Context, text string, src turnS
|
||||
}
|
||||
if err := h.api.ResolveNudge(ctx, target.ID, store.NudgeActed, now); err != nil {
|
||||
log.Printf("voice: ack nudge %d (%s, %s): %v", target.ID, target.Rule, src, err)
|
||||
return "не получилось отметить.", true
|
||||
return phraser.Ack(phraser.FailAck, nil), true
|
||||
}
|
||||
log.Printf("voice: acked nudge %d (rule %s) from %s", target.ID, target.Rule, src)
|
||||
return "отлично, отметила.", true
|
||||
return phraser.Ack(phraser.AckNudge, nil), true
|
||||
}
|
||||
|
||||
// ackFromFact — post-action hook, called once the turn's decision has been
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
// it for recall, and let pattern detection propose a routine.
|
||||
func (h *reactiveHandler) actionFact(ctx context.Context, dec router.Decision) string {
|
||||
if !dec.Slots.HasKey {
|
||||
return "не разобрала, что записать — попробуй иначе."
|
||||
return phraser.Ack(phraser.FailFactUnparsed, nil)
|
||||
}
|
||||
// A question is never a fact about him (#470). "какая последняя версия
|
||||
// языка Go?" used to land here, and the value stored was whatever the
|
||||
@@ -62,7 +63,7 @@ func (h *reactiveHandler) actionFact(ctx context.Context, dec router.Decision) s
|
||||
factID, err := h.api.WriteFact(ctx, req)
|
||||
if err != nil {
|
||||
log.Printf("voice: write fact: %v", err)
|
||||
return "не получилось сохранить факт."
|
||||
return phraser.Ack(phraser.FailFact, nil)
|
||||
}
|
||||
// Index the fact in long-term memory (best-effort, must not fail the fact
|
||||
// write). Facts aren't in the notes table, so this is the only recall path
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
@@ -23,13 +24,13 @@ func (h *reactiveHandler) actionNote(ctx context.Context, dec router.Decision) s
|
||||
vec, err := router.EmbedPassage(ctx, h.embedder, dec.Utterance)
|
||||
if err != nil {
|
||||
log.Printf("voice: embed note: %v", err)
|
||||
return "не получилось сохранить заметку."
|
||||
return phraser.Ack(phraser.FailNote, nil)
|
||||
}
|
||||
noteTs := h.now()
|
||||
noteID, err := h.api.WriteNote(ctx, noteTs, dec.Utterance, vec, "tap:voice")
|
||||
if err != nil {
|
||||
log.Printf("voice: write note: %v", err)
|
||||
return "не получилось сохранить заметку."
|
||||
return phraser.Ack(phraser.FailNote, nil)
|
||||
}
|
||||
// Insert into long-term memory (best-effort, must not fail the note write).
|
||||
// text/ts in the meta make a Search hit self-describing (see bestRecall).
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
@@ -21,13 +22,13 @@ func (h *reactiveHandler) actionReminder(ctx context.Context, dec router.Decisio
|
||||
}
|
||||
}
|
||||
if !dec.Slots.HasTime {
|
||||
return "не получилось разобрать время напоминания."
|
||||
return phraser.Ack(phraser.FailReminderTime, nil)
|
||||
}
|
||||
}
|
||||
payload := `{"text":` + jsonString(dec.Utterance) + `}`
|
||||
if _, err := h.api.CreateReminder(ctx, dec.Slots.Time, payload, ""); err != nil {
|
||||
log.Printf("voice: create reminder: %v", err)
|
||||
return "не получилось поставить напоминание."
|
||||
return phraser.Ack(phraser.FailReminder, nil)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/store"
|
||||
"github.com/kami/maven/internal/tasks"
|
||||
@@ -40,18 +41,18 @@ func (h *reactiveHandler) captureTaskFromNote(ctx context.Context, dec router.De
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("voice: capture task: %v", err)
|
||||
return "не получилось записать задачу.", true
|
||||
return phraser.Ack(phraser.FailTask, nil), true
|
||||
}
|
||||
if resp.Promoted {
|
||||
// It was a candidate Maven derived from something she read, and he has
|
||||
// now said it himself. Saying "уже в списке" here would be answering a
|
||||
// confirmation with a shrug.
|
||||
return "поняла, беру в работу: " + cap.Text, true
|
||||
return phraser.Ack(phraser.AckTaskUrgent, map[string]string{"text": cap.Text}), true
|
||||
}
|
||||
if !resp.Created {
|
||||
return "это уже в списке.", true
|
||||
return phraser.Ack(phraser.AckTaskDuplicate, nil), true
|
||||
}
|
||||
return "записала: " + cap.Text, true
|
||||
return phraser.Ack(phraser.AckTask, map[string]string{"text": cap.Text}), true
|
||||
}
|
||||
|
||||
// queryTasks — "какие у меня задачи?", "что мне нужно сделать?".
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
@@ -108,7 +109,7 @@ func TestCaptureTaskFromNoteReportsStoreFailure(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("a failed capture still claims the turn — the note path must not double-write")
|
||||
}
|
||||
if !strings.Contains(reply, "не получилось") {
|
||||
if !phraser.IsAck(phraser.FailTask, nil, reply) {
|
||||
t.Errorf("reply = %q, want an honest failure", reply)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
)
|
||||
|
||||
// resolveQuietToggle — pre-route keyword check. Returns (reply, true) when
|
||||
@@ -32,10 +33,10 @@ func (h *reactiveHandler) resolveQuietToggle(ctx context.Context, text string, s
|
||||
return "", false
|
||||
}
|
||||
val := "false"
|
||||
reply := "тихий режим выключен."
|
||||
reply := phraser.Ack(phraser.AckQuietOff, nil)
|
||||
if on {
|
||||
val = "true"
|
||||
reply = "тихий режим включён. буду реже напоминать."
|
||||
reply = phraser.Ack(phraser.AckQuietOn, nil)
|
||||
}
|
||||
if _, err := h.api.WriteFact(ctx, ipc.WriteFactReq{
|
||||
Ts: h.now(),
|
||||
@@ -46,7 +47,7 @@ func (h *reactiveHandler) resolveQuietToggle(ctx context.Context, text string, s
|
||||
Confidence: 1.0,
|
||||
}); err != nil {
|
||||
log.Printf("voice: write quiet_hours: %v", err)
|
||||
return "не получилось переключить тихий режим.", true
|
||||
return phraser.Ack(phraser.FailQuiet, nil), true
|
||||
}
|
||||
return reply, true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/kami/maven/internal/llm"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/voice"
|
||||
)
|
||||
@@ -29,12 +30,12 @@ func TestLLMReplierPassesTheModelReplyThrough(t *testing.T) {
|
||||
|
||||
func TestLLMReplierFallsBackToStubOnError(t *testing.T) {
|
||||
r := newLLMReplier(stubCompleter{err: errReplierTest}, nil)
|
||||
assertStub(t, r, router.Decision{Intent: router.IntentNote}, "llm error")
|
||||
assertAck(t, r, router.Decision{Intent: router.IntentNote}, phraser.AckNote, "llm error")
|
||||
}
|
||||
|
||||
func TestLLMReplierFallsBackToStubOnEmpty(t *testing.T) {
|
||||
r := newLLMReplier(stubCompleter{out: ""}, nil)
|
||||
assertStub(t, r, router.Decision{Intent: router.IntentNote}, "empty llm")
|
||||
assertAck(t, r, router.Decision{Intent: router.IntentNote}, phraser.AckNote, "empty llm")
|
||||
}
|
||||
|
||||
func TestLLMReplierClarifyUsesStub(t *testing.T) {
|
||||
@@ -42,6 +43,16 @@ func TestLLMReplierClarifyUsesStub(t *testing.T) {
|
||||
assertStub(t, r, router.Decision{Clarify: true}, "clarify")
|
||||
}
|
||||
|
||||
// assertAck — the stub picks between variants now, so two calls to it are not
|
||||
// expected to match. What must hold is that the reply is a line that entry can
|
||||
// produce, which is the same claim without pinning one wording.
|
||||
func assertAck(t *testing.T, r *llmReplier, d router.Decision, key, what string) {
|
||||
t.Helper()
|
||||
if got := r.Reply(d); !phraser.IsAck(key, nil, got) {
|
||||
t.Errorf("on %s: got %q, want a %q line", what, got, key)
|
||||
}
|
||||
}
|
||||
|
||||
func assertStub(t *testing.T, r *llmReplier, d router.Decision, what string) {
|
||||
t.Helper()
|
||||
got, want := r.Reply(d), voice.NewStubReplier().Reply(d)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
@@ -51,10 +52,10 @@ func (h *reactiveHandler) resolveSnooze(ctx context.Context, text string, src tu
|
||||
}
|
||||
if err := h.api.ResolveNudge(ctx, target.ID, store.NudgeSnoozed, now); err != nil {
|
||||
log.Printf("voice: snooze nudge %d (%s, %s): %v", target.ID, target.Rule, src, err)
|
||||
return "не получилось отложить.", true
|
||||
return phraser.Ack(phraser.FailSnooze, nil), true
|
||||
}
|
||||
log.Printf("voice: snoozed nudge %d (rule %s) from %s", target.ID, target.Rule, src)
|
||||
return "хорошо, вернусь к этому позже.", true
|
||||
return phraser.Ack(phraser.AckSnooze, nil), true
|
||||
}
|
||||
|
||||
// pendingNudge — the newest still-pending nudge sent inside snoozeWindow.
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
)
|
||||
|
||||
// TestFallbackPersona scores every line in fallbacks_ru_v1.json on the persona
|
||||
// checks the nudges are already held to. These lines are heard out loud, and
|
||||
// they live in a JSON file now, so a reworded variant that says "рад" or "вы"
|
||||
// would otherwise reach him with nothing between it and the speaker.
|
||||
// TestFallbackPersona scores every line in fallbacks_ru_v1.json and
|
||||
// ack_ru_v1.json on the persona checks the nudges already pass. These lines are
|
||||
// heard out loud and they live in a JSON file now, so a reworded variant that
|
||||
// says "рад" or "вы" would otherwise reach him with nothing in between.
|
||||
//
|
||||
// Only the persona checks run. Mood and topic belong to a nudge, and these are
|
||||
// not nudges: they are what she says when there is no answer.
|
||||
// not nudges.
|
||||
func TestFallbackPersona(t *testing.T) {
|
||||
fb, err := phraser.LoadFallbacks(rand.NewSource(20260804))
|
||||
if err != nil {
|
||||
@@ -24,13 +24,20 @@ func TestFallbackPersona(t *testing.T) {
|
||||
CheckLang: true, CheckFeminine: true, CheckHisGender: true,
|
||||
CheckAddress: true, CheckCringe: true, CheckLength: true,
|
||||
}
|
||||
variants := fb.Variants()
|
||||
ack, err := phraser.LoadAcks(rand.NewSource(20260804))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAcks: %v", err)
|
||||
}
|
||||
variants := append(fb.Variants(), ack.Variants()...)
|
||||
if len(variants) == 0 {
|
||||
t.Fatal("no variants — the file loaded empty")
|
||||
}
|
||||
for _, v := range variants {
|
||||
// {sources} stands for his own notes and never carries persona of its own.
|
||||
body := strings.ReplaceAll(v, "{sources}", "два литра")
|
||||
// The placeholders stand for his own words and carry no persona.
|
||||
body := v
|
||||
for _, ph := range []string{"{sources}", "{key}", "{value}", "{fn}", "{text}"} {
|
||||
body = strings.ReplaceAll(body, ph, "вода")
|
||||
}
|
||||
for _, r := range RunChecks(Case{}, body, "neutral") {
|
||||
if persona[r.Name] && !r.Pass {
|
||||
t.Errorf("%q fails %s: %s", v, r.Name, r.Detail)
|
||||
|
||||
@@ -55,3 +55,20 @@ func TestFallbacksDoNotRepeat(t *testing.T) {
|
||||
prev = got
|
||||
}
|
||||
}
|
||||
|
||||
// The acknowledgements load, fill his words into the frame, and answer from the
|
||||
// floor when the file is gone.
|
||||
func TestAcksLoad(t *testing.T) {
|
||||
a, err := LoadAcks(rand.NewSource(1))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAcks: %v", err)
|
||||
}
|
||||
got := a.Say(AckFactValue, map[string]string{"key": "вода", "value": "2л"})
|
||||
if !strings.Contains(got, "вода") || !strings.Contains(got, "2л") {
|
||||
t.Errorf("Say(%s) = %q, want his key and value in it", AckFactValue, got)
|
||||
}
|
||||
var nilAcks *Acks
|
||||
if got := nilAcks.Say(AckNote, nil); got != ackFloor[AckNote] {
|
||||
t.Errorf("nil Acks said %q, want the floor %q", got, ackFloor[AckNote])
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -25,7 +25,10 @@
|
||||
// the daemon seam (config wiring, no CoreAPI or voice-package change).
|
||||
package voice
|
||||
|
||||
import "github.com/kami/maven/internal/router"
|
||||
import (
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
// Replier — the reactive reply phrasing seam. The daemon's reactive handler
|
||||
// calls Reply with the router's Decision; the impl produces a terse reply
|
||||
@@ -60,27 +63,24 @@ func (s *StubReplier) Reply(d router.Decision) string {
|
||||
if !d.Slots.HasFn {
|
||||
return "не могу это сделать — не разобрала действие."
|
||||
}
|
||||
return "ок, записала действие: " + d.Slots.Fn
|
||||
return phraser.Ack(phraser.AckAct, map[string]string{"fn": d.Slots.Fn})
|
||||
case router.IntentReminder:
|
||||
if d.Slots.HasTime {
|
||||
return "напомню."
|
||||
}
|
||||
return "напомню."
|
||||
return phraser.Ack(phraser.AckReminder, nil)
|
||||
case router.IntentFact:
|
||||
if d.Slots.HasKey {
|
||||
if d.Slots.Value != "" {
|
||||
return "отметила: " + d.Slots.Key + " = " + d.Slots.Value
|
||||
return phraser.Ack(phraser.AckFactValue, map[string]string{"key": d.Slots.Key, "value": d.Slots.Value})
|
||||
}
|
||||
return "отметила: " + d.Slots.Key
|
||||
return phraser.Ack(phraser.AckFactKey, map[string]string{"key": d.Slots.Key})
|
||||
}
|
||||
return "записала факт."
|
||||
return phraser.Ack(phraser.AckFact, nil)
|
||||
case router.IntentNote:
|
||||
return "сохранила заметку."
|
||||
return phraser.Ack(phraser.AckNote, nil)
|
||||
case router.IntentQuery:
|
||||
return "поискала в заметках — ничего не нашла."
|
||||
case router.IntentChat:
|
||||
return "поговорили." // stub — LLMReplier replaces this
|
||||
default:
|
||||
return "приняла."
|
||||
return phraser.Ack(phraser.AckGeneric, nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user