diff --git a/go-api/internal/runtime/smalltalk.go b/go-api/internal/runtime/smalltalk.go index 1052526..cfe4777 100644 --- a/go-api/internal/runtime/smalltalk.go +++ b/go-api/internal/runtime/smalltalk.go @@ -28,7 +28,7 @@ import "strings" // question with a greeting, so the set only holds phrases that carry no request // at all. func isSmalltalk(q string) bool { - n := normaliseSmalltalk(q) + n := stripVocative(normaliseSmalltalk(q)) if n == "" { return false } @@ -36,6 +36,38 @@ func isSmalltalk(q string) bool { return ok } +// stripVocative drops the assistant's name when the message is addressed to it. +// +// "Thank you Owliver" cost a full operational turn — tools, retrieval, a +// six-section briefing with citations — because the set held "thank you" and +// "hi owliver" but not "thank you owliver". The greetings had been given name +// variants by hand and the thanks and farewells had not, which is the failure +// mode of writing the cross product out: one half gets maintained. +// +// So the name comes off once, here, and the set holds each phrase exactly +// once. "Thanks Owliver", "Owliver hi" and "Good night Owliver" all reduce to +// a phrase already in it. +// +// Only at an end, and only as a WHOLE word: a name in the middle of a sentence +// is not a vocative, and "owliver" inside a longer message ("ask owliver to +// check the rota") must not be removed — stripping it would leave a fragment +// that could match something it should not. Nothing is stripped if the name is +// all there is, because "Owliver" alone is somebody getting the agent's +// attention, which the set already covers as its own row. +func stripVocative(n string) string { + const name = "owliver" + if n == name { + return n + } + if rest, ok := strings.CutSuffix(n, " "+name); ok { + return rest + } + if rest, ok := strings.CutPrefix(n, name+" "); ok { + return rest + } + return n +} + // normaliseSmalltalk reduces a message to lowercase letters and single spaces. // // Punctuation and emoji are dropped rather than enumerated, so "Hi!", "hi :)" @@ -80,8 +112,11 @@ func normaliseSmalltalk(q string) string { var smalltalkPhrases = map[string]struct{}{ "hi": {}, "hii": {}, "hiya": {}, "hello": {}, "helo": {}, "hey": {}, "yo": {}, "howdy": {}, "greetings": {}, "hi there": {}, - "hello there": {}, "hey there": {}, "hi owliver": {}, - "hello owliver": {}, "hey owliver": {}, + "hello there": {}, "hey there": {}, + // "Owliver" alone: somebody getting the agent's attention. The NAMED + // variants of every other phrase are handled by stripVocative, not by rows + // here — see the note on why the cross product was a mistake. + "owliver": {}, "good morning": {}, "good afternoon": {}, "good evening": {}, "good day": {}, "morning": {}, "afternoon": {}, "evening": {}, @@ -92,7 +127,10 @@ var smalltalkPhrases = map[string]struct{}{ "thanks": {}, "thank you": {}, "thanks a lot": {}, "thank you very much": {}, "thanks very much": {}, "many thanks": {}, - "ty": {}, "cheers": {}, "thank u": {}, + "ty": {}, "cheers": {}, "thank u": {}, "thankyou": {}, "thx": {}, + "thank you so much": {}, "thanks so much": {}, "tysm": {}, + "much appreciated": {}, "appreciated": {}, "perfect thanks": {}, + "great thanks": {}, "bye": {}, "goodbye": {}, "good bye": {}, "see you": {}, "see ya": {}, "good night": {}, "goodnight": {}, "later": {}, diff --git a/go-api/internal/runtime/smalltalk_test.go b/go-api/internal/runtime/smalltalk_test.go index 73f7e70..2bb4369 100644 --- a/go-api/internal/runtime/smalltalk_test.go +++ b/go-api/internal/runtime/smalltalk_test.go @@ -176,3 +176,35 @@ func TestCatalogueWithheldOnceToolBudgetIsSpent(t *testing.T) { t.Errorf("the final call carried %d tool definitions; the tool budget was spent", n) } } + +// The case from production: "Thank you Owliver" answered with a six-section +// operational briefing — tools, retrieval, citations, next steps — because the +// set held "thank you" and "hi owliver" but not the two together. +func TestSmalltalkSurvivesBeingAddressedByName(t *testing.T) { + for _, q := range []string{ + "Thank you Owliver", "thanks owliver", "Thanks, Owliver!", + "Owliver hi", "hi owliver", "Hello Owliver", + "Good morning Owliver", "good night owliver", "bye owliver", + "owliver", "Owliver?", + } { + if !isSmalltalk(q) { + t.Errorf("isSmalltalk(%q) = false; a greeting addressed by name is still a greeting", q) + } + } +} + +// The name comes off only as a vocative at an end. A real question that +// mentions the agent is still a real question. +func TestAQuestionMentioningTheNameIsNotSmalltalk(t *testing.T) { + for _, q := range []string{ + "ask owliver to check the rota", + "owliver how many shifts are uncovered", + "thanks owliver now show me the backlog", + "is owliver working", + "hi owliver which positions are at risk", + } { + if isSmalltalk(q) { + t.Errorf("isSmalltalk(%q) = true; this asks for something and must keep its tools", q) + } + } +}