Phase 8 follow-up: resolve the child named in the question
The Hub forwards `person` equal to the asker (`PersonOrDefault`), so the spoken question "what does Harry have on today at school?" arrives with person "sam" and returned "no data for that person" (found by the Hub agent on AUD-116). resolveChild now falls back to the question text when the requested person is not one of this project's children: whole-word, case-insensitive match against the child names this service already publishes in /capabilities. An explicit person still wins, two named children still return answer:null, and the permission check still runs on the resolved child. No model, no free text. Live end-to-end through the Hub, with no person field: POST http://192.168.20.13:8788/ask {"text":"what does Harry have on today at school?","asker":"sam","source":"console"} 200 {"status":"ok", "answer":"Harry has 8 school events today from 8:55am to 6:00pm and 6 overdue tasks.", "route":{"kind":"project","project":"compass_nhs"}} Image 0.6.1-phase8; docs/phase-8-hub-notes.md records the fallback and the end-to-end transcript. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"gitea.lab.audasmedia.com.au/sam/compass_teams_nhs/internal/dashboard"
|
||||
"gitea.lab.audasmedia.com.au/sam/compass_teams_nhs/internal/store"
|
||||
@@ -156,7 +157,7 @@ func (a *Answerer) Answer(ctx context.Context, req AskRequest) AskResponse {
|
||||
if err != nil {
|
||||
return cannotAnswer("the school data could not be read")
|
||||
}
|
||||
child := findChild(children, person)
|
||||
child := resolveChild(children, person, req.Question)
|
||||
if child == nil {
|
||||
return cannotAnswer("no data for that person")
|
||||
}
|
||||
@@ -415,6 +416,55 @@ func findChild(children []store.Child, name string) *store.Child {
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveChild picks the child a question is about.
|
||||
//
|
||||
// The contract makes `person` optional and says it defaults to `asker`. The Hub
|
||||
// (as of AUD-116) forwards `person = asker`, so a spoken question such as "what
|
||||
// does Harry have on today at school?" arrives as person "sam". This project is
|
||||
// the expert on its own data: when the requested person is not one of its
|
||||
// children, it looks for exactly one child named in the question text. The name
|
||||
// must be one the service already publishes in /capabilities, the match is
|
||||
// whole-word and case-insensitive, and the permission check still runs
|
||||
// afterwards, so nothing is guessed and no data leaks between children.
|
||||
func resolveChild(children []store.Child, person, question string) *store.Child {
|
||||
if c := findChild(children, person); c != nil {
|
||||
return c
|
||||
}
|
||||
words := wordsIn(question)
|
||||
var named []*store.Child
|
||||
for i := range children {
|
||||
if words[normalize(children[i].Name)] {
|
||||
named = append(named, &children[i])
|
||||
}
|
||||
}
|
||||
if len(named) == 1 {
|
||||
return named[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// wordsIn lower-cases a question and splits it into whole words, so "harry"
|
||||
// matches in "what does Harry have on?" but never inside a longer word.
|
||||
func wordsIn(s string) map[string]bool {
|
||||
out := map[string]bool{}
|
||||
var b strings.Builder
|
||||
flush := func() {
|
||||
if b.Len() > 0 {
|
||||
out[b.String()] = true
|
||||
b.Reset()
|
||||
}
|
||||
}
|
||||
for _, r := range s {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
continue
|
||||
}
|
||||
flush()
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
|
||||
func onlyType(items []store.Item, typ string) []store.Item {
|
||||
out := make([]store.Item, 0, len(items))
|
||||
for _, it := range items {
|
||||
|
||||
Reference in New Issue
Block a user