4dfe106fe3
IntentFact used to persist whatever the model invented for a question-shaped utterance, at confidence 1.00, and index it for recall under the question's own text. Two such rows then claimed seven unrelated world questions and silently disabled world answering. A question now goes down the query chain, which is what he asked for. The second half is confidence: a value grounded in what he said stays 1.00, a value the model supplied for words he never said drops to 0.60 and says so in the log. Same reasoning as 'LLM output is not authorization' on the act path.
92 lines
3.9 KiB
Go
92 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"strconv"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// actionFact handles router.IntentFact: persist a tapped self-fact, index
|
|
// 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 "не разобрала, что записать — попробуй иначе."
|
|
}
|
|
// A question is never a fact about him (#470). "какая последняя версия
|
|
// языка Go?" used to land here, and the value stored was whatever the
|
|
// model invented for it, at confidence 1.00, indexed for recall under the
|
|
// question's own text. Two such rows then claimed seven unrelated world
|
|
// questions through recall and silently disabled world answering.
|
|
//
|
|
// The routing error itself is not fixed here — the answer is to answer.
|
|
// Sending the turn down the query chain is what he asked for anyway, and
|
|
// it costs a mis-routed capture nothing: an explicit "запиши ..." is not
|
|
// question-shaped, so it never takes this branch.
|
|
if router.IsQuestionShaped(dec.Utterance) {
|
|
log.Printf("voice: fact write refused, utterance is a question: %q (key %q) — answering as a query",
|
|
dec.Utterance, dec.Slots.Key)
|
|
q := dec
|
|
q.Intent = router.IntentQuery
|
|
// The key the model extracted is its guess at what to store, not a
|
|
// fact he has. Left in place, queryFactByKey would read it back and
|
|
// claim the turn before any real source ran.
|
|
q.Slots.Key, q.Slots.HasKey = "", false
|
|
q.Slots.Value = ""
|
|
return h.actionQuery(ctx, q)
|
|
}
|
|
now := h.now()
|
|
req := ipc.WriteFactReq{
|
|
Ts: now,
|
|
Kind: "self",
|
|
Key: dec.Slots.Key,
|
|
Value: dec.Slots.Value,
|
|
Source: "tap:voice",
|
|
// Not 1.00 unconditionally any more (#470). A value he said is
|
|
// evidence; a value the model supplied for words he never said is a
|
|
// guess, and writing a guess at full confidence is the same mistake
|
|
// the act path already refuses under "LLM output is not
|
|
// authorization".
|
|
Confidence: factConfidence(dec.Utterance, dec.Slots.Value),
|
|
// Subject: the key doubles as the entity-resolution candidate —
|
|
// a voice-tapped fact's key is usually the thing/person it's
|
|
// about ("espresso_machine", "kate"), so queueing it for Nexus
|
|
// resolution costs one async lookup and is a no-op (not_found)
|
|
// for the abstract self-state keys (mood, water) that aren't
|
|
// entities at all.
|
|
Subject: dec.Slots.Key,
|
|
}
|
|
factID, err := h.api.WriteFact(ctx, req)
|
|
if err != nil {
|
|
log.Printf("voice: write fact: %v", err)
|
|
return "не получилось сохранить факт."
|
|
}
|
|
// Index the fact utterance 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 for them — "когда я пил воду?" reads back from here.
|
|
if h.memStore != nil {
|
|
if vec, err := router.EmbedPassage(ctx, h.embedder, dec.Utterance); err != nil {
|
|
log.Printf("voice: embed fact for memory: %v", err)
|
|
} else if err := h.memStore.Insert(ctx, "fact:"+dec.Slots.Key+":"+strconv.FormatInt(now.Unix(), 10), vec, map[string]string{
|
|
"source": "voice",
|
|
"type": "fact",
|
|
"text": dec.Utterance,
|
|
"ts": strconv.FormatInt(now.Unix(), 10),
|
|
}); err != nil {
|
|
log.Printf("voice: memory insert fact: %v", err)
|
|
}
|
|
}
|
|
// Event extraction + pattern detection (best-effort, must not fail the
|
|
// fact write). If the fact describes a recognizable action, it becomes a
|
|
// normalized event; if ≥3 events for the same action+object show stable
|
|
// intervals, a proposed routine is created and parked for confirmation.
|
|
if h.dataStore != nil {
|
|
if phrase := h.detectPattern(ctx, factID, dec.Slots.Key, dec.Slots.Value, now); phrase != "" {
|
|
return phrase // "ты заправляешь ... напоминать?"
|
|
}
|
|
}
|
|
return "" // replier phrases the success reply
|
|
}
|