4d94277836
router.ClaimOf maps a Decision onto the common unit. Stage 0 is anchored, the LLM path is structural, the classifier is nearest, and anything with a structural hole is vetoed whoever produced it. The veto recovers the reason gateLLMDecision throws away. Folding three named holes into llmThinConfidence leaves 0.3, which says something was wrong and never which thing, so the same conditions are read here as sentences a trace can print. Nothing in Route calls this. Decision.Confidence keeps its float and keeps working, because r.threshold and gateLLMDecision read it and the classifier is the failure floor. TestClaimOfLeavesTheDecisionAlone asserts that. TestONNXBaseline is unchanged at 64/91.
114 lines
4.1 KiB
Go
114 lines
4.1 KiB
Go
package router
|
|
|
|
import "github.com/kami/maven/internal/claim"
|
|
|
|
// ClaimOf — build a claim.Claim from a Decision (V-565, design in
|
|
// docs/plans/19-dialogue-arbitration.md).
|
|
//
|
|
// Additive and beside the existing path. Decision.Confidence keeps its float
|
|
// and keeps working: r.threshold and gateLLMDecision read it, and the
|
|
// classifier cascade is the failure floor. Nothing in Route calls this yet.
|
|
// The arbiter that reads claims is V-560.
|
|
//
|
|
// claimant names who produced the decision. The cascade does not record which
|
|
// stage-0 grammar matched, so the caller passes what it knows and the builder
|
|
// does not guess.
|
|
func ClaimOf(claimant string, d Decision) claim.Claim {
|
|
consumed, unexplained := claim.Split(d.Utterance, claimSpans(d)...)
|
|
return claim.Claim{
|
|
Claimant: claimant,
|
|
Intent: string(d.Intent),
|
|
Filled: filledSlots(d.Slots),
|
|
Consumed: consumed,
|
|
Unexplained: unexplained,
|
|
Band: bandOf(d),
|
|
Veto: vetoOf(d),
|
|
}
|
|
}
|
|
|
|
// claimSpans — the parts of the utterance the decision says it read. Slot
|
|
// values, not the utterance, because coverage is the question of how much of
|
|
// the sentence the claim actually explains.
|
|
//
|
|
// A stage-0 grammar reports whatever its Build put in the slots, which for the
|
|
// reminder rule is the text after "напомни" and not the verb itself. That
|
|
// under-reports coverage rather than over-reporting it, which is the safe
|
|
// direction: a claim that overstates what it explains wins arbitrations it
|
|
// should lose.
|
|
func claimSpans(d Decision) []string {
|
|
spans := []string{d.Slots.Text, d.Slots.Key, d.Slots.Value, d.Slots.Fn}
|
|
return append(spans, d.Slots.Args...)
|
|
}
|
|
|
|
// filledSlots — the slot names this decision would fill. Text counts only when
|
|
// it differs from the whole utterance: fillSlots backfills the raw utterance
|
|
// into Text for a note, a query and a chat turn, so a set Text is not by itself
|
|
// evidence that anything was extracted.
|
|
func filledSlots(s Slots) []string {
|
|
var out []string
|
|
if s.HasTime {
|
|
out = append(out, "time")
|
|
}
|
|
if s.HasFn {
|
|
out = append(out, "fn")
|
|
}
|
|
if s.HasKey {
|
|
out = append(out, "key")
|
|
}
|
|
if s.Text != "" {
|
|
out = append(out, "text")
|
|
}
|
|
return out
|
|
}
|
|
|
|
// bandOf — which kind of evidence this decision rests on.
|
|
//
|
|
// Stage 0 is anchored: a literal pattern matched and its span decided the
|
|
// intent. The LLM path (stage 1) is structural: the model read the whole
|
|
// sentence, and gateLLMDecision already checked the route for structural
|
|
// holes. The classifier (stages 2 and 3) is nearest, and the measurement is why
|
|
// it is one band rather than a scale — on the 91-case RU fixture its cosine
|
|
// spans 0.859 to 0.942 and scores 62% at both ends.
|
|
//
|
|
// A decision carrying a veto lands in BandVetoed regardless of who produced it.
|
|
// That is the point of the band: a self-vetoed claim should lose to any claim
|
|
// that is not, whatever machinery built it.
|
|
func bandOf(d Decision) claim.Band {
|
|
if vetoOf(d) != "" {
|
|
return claim.BandVetoed
|
|
}
|
|
switch d.Stage {
|
|
case 0:
|
|
return claim.BandAnchored
|
|
case 1:
|
|
return claim.BandStructural
|
|
default:
|
|
return claim.BandNearest
|
|
}
|
|
}
|
|
|
|
// vetoOf — why this decision should not win, recovered as a reason rather than
|
|
// a number.
|
|
//
|
|
// gateLLMDecision flattens three named structural holes into
|
|
// llmThinConfidence, and the reason is lost at that point: 0.3 tells a reader
|
|
// that something was wrong and never which thing. The same three conditions are
|
|
// checked here so the claim carries the sentence a trace can print and the
|
|
// owner can be told.
|
|
//
|
|
// Clarify is checked last and is the general case. A decision below threshold
|
|
// has already asked to be doubted, whichever path set it.
|
|
func vetoOf(d Decision) string {
|
|
switch {
|
|
case d.Intent == IntentFact && !d.Slots.HasKey:
|
|
return "fact with no key: nothing to write, or a confident write under the wrong key"
|
|
case d.Intent == IntentAct && !d.Slots.HasFn:
|
|
return "act with no allowlisted fn: running an unlisted command or silently doing nothing"
|
|
case d.Intent == IntentReminder && !reminderHasSubject(d.Slots.Text):
|
|
return "reminder with no subject: it would fire empty at the hour"
|
|
case d.Clarify:
|
|
return "below the confidence gate"
|
|
}
|
|
return ""
|
|
}
|