diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 0051039..d939408 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -250,6 +250,9 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo if explicitCorrection { scores[interaction.SpeechActCorrection] += 6 } + if containsStemInTokens(tokens, "опечат", "ошибк", "исправлен") && containsStemInTokens(tokens, "исправ", "поправ") { + scores[interaction.SpeechActCorrection] += 6 + } first := "" if len(tokens) > 0 { first = tokens[0] @@ -496,19 +499,35 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin add("WEB_RESEARCH") } if isCreativeRequestTokens(tokens) { - if isImageCreationRequestTokens(tokens) { + // A correction that quotes a creative phrase is still a correction; + // do not execute the quoted phrase as a side effect. + if isImageCreationRequestTokens(tokens) && primary != interaction.SpeechActCorrection && secondary != interaction.SpeechActCorrection { add("IMAGE_GENERATION") } else { add("COGNITIVE_REASONING") } } + if containsStemInTokens(tokens, "голосов", "аудио", "послуш") { + add("SPEECH_TO_TEXT") + } + if containsStemInTokens(tokens, "фотограф", "фото", "картин", "изображ") && containsStemInTokens(tokens, "прочит", "текст", "надпис") { + add("OCR") + } else if containsStemInTokens(tokens, "фотограф", "фото", "картин", "изображ") { + add("IMAGE_UNDERSTANDING") + } + if containsStemInTokens(tokens, "видео", "ролик") { + add("VIDEO_UNDERSTANDING") + } if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") { add("SCHEDULE_REMINDER") } + if containsStemInTokens(tokens, "поставь", "создай", "добавь") && containsStemInTokens(tokens, "цел", "задач", "план") { + add("COGNITIVE_REASONING") + } if isExplicitEmailAction(tokens) { add("SEND_EMAIL") } - if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") { + if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") || containsToken(tokens, "проект") || containsStemInTokens(tokens, "тест") { if primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActDirective { add("MODIFY_CODE") add("RUN_TESTS") @@ -529,6 +548,9 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin if containsInterrogative(tokens) && containsArithmetic(tokens) { add("COGNITIVE_REASONING") } + if isCreativeRequestTokens(tokens) && containsStemInTokens(tokens, "погод", "ресторан", "кафе", "билет", "рейс", "цен", "найд", "узнай", "проверь") { + add("WEB_RESEARCH") + } if containsToken(tokens, "свободно") || containsToken(tokens, "гб") { add("VERIFY_FACT") } @@ -567,8 +589,9 @@ func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens [] currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо") observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2) placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац", "бренд", "товар", "артикул", "продавец", "маркетплейс", "wildberries", "wb") - travelSearchCue := containsStemInTokens(tokens, "билет", "рейс", "поезд", "автобус", "перелет", "перелёт", "отправлен", "прибыт") - return currentCue || observedStateCue || placeSearchCue || travelSearchCue + travelSearchCue := containsStemInTokens(tokens, "билет", "рейс", "поезд", "автобус", "перелет", "перелёт", "отправлен", "прибыт", "расстоян", "километр", "добрат", "маршрут") + timeDifferenceCue := containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час") + return currentCue || observedStateCue || placeSearchCue || travelSearchCue || timeDifferenceCue } // approximateToken catches ordinary keyboard slips without turning routing diff --git a/tests/household_scenarios/checklist_50.json b/tests/household_scenarios/checklist_50.json new file mode 100644 index 0000000..0540194 --- /dev/null +++ b/tests/household_scenarios/checklist_50.json @@ -0,0 +1,52 @@ +[ + {"id":"HH50-001","kind":"question","message":"ты кто?","outcome":"identity"}, + {"id":"HH50-002","kind":"question","message":"как тебя зовут?","outcome":"identity"}, + {"id":"HH50-003","kind":"task","message":"я назову тебя Дед, запомни","outcome":"memory"}, + {"id":"HH50-004","kind":"task","message":"теперь тебя зовут Фея","outcome":"memory"}, + {"id":"HH50-005","kind":"question","message":"что ты умеешь?","outcome":"conversation"}, + {"id":"HH50-006","kind":"task","message":"запомни: мне нравится красный цвет","outcome":"memory"}, + {"id":"HH50-007","kind":"question","message":"что ты помнишь обо мне?","outcome":"memory"}, + {"id":"HH50-008","kind":"question","message":"выведи мою память таблицей","outcome":"memory"}, + {"id":"HH50-009","kind":"question","message":"какая сейчас погода в Казани?","outcome":"research"}, + {"id":"HH50-010","kind":"question","message":"какая разница во времени между Москвой и Шанхаем?","outcome":"research"}, + {"id":"HH50-011","kind":"assertion","message":"я сейчас в Москве","outcome":"memory"}, + {"id":"HH50-012","kind":"question","message":"сколько километров до Сочи?","outcome":"research"}, + {"id":"HH50-013","kind":"question","message":"как можно добраться до Сочи?","outcome":"research"}, + {"id":"HH50-014","kind":"question","message":"какой ближайший рейс до Казани?","outcome":"research"}, + {"id":"HH50-015","kind":"question","message":"найди ближайший ресторан в Сочи","outcome":"research"}, + {"id":"HH50-016","kind":"question","message":"назови доступные модели","outcome":"conversation"}, + {"id":"HH50-017","kind":"question","message":"какая модель отвечает?","outcome":"conversation"}, + {"id":"HH50-018","kind":"question","message":"что ты можешь сделать без внешних моделей?","outcome":"conversation"}, + {"id":"HH50-019","kind":"question","message":"проверь, работает ли интернет","outcome":"research"}, + {"id":"HH50-020","kind":"question","message":"почему ты не смог найти ответ?","outcome":"conversation"}, + {"id":"HH50-021","kind":"question","message":"найди на WB бренд marka-wear","outcome":"research"}, + {"id":"HH50-022","kind":"question","message":"проверь цену на видеокарту RTX 4090","outcome":"research"}, + {"id":"HH50-023","kind":"question","message":"найди билеты до Ялты","outcome":"research"}, + {"id":"HH50-024","kind":"question","message":"покажи ближайшие рейсы до Сочи","outcome":"research"}, + {"id":"HH50-025","kind":"question","message":"какой ближайший самолёт из Москвы в Казань сегодня?","outcome":"research"}, + {"id":"HH50-026","kind":"question","message":"нарисуй птицу","outcome":"media"}, + {"id":"HH50-027","kind":"task","message":"нарисуй зелёную птицу","outcome":"media"}, + {"id":"HH50-028","kind":"task","message":"нарисуй коня","outcome":"media"}, + {"id":"HH50-029","kind":"task","message":"нарисуй бурого медведя","outcome":"media"}, + {"id":"HH50-030","kind":"task","message":"нарисуй птицу в HD","outcome":"media"}, + {"id":"HH50-031","kind":"task","message":"нарисуй круг","outcome":"media"}, + {"id":"HH50-032","kind":"question","message":"расскажи сказку о Колобке","outcome":"conversation"}, + {"id":"HH50-033","kind":"task","message":"придумай короткую шутку","outcome":"conversation"}, + {"id":"HH50-034","kind":"task","message":"исправь опечатку: нарисуй прицу","outcome":"conversation"}, + {"id":"HH50-035","kind":"task","message":"поставь цель подготовиться к поездке","outcome":"action"}, + {"id":"HH50-036","kind":"task","message":"напомни завтра в 10 купить билет","outcome":"action"}, + {"id":"HH50-037","kind":"question","message":"что осталось сделать по цели?","outcome":"action"}, + {"id":"HH50-038","kind":"task","message":"проверь погоду в Казани и найди рядом кафе","outcome":"multi"}, + {"id":"HH50-039","kind":"task","message":"узнай погоду в Сочи и нарисуй птицу","outcome":"multi"}, + {"id":"HH50-040","kind":"task","message":"напомни вечером позвонить и проверь погоду","outcome":"multi"}, + {"id":"HH50-041","kind":"question","message":"послушай это голосовое сообщение","outcome":"audio"}, + {"id":"HH50-042","kind":"question","message":"что на этой фотографии?","outcome":"vision"}, + {"id":"HH50-043","kind":"question","message":"прочитай текст на фотографии","outcome":"ocr"}, + {"id":"HH50-044","kind":"question","message":"что происходит на этом видео?","outcome":"video"}, + {"id":"HH50-045","kind":"assertion","message":"источники противоречат друг другу","outcome":"conversation"}, + {"id":"HH50-046","kind":"question","message":"основной runtime сейчас недоступен","outcome":"research"}, + {"id":"HH50-047","kind":"task","message":"купи билет до Казани","outcome":"confirmation"}, + {"id":"HH50-048","kind":"task","message":"отправь письмо о поездке","outcome":"confirmation"}, + {"id":"HH50-049","kind":"task","message":"измени проект и запусти тесты","outcome":"agent"}, + {"id":"HH50-050","kind":"question","message":"не получилось ли действие?","outcome":"conversation"} +] diff --git a/tests/household_scenarios/checklist_50_test.go b/tests/household_scenarios/checklist_50_test.go new file mode 100644 index 0000000..c0ac740 --- /dev/null +++ b/tests/household_scenarios/checklist_50_test.go @@ -0,0 +1,90 @@ +package household_scenarios + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "git.cin.su/m/Assistant/contracts/capability" + interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" +) + +type checklistItem struct { + ID string `json:"id"` + Kind string `json:"kind"` + Message string `json:"message"` + Outcome string `json:"outcome"` +} + +func TestHouseholdChecklist50ReachesPositiveSemanticReaction(t *testing.T) { + data, err := os.ReadFile(filepath.Join("checklist_50.json")) + if err != nil { + t.Fatal(err) + } + var items []checklistItem + if err := json.Unmarshal(data, &items); err != nil { + t.Fatal(err) + } + if len(items) != 50 { + t.Fatalf("checklist has %d items, want exactly 50", len(items)) + } + seen := map[string]bool{} + for _, item := range items { + if item.ID == "" || item.Kind == "" || strings.TrimSpace(item.Message) == "" || item.Outcome == "" { + t.Fatalf("incomplete checklist item: %+v", item) + } + if seen[item.ID] { + t.Fatalf("duplicate checklist item %q", item.ID) + } + seen[item.ID] = true + checkSemanticReaction(t, item) + } +} + +func checkSemanticReaction(t *testing.T, item checklistItem) { + t.Helper() + candidate := interactiondomain.BuildDeterministicCandidate(item.Message, "", time.Unix(10, 0).UnixMilli()) + has := func(kind capability.CapabilityKind) bool { + for _, hint := range candidate.CapabilityHints { + if capability.CapabilityKind(hint) == kind { + return true + } + } + return false + } + switch item.Outcome { + case "identity", "memory", "conversation", "confirmation": + if has(capability.CapabilityKindWebResearch) || has(capability.CapabilityKindImageGeneration) { + t.Errorf("%s %q routed unrelated capability for %s: %+v", item.ID, item.Message, item.Outcome, candidate) + } + case "research": + if !has(capability.CapabilityKindWebResearch) { + t.Errorf("%s %q did not select research route: %+v", item.ID, item.Message, candidate) + } + case "media": + if !has(capability.CapabilityKindImageGeneration) { + t.Errorf("%s %q did not select image route: %+v", item.ID, item.Message, candidate) + } + case "action": + if !has(capability.CapabilityKindScheduleReminder) && !has(capability.CapabilityKindCognitiveReasoning) && !candidate.NeedsCognition { + t.Errorf("%s %q has no action route or cognitive handoff: %+v", item.ID, item.Message, candidate) + } + case "agent": + if !candidate.NeedsCognition && len(candidate.CapabilityHints) == 0 { + t.Errorf("%s %q has no agent route or cognitive handoff: %+v", item.ID, item.Message, candidate) + } + case "multi": + if len(candidate.CapabilityHints) < 2 { + t.Errorf("%s %q was not decomposed into multiple capabilities: %+v", item.ID, item.Message, candidate) + } + case "audio", "vision", "ocr", "video": + if len(candidate.CapabilityHints) == 0 && !candidate.NeedsCognition { + t.Errorf("%s %q has no modality route or cognitive handoff: %+v", item.ID, item.Message, candidate) + } + default: + t.Fatalf("%s uses unknown outcome %q", item.ID, item.Outcome) + } +}