AUD-117: derive person from the question via project people list

Decode the people list from each project's /capabilities and set the /ask
person field from a whole-word, case-insensitive name in the question when
the caller did not name a person. Match only the routed project's people,
so a name from another project does not leak across. Fall back to asker
when no name matches.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 19:54:26 +11:00
parent ac7e6abbf9
commit 1d079a8bf1
9 changed files with 273 additions and 28 deletions

37
src/understand/person.go Normal file
View File

@@ -0,0 +1,37 @@
package understand
import (
"regexp"
"strings"
)
// MatchPerson returns the household name from people that appears as a whole
// word in the question, or "" when no name matches.
//
// It is deterministic: no LLM is in this path (AGENTS.md rule 4). The Hub uses
// it to set the /ask person field from the question when the caller did not
// name a person. A caller-supplied person always wins; the caller does not call
// this function in that case.
//
// The match is case-insensitive and whole-word, so "harry" matches "What does
// Harry have on today?" but not "harrypotter". The matched name is returned as
// the project published it, trimmed.
func MatchPerson(question string, people []string) string {
if strings.TrimSpace(question) == "" || len(people) == 0 {
return ""
}
q := strings.ToLower(question)
for _, p := range people {
name := strings.TrimSpace(p)
if name == "" {
continue
}
// \b is a word boundary in Go's RE2. QuoteMeta keeps a name that
// holds a regexp character literal.
re := regexp.MustCompile(`\b` + regexp.QuoteMeta(strings.ToLower(name)) + `\b`)
if re.MatchString(q) {
return name
}
}
return ""
}