package main import ( "regexp" "strings" "unicode" "github.com/kami/maven/internal/router" ) // latinRun matches a run of Latin-script words — the shape a service, host or // project name takes in a Russian sentence. Digits, dot, dash and underscore // ride along because "muzick-indexer" and "nginx.conf" are one name, not two. var latinRun = regexp.MustCompile(`[A-Za-z][A-Za-z0-9._-]*(?:\s+[A-Za-z][A-Za-z0-9._-]*)*`) // maxEntityReferences caps how many names one utterance may send to Nexus. The // cap is not about correctness, it is about one turn not fanning out into a // dozen HTTP calls when the utterance is a paragraph of English. const maxEntityReferences = 4 // hasLatin reports whether s carries a Latin letter. func hasLatin(s string) bool { for _, r := range s { if unicode.In(r, unicode.Latin) { return true } } return false } // entityReferences returns the names Nexus is asked to resolve, in the order // they were said. // // Normally there is one, and it is the router's Text slot — the verb phrase the // model wrote. But the resident model rewrites a Russian utterance as it routes, // and on the way it transliterates: "перезапусти muzick indexer" came back as // "перезагрузить музик индексер" (Vikunja #476). Nexus is then asked for a // service nobody has ever named, so the act cannot resolve its target even with // every gate open. // // The recovery is deliberately narrow. Only when the utterance holds a Latin run // and the model's Text holds none has a name certainly been rewritten. Anything // else keeps the Text slot, so an English utterance and a Russian entity name // are both untouched. Un-transliterating the Cyrillic back is not attempted: the // surface form he said is right there, and guessing at a reverse mapping would // invent a second name to be wrong about. // // What this does NOT do is pick. It used to return the longest run, and length // is a guess: "перезапусти nginx на muzick-indexer" has two names in it and the // longer one is not reliably the target. Nexus owns which names it knows // (docs/ecosystem.md — ambiguous resolution asks the owner, it does not pick), // so every run goes over and Nexus answers. Two runs that both resolve are a // clarify, not a coin toss. func entityReferences(dec router.Decision) []string { text := dec.Slots.Text if hasLatin(text) || !hasLatin(dec.Utterance) { return []string{text} } var refs []string seen := map[string]bool{} for _, m := range latinRun.FindAllString(dec.Utterance, -1) { m = strings.TrimSpace(m) // A single stray letter is not a name. if len(m) < 2 { continue } key := strings.ToLower(m) if seen[key] { continue } seen[key] = true refs = append(refs, m) if len(refs) == maxEntityReferences { break } } if len(refs) == 0 { return []string{text} } return refs }