Handle household typos through semantic cognition #395
@@ -91,7 +91,7 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты отвечаешь как Домовой на обычный вопрос пользователя. Сначала пойми цель вопроса и верни короткий естественный ответ по устойчивому общеизвестному знанию. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты.\n\nВопрос пользователя:\n" + userMessage
|
||||
prompt := "Ты отвечаешь как Домовой на обычный вопрос пользователя. Сначала пойми цель вопроса и верни короткий естественный ответ по устойчивому общеизвестному знанию. Начинай сразу с ответа и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты.\n\nВопрос пользователя:\n" + userMessage
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
|
||||
@@ -860,6 +860,17 @@ func main() {
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
if planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
|
||||
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, personalMemory.ContextText(), cognitive.TaskMeaning{
|
||||
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
})
|
||||
cancelAnswer()
|
||||
if answerErr == nil && stableKnowledgeAnswer(answer) {
|
||||
planningReply = answer
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
|
||||
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
||||
needsCognition = false
|
||||
|
||||
@@ -612,7 +612,7 @@ func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []
|
||||
currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо")
|
||||
observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2)
|
||||
placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац", "бренд", "товар", "артикул", "продавец", "маркетплейс", "wildberries", "wb")
|
||||
travelSearchCue := containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт", "отправлен", "прибыт", "расстоян", "километр", "добрат", "маршрут")
|
||||
travelSearchCue := travelCue(tokens) || containsStemInTokens(tokens, "отправлен", "прибыт", "расстоян", "километр", "добрат", "маршрут")
|
||||
timeDifferenceCue := containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час")
|
||||
return currentCue || observedStateCue || placeSearchCue || travelSearchCue || timeDifferenceCue
|
||||
}
|
||||
@@ -622,13 +622,33 @@ func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []
|
||||
// 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 {
|
||||
if nearToken(token, target, maxDistance) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nearToken(token, target string, maxDistance int) bool {
|
||||
if editDistance([]rune(token), []rune(target)) <= maxDistance {
|
||||
return true
|
||||
}
|
||||
if maxDistance < 1 {
|
||||
return false
|
||||
}
|
||||
left, right := []rune(token), []rune(target)
|
||||
if len(left) != len(right) || len(left) < 2 {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == right[index] {
|
||||
continue
|
||||
}
|
||||
return index+1 < len(left) && left[index] == right[index+1] && left[index+1] == right[index] && string(left[index+2:]) == string(right[index+2:])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func editDistance(left, right []rune) int {
|
||||
previous := make([]int, len(right)+1)
|
||||
for index := range previous {
|
||||
|
||||
@@ -50,9 +50,11 @@ func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t
|
||||
}
|
||||
|
||||
func TestPlaneRequestReceivesTravelResearchCapability(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("найди ближайший самолет до Казани", "", 100)
|
||||
if !containsToken(candidate.CapabilityHints, "WEB_RESEARCH") {
|
||||
t.Fatalf("plane request did not select web research: %+v", candidate)
|
||||
for _, message := range []string{"найди ближайший самолет до Казани", "найди сомалет до Казани"} {
|
||||
candidate := BuildDeterministicCandidate(message, "", 100)
|
||||
if !containsToken(candidate.CapabilityHints, "WEB_RESEARCH") {
|
||||
t.Fatalf("plane request did not select web research for %q: %+v", message, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,14 @@ func TestTravelResearchQueryUsesKnownOriginForTrainSchedule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTravelResearchQueryNormalizesNearUnambiguousTravelTypo(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||||
got := TravelResearchQuery("найди сомалет до Казани", history, time.Time{})
|
||||
if !strings.Contains(got, "самолёт") || !strings.Contains(got, "пункт отправления: Москве") {
|
||||
t.Fatalf("travel typo was not normalized with remembered route context: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndependentRequestPartsKeepsCoordinatedEntitiesTogether(t *testing.T) {
|
||||
if got := IndependentRequestParts("какая разница во времени между Москвой и Шанхаем?"); len(got) != 1 {
|
||||
t.Fatalf("coordinated entities became independent requests: %#v", got)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// TravelSearchQuery carries the implicit current-day meaning of a nearest
|
||||
// flight request into the research adapter.
|
||||
func TravelSearchQuery(message string, now time.Time) string {
|
||||
cleaned := strings.TrimSpace(StripAssistantVocative(message))
|
||||
cleaned := normalizedTravelSearchText(message)
|
||||
tokens := semanticTokens(cleaned)
|
||||
if !travelCue(tokens) || !containsStemInTokens(tokens, "ближ") || isTravelDateSpecified(tokens) {
|
||||
return cleaned
|
||||
@@ -21,6 +21,25 @@ func TravelSearchQuery(message string, now time.Time) string {
|
||||
return cleaned + " сегодня " + now.Format("02.01.2006")
|
||||
}
|
||||
|
||||
// normalizedTravelSearchText corrects only a near-unambiguous typo in a
|
||||
// capability word before an external query. The original utterance is kept by
|
||||
// Conversation; this is a replaceable search-skill normalization, not a fact
|
||||
// about a user, city or provider.
|
||||
func normalizedTravelSearchText(message string) string {
|
||||
tokens := semanticTokens(StripAssistantVocative(message))
|
||||
changed := false
|
||||
for index, token := range tokens {
|
||||
if nearToken(token, "самолет", 2) || nearToken(token, "самолёт", 2) {
|
||||
tokens[index] = "самолёт"
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return strings.TrimSpace(StripAssistantVocative(message))
|
||||
}
|
||||
return strings.Join(tokens, " ")
|
||||
}
|
||||
|
||||
// TravelResearchQuery keeps route and distance questions tied to a known
|
||||
// origin before they reach a generic web search. The history is semantic
|
||||
// context, not a list of example-specific answers.
|
||||
@@ -419,7 +438,7 @@ func capitalizeWord(value string) string {
|
||||
}
|
||||
|
||||
func travelCue(tokens []string) bool {
|
||||
return containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт")
|
||||
return containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт") || approximateToken(tokens, "самолет", 2) || approximateToken(tokens, "самолёт", 2)
|
||||
}
|
||||
|
||||
func trainScheduleRequest(tokens []string) bool {
|
||||
|
||||
Reference in New Issue
Block a user