Recognise smalltalk that is addressed by name

"Thank you Owliver" was answered with a six-section operational briefing:
decision backlog, screening backlog, two at-risk roles, shift coverage,
overtime, withdrawals, five numbered next steps and three policy citations.
Tools were called and the corpus was retrieved, for a message that said thanks.

The set held "thank you" and it held "hi owliver". It did not hold the two
together, because the name variants had been written out by hand for the
greetings and never for the thanks or the farewells. That is the failure mode
of enumerating a cross product: one half gets maintained and the other half
silently does not, and nothing points at the gap.

So the name comes off once, in stripVocative, and the set holds each phrase
exactly once. "Thanks Owliver", "Owliver hi" and "Good night Owliver" all
reduce to a row that already existed. The three hand-written "hi owliver" rows
are gone; "owliver" alone stays, since that is somebody getting the agent's
attention rather than a phrase with a name attached.

Stripped only at an end and only as a whole word. "ask owliver to check the
rota" and "hi owliver which positions are at risk" keep their tools and their
evidence — a message that asks for something is not smalltalk however politely
it opens. Both directions are tested.

Also adds the thanks nobody had written down yet: thankyou, thx, tysm, thank
you so much, much appreciated, perfect thanks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 15:44:15 +05:30
parent d242329e50
commit 98025f3980
2 changed files with 74 additions and 4 deletions

View File

@@ -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": {},

View File

@@ -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)
}
}
}