voice/routing: fix time-query misroute (seed collision, threshold, stage-0 grammars)
three bugs causing time queries to land on reminder or fact intent: - seed collision: query.txt and system.txt shared identical time/date seeds (который час, сколько времени), making system intent indistinguishable from query intent in centroid space - threshold (0.35) too low for ONNX embedder — cosine similarities cluster 0.5-0.7 for related intents, so Clarify never fired - reminder centroid contaminated by time-lexicon (every seed has a time expression), pulling any time-word utterance toward reminder intent fixes: - remove 3 duplicate time/date seeds from query.txt (keep in system.txt) - DefaultRouterThreshold 0.35 -> 0.55 - stage-0 grammar for напомни/remind me -> IntentReminder, bypasses classifier (fixes 'напомни через час' being misrouted to fact) - stage-0 grammars for time/date system queries (сколько времени, который час, какой сегодня день) -> IntentSystem, with Build filter to exclude elapsed/duration queries (сколько времени прошло) - time parser fallback in applyAction for stage-0 reminder matches (extractor doesn't run on stage-0 decisions)
This commit is contained in:
@@ -320,7 +320,7 @@ const (
|
||||
DefaultTickInterval = 60 * time.Second
|
||||
DefaultRepeatInterval = 5 * time.Minute
|
||||
DefaultAutotuneInterval = 10 * time.Minute
|
||||
DefaultRouterThreshold = 0.35
|
||||
DefaultRouterThreshold = 0.55
|
||||
DefaultQueryMinScore = 0.55
|
||||
DefaultToolTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
@@ -53,3 +53,102 @@ func DefaultGrammars(actMatcher ActMatcher) []Grammar {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// --- напомни / remind me stage-0 grammar ---
|
||||
//
|
||||
// "напомни через час выпить воды" / "remind me in 30 minutes to water plants"
|
||||
// routes directly to IntentReminder, bypassing the classifier entirely.
|
||||
// Without this grammar the reminder centroid (dense with time-lexicon) pulls
|
||||
// non-reminder time queries toward it, and the verb+action overlap pushes
|
||||
// actual reminders toward fact — a double contamination. Stage 0 fixes both.
|
||||
//
|
||||
// The grammar captures the part after "напомни"/"remind me" into Slots.Text
|
||||
// so the daemon's time parser can extract the fire time from it. The grammar
|
||||
// itself does NOT parse time — that's the extractor's job (stage 2), but
|
||||
// stage 0 skips the extractor. The daemon's applyAction fallback calls the
|
||||
// time parser for stage-0 reminders that arrive without HasTime.
|
||||
func ReminderGrammar() Grammar {
|
||||
return Grammar{
|
||||
Name: "reminder-wakeword",
|
||||
Pattern: regexp.MustCompile(`(?i)^\s*(?:напомни|remind me)[\s,:]+(.+)$`),
|
||||
Build: func(m []string) (Decision, bool) {
|
||||
rest := strings.TrimSpace(m[1])
|
||||
if rest == "" {
|
||||
return Decision{}, false
|
||||
}
|
||||
return Decision{
|
||||
Stage: 0,
|
||||
Intent: IntentReminder,
|
||||
Confidence: 1.0,
|
||||
Slots: Slots{Text: rest},
|
||||
}, true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SystemTimeDateGrammars — stage-0 grammars for high-frequency system queries
|
||||
// that replySystem handles deterministically (time, date, day-of-week).
|
||||
// "сколько времени" is seeded in BOTH system.txt and query.txt (a centroid
|
||||
// collision), and the reminder centroid contaminates any utterance with time
|
||||
// vocabulary. These grammars route directly to IntentSystem, skipping the
|
||||
// classifier entirely — the answer is always deterministic.
|
||||
//
|
||||
// The time-query grammar uses a broad pattern (prefix match) with a Build
|
||||
// filter: utterances containing "прошло"/"осталось" or starting with "до"
|
||||
// after the time expression are elapsed/duration queries that belong to the
|
||||
// classifier, not to replySystem's "what time is it" handler.
|
||||
func SystemTimeDateGrammars() []Grammar {
|
||||
return []Grammar{
|
||||
{
|
||||
Name: "time-query",
|
||||
Pattern: regexp.MustCompile(`(?i)^\s*сколько\s+(сейчас\s+)?времени(.*)$`),
|
||||
Build: timeQueryBuild,
|
||||
},
|
||||
{
|
||||
Name: "clock-query",
|
||||
Pattern: regexp.MustCompile(`(?i)^\s*который\s+(сейчас\s+)?час(\s+у\s+нас|\s+в\s+\w+)?\s*\??\s*$`),
|
||||
Build: timeDateBuild,
|
||||
},
|
||||
{
|
||||
Name: "date-query",
|
||||
Pattern: regexp.MustCompile(`(?i)^\s*(?:какой\s+сегодня\s+(?:день|день\s+недели|число)|какое\s+сегодня\s+число)\s*\??\s*$`),
|
||||
Build: timeDateBuild,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// timeQueryBuild — Build for the time-query grammar. Returns ok=false for
|
||||
// elapsed/duration queries ("сколько времени прошло", "сколько времени
|
||||
// осталось", "сколько времени до") so they fall through to the classifier.
|
||||
// The classifier handles them as query intent (notes RAG), not system.
|
||||
func timeQueryBuild(m []string) (Decision, bool) {
|
||||
suffix := strings.TrimSpace(m[2])
|
||||
if suffix != "" && !strings.HasPrefix(suffix, "?") {
|
||||
lower := strings.ToLower(suffix)
|
||||
// If the first word after "времени" is a duration marker, this is an
|
||||
// elapsed-time query, not a "what time is it" query.
|
||||
firstWord := strings.Fields(lower)
|
||||
if len(firstWord) > 0 {
|
||||
switch firstWord[0] {
|
||||
case "прошло", "осталось", "до", "пройдет", "минуло", "проходит":
|
||||
return Decision{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return Decision{
|
||||
Stage: 0,
|
||||
Intent: IntentSystem,
|
||||
Confidence: 1.0,
|
||||
}, true
|
||||
}
|
||||
|
||||
// timeDateBuild — shared Build for clock-query and date-query grammars. Returns a
|
||||
// Decision routed to IntentSystem with the original utterance intact, so the
|
||||
// daemon's replySystem handler can keyword-match and answer it.
|
||||
func timeDateBuild(m []string) (Decision, bool) {
|
||||
return Decision{
|
||||
Stage: 0,
|
||||
Intent: IntentSystem,
|
||||
Confidence: 1.0,
|
||||
}, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user