Preserve meaning when cognitive model is unavailable #287

Merged
m merged 2 commits from codex/chat-semantic-inventory into main 2026-08-24 22:02:28 +00:00
4 changed files with 67 additions and 7 deletions
+4 -2
View File
@@ -186,7 +186,7 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string)
}
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
text := "Я — Домовой, ваш помощник. Помогаю разбираться в вопросах, искать информацию и выполнять задачи."
text := "Проверю запрос доступным маршрутом и сообщу результат."
switch request.Meaning.InformationRequestKind {
case interaction.InformationRequestCognitiveModelInventory:
text = "Проверю подключённые модели. Ключи и пароли не показываю."
@@ -201,7 +201,9 @@ func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.Gateway
case "REQUEST_TO_INVESTIGATE":
text = "Понял задачу. Проверю доступный путь и сообщу результат."
case "QUESTION":
text = "Я — Домовой, ваш помощник. Помогаю разбираться в вопросах, искать информацию и выполнять задачи."
text = "Проверю вопрос доступным источником и отвечу по существу."
case "CONVERSATIONAL":
text = "Я — Домовой, ваш помощник."
}
}
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
@@ -196,7 +196,7 @@ func TestBuiltInAdvisoryIdentityIsNaturalAndDoesNotMentionResponseLength(t *test
RequestID: "request-identity",
Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION"},
})
if response.AdvisoryText != "Я — Домовой, ваш помощник. Помогаю разбираться в вопросах, искать информацию и выполнять задачи." {
if response.AdvisoryText != "Проверю вопрос доступным источником и отвечу по существу." {
t.Fatalf("unexpected identity response: %q", response.AdvisoryText)
}
if strings.Contains(response.AdvisoryText, "коротко") || strings.Contains(response.AdvisoryText, "подробност") {
+47 -4
View File
@@ -214,7 +214,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo
questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}}
conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"}
investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри", "попроб", "повтор", "снов"}
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"}
directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь"}
explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"}
preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"}
hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"}
@@ -472,13 +472,56 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
// place, person or one-off question branch; when the cue is absent the
// truth-first fallback remains in force.
func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []string) bool {
inquiry := primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate
inquiry := primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate || (primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "найд", "поищ", "подбер"))
if !inquiry {
return false
}
currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо")
observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок")
return currentCue || observedStateCue
observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2)
placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац")
return currentCue || observedStateCue || placeSearchCue
}
// approximateToken catches ordinary keyboard slips without turning routing
// into a list of complete user questions. It is only used for broad
// source-dependent concepts such as weather.
func approximateToken(tokens []string, target string, maxDistance int) bool {
for _, token := range tokens {
if editDistance([]rune(token), []rune(target)) <= maxDistance {
return true
}
}
return false
}
func editDistance(left, right []rune) int {
previous := make([]int, len(right)+1)
for index := range previous {
previous[index] = index
}
for i, leftRune := range left {
current := make([]int, len(right)+1)
current[0] = i + 1
for j, rightRune := range right {
cost := 0
if leftRune != rightRune {
cost = 1
}
current[j+1] = minInt(current[j]+1, previous[j+1]+1, previous[j]+cost)
}
previous = current
}
return previous[len(right)]
}
func minInt(values ...int) int {
result := values[0]
for _, value := range values[1:] {
if value < result {
result = value
}
}
return result
}
func informationRequestKind(message string, candidate interaction.SemanticCandidate) interaction.InformationRequestKind {
+15
View File
@@ -154,6 +154,21 @@ func TestSourceDependentQuestionRequestsWebResearchWithoutEntityBranch(t *testin
}
}
func TestSourceDependentQuestionToleratesTyposAndPlaceSearch(t *testing.T) {
for _, message := range []string{"какая пугода в Сочи", "найди ближайший ресторан в Сочи"} {
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
seen := false
for _, hint := range candidate.CapabilityHints {
if hint == "WEB_RESEARCH" {
seen = true
}
}
if !seen {
t.Fatalf("message=%q was not routed to WEB_RESEARCH: %+v", message, candidate)
}
}
}
func TestRequestToClarifyCurrentFactRequestsWebResearch(t *testing.T) {
candidate := BuildDeterministicCandidate("уточни погоду в питере", "", time.Unix(10, 0).UnixMilli())
for _, hint := range candidate.CapabilityHints {