Wire voice-tapped facts into entity resolution; fix phraser model config
WriteFactReq gains an optional Subject field (empty = old behavior, no CoreAPI signature change) and the IntentFact handler now passes the fact's key as its resolution subject, so voice-tapped facts flow into the Vikunja #279 enrichment queue automatically. Also: deploy/mavend.json's phraser was pointed at a 4B model with n_gpu_layers=99, which OOM'd under memory pressure and left a zombie llama-server child. Swapped to the 2B Qwen model matching the intended resident-model size, keeping GPU offload. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/ipc"
|
||||||
|
"github.com/kami/maven/internal/memory"
|
||||||
|
"github.com/kami/maven/internal/router"
|
||||||
|
"github.com/kami/maven/internal/tool"
|
||||||
|
"github.com/kami/maven/internal/voice"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestApplyAction_FactCapture_QueuesEntityResolution covers the wiring from
|
||||||
|
// Vikunja #279: a voice-tapped fact's key doubles as its entity-resolution
|
||||||
|
// subject, so writing a fact through applyAction (not directly through the
|
||||||
|
// store) must leave it in the fact-enrichment pending queue.
|
||||||
|
func TestApplyAction_FactCapture_QueuesEntityResolution(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
st := newTestStore(t)
|
||||||
|
api := ipc.NewStoreAPI(st)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
emb := router.NewHashEmbedder(1024)
|
||||||
|
matcher := tool.NewMatcher(api)
|
||||||
|
rtr := buildRouter(emb, matcher, 0.55, nil)
|
||||||
|
|
||||||
|
h := &reactiveHandler{
|
||||||
|
api: api,
|
||||||
|
embedder: emb,
|
||||||
|
router: rtr,
|
||||||
|
replier: voice.NewStubReplier(),
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
memStore: memory.NewInMemoryStore(),
|
||||||
|
dataStore: st,
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := router.Decision{
|
||||||
|
Intent: router.IntentFact,
|
||||||
|
Slots: router.Slots{Key: "the espresso machine", HasKey: true, Value: `"needs descaling"`},
|
||||||
|
}
|
||||||
|
h.applyAction(ctx, dec)
|
||||||
|
|
||||||
|
pending, err := st.PendingFactResolutions(ctx, 10)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PendingFactResolutions: %v", err)
|
||||||
|
}
|
||||||
|
if len(pending) != 1 {
|
||||||
|
t.Fatalf("expected 1 fact queued for entity resolution, got %d: %+v", len(pending), pending)
|
||||||
|
}
|
||||||
|
if pending[0].Subject != "the espresso machine" {
|
||||||
|
t.Fatalf("expected subject to carry the fact key, got %q", pending[0].Subject)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -550,6 +550,13 @@ func (h *reactiveHandler) applyAction(ctx context.Context, dec router.Decision)
|
|||||||
Value: dec.Slots.Value,
|
Value: dec.Slots.Value,
|
||||||
Source: "tap:voice",
|
Source: "tap:voice",
|
||||||
Confidence: 1.0,
|
Confidence: 1.0,
|
||||||
|
// 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)
|
factID, err := h.api.WriteFact(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
"state_dir": "/var/lib/maven",
|
"state_dir": "/var/lib/maven",
|
||||||
|
|
||||||
"phraser": {
|
"phraser": {
|
||||||
"model_path": "/opt/maven/models/llm/nemotron3-nano/NVIDIA-Nemotron-3-Nano-4B-UD-Q4_K_XL.gguf",
|
"model_path": "/opt/maven/models/llm/qwen3.5/Qwen3.5-2B-UD-Q4_K_XL.gguf",
|
||||||
"bin_path": "llama-server",
|
"bin_path": "llama-server",
|
||||||
"n_gpu_layers": 99,
|
"n_gpu_layers": 99,
|
||||||
"n_ctx": 2048,
|
"n_ctx": 2048,
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ type WriteFactReq struct {
|
|||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Confidence float64 `json:"confidence"`
|
Confidence float64 `json:"confidence"`
|
||||||
VoidsID *int64 `json:"voids_id,omitempty"`
|
VoidsID *int64 `json:"voids_id,omitempty"`
|
||||||
|
|
||||||
|
// Subject — free-text "who/what this fact is about" (e.g. "the espresso
|
||||||
|
// machine", "Kate"). Empty (the default, so old callers are unaffected)
|
||||||
|
// means the fact isn't about a resolvable entity. When set, the fact
|
||||||
|
// enrichment worker (cmd/mavend/factenrichment.go) later resolves it
|
||||||
|
// against Nexus into an entity_id — see Vikunja #279.
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// idReq — methods keyed by a single id.
|
// idReq — methods keyed by a single id.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (a *storeAPI) WriteFact(ctx context.Context, req WriteFactReq) (int64, erro
|
|||||||
if req.VoidsID != nil {
|
if req.VoidsID != nil {
|
||||||
voids = sql.NullInt64{Int64: *req.VoidsID, Valid: true}
|
voids = sql.NullInt64{Int64: *req.VoidsID, Valid: true}
|
||||||
}
|
}
|
||||||
id, err := a.s.WriteFact(ctx, req.Ts, store.FactKind(req.Kind), req.Key, req.Value, req.Source, req.Confidence, voids)
|
id, err := a.s.WriteFactAboutSubject(ctx, req.Ts, store.FactKind(req.Kind), req.Key, req.Subject, req.Value, req.Source, req.Confidence, voids)
|
||||||
return id, mapErr(err)
|
return id, mapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user