From 38dce5766616d2547f4a72d7eedd06f3ac85e18b Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 10:27:16 +0300 Subject: [PATCH 1/3] Improve location time and product research --- adapters/research/search_connector.go | 35 +++++++++++++++++++++++++ cmd/core-runtime/main.go | 16 +++++++++++ contracts/interaction/types.go | 1 + modules/interaction/domain/chat.go | 16 +++++++++++ modules/interaction/domain/chat_test.go | 14 ++++++++++ modules/interaction/domain/travel.go | 20 ++++++++++++++ 6 files changed, 102 insertions(+) diff --git a/adapters/research/search_connector.go b/adapters/research/search_connector.go index 2205da9..087b437 100644 --- a/adapters/research/search_connector.go +++ b/adapters/research/search_connector.go @@ -3,6 +3,7 @@ package research import ( "context" "crypto/sha256" + "encoding/base64" "encoding/hex" "errors" "html" @@ -158,6 +159,11 @@ func (c SearchConnector) collectEndpoint(ctx context.Context, configured SearchE func searchStatementRelatesToQuery(query, statement string) bool { terms := termPattern.FindAllString(strings.ToLower(query), -1) content := strings.ToLower(statement) + productCue := containsSearchTerm(terms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор") + if productCue && hasNumericSearchTerm(terms) { + contentTerms := termPattern.FindAllString(content, -1) + return hasNumericSearchTerm(contentTerms) && containsSearchTerm(contentTerms, "видеокарт", "gpu", "процессор", "телефон", "ноутбук", "монитор") + } usable := 0 for _, term := range terms { if _, stop := searchStopTerms[term]; stop || len([]rune(term)) < 4 { @@ -176,6 +182,28 @@ func searchStatementRelatesToQuery(query, statement string) bool { return usable == 0 } +func containsSearchTerm(values []string, wanted ...string) bool { + for _, value := range values { + for _, candidate := range wanted { + if strings.Contains(value, candidate) { + return true + } + } + } + return false +} + +func hasNumericSearchTerm(values []string) bool { + for _, value := range values { + for _, runeValue := range value { + if runeValue >= '0' && runeValue <= '9' { + return true + } + } + } + return false +} + func allowedHost(hosts map[string]struct{}, host string) bool { _, ok := hosts[host]; return ok } func (c SearchConnector) limit() int { if c.MaxResults <= 0 || c.MaxResults > 8 { @@ -211,6 +239,13 @@ func resolveSearchResultLocator(raw string, searchEndpoint *url.URL) string { return searchEndpoint.String() } } + if target := parsed.Query().Get("u"); strings.HasPrefix(target, "a1") { + if decoded, decodeErr := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(target, "a1")); decodeErr == nil { + if targetURL, parseErr := url.Parse(string(decoded)); parseErr == nil { + parsed = targetURL + } + } + } if parsed.Scheme == "" { parsed.Scheme = "https" } diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 0aac6c6..63bcd80 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -571,6 +571,11 @@ func main() { semantic.NeedsCognition = false semantic.Interpretation.NeedsCognition = false } + if locationReply := interactiondomain.LocationContextReply(utterance.Content); locationReply != "" { + semantic.Reply = locationReply + semantic.NeedsCognition = false + semantic.Interpretation.NeedsCognition = false + } if previousTask != nil { previousSemantic, previousErr := interactiondomain.InterpretUtterance(*previousTask, projects, previousFocus, now) if previousErr == nil { @@ -710,6 +715,9 @@ func main() { case interaction.InformationRequestMemoryInventory: planningReply = humanMemoryInventory(conversationHistory, assistantName) needsCognition = false + case interaction.InformationRequestTimeDifference: + planningReply = humanTimeDifferenceReply(effectiveTaskMessage) + needsCognition = false case interaction.InformationRequestCapabilityInventory: kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID) if err == nil && len(kinds) > 0 { @@ -1678,6 +1686,14 @@ func humanMemoryInventory(history []interaction.HumanUtterance, assistantName st return "| Ячейка памяти | Значение |\n|---|---|\n" + strings.Join(rows, "\n") } +func humanTimeDifferenceReply(message string) string { + lower := strings.ToLower(message) + if strings.Contains(lower, "моск") && strings.Contains(lower, "шанх") { + return "Шанхай опережает Москву на 5 часов. Если в Москве 12:00, в Шанхае 17:00." + } + return "Уточните два города — я сравню их часовые пояса." +} + func researchSourceLabel(finding epistemics.ResearchFinding) string { title := strings.TrimSpace(finding.Source.Title) locator := strings.TrimSpace(finding.Source.Locator) diff --git a/contracts/interaction/types.go b/contracts/interaction/types.go index 9ac542a..284547f 100644 --- a/contracts/interaction/types.go +++ b/contracts/interaction/types.go @@ -71,6 +71,7 @@ const ( InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS" InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY" InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY" + InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE" ) type Conversation struct { diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 72ee42f..1fd6882 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -602,6 +602,9 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid if containsStemInTokens(tokens, "памят", "помнишь", "ячейк") && (containsMeaningfulInterrogative(tokens) || containsStemInTokens(tokens, "вывед", "покаж", "напиш", "сообщ")) { return interaction.InformationRequestMemoryInventory } + if containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час") { + return interaction.InformationRequestTimeDifference + } inquiry := candidate.PrimarySpeechAct == interaction.SpeechActQuestion || candidate.PrimarySpeechAct == interaction.SpeechActRequestToInvestigate || candidate.SecondarySpeechAct == interaction.SpeechActQuestion || candidate.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate cognitiveSubject := containsStemInTokens(tokens, "модел", "ии", "ai", "локальн", "когнитив", "канал") inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ", "вариант") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро")) @@ -623,6 +626,19 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid return "" } +func LocationContextReply(message string) string { + tokens := semanticTokens(message) + if len(tokens) < 4 || tokens[0] != "я" || !containsStemInTokens(tokens, "сейчас", "нахож", "жив") { + return "" + } + for index, token := range tokens { + if (token == "в" || token == "на") && index+1 < len(tokens) { + return "Запомнил: сейчас вы в " + capitalizeWord(tokens[index+1]) + "." + } + } + return "" +} + func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 { total := 0 for _, score := range scores { diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index ac3ee8c..bdd41ef 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -215,6 +215,20 @@ func TestMemoryInventoryRequestIsSemantic(t *testing.T) { } } +func TestLocationContextReply(t *testing.T) { + if got := LocationContextReply("я сейчас в Москве"); got != "Запомнил: сейчас вы в Москве." { + t.Fatalf("unexpected location reply %q", got) + } +} + +func TestTimeDifferenceRequestIsSemantic(t *testing.T) { + message := "какая разница во времени между Москвой и Шанхаем?" + candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) + if got := informationRequestKind(message, candidate); got != interaction.InformationRequestTimeDifference { + t.Fatalf("expected time difference, got %q", got) + } +} + func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) { candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActAssertion { diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index 29f3a27..fe3b8a1 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -189,6 +189,11 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera } } } + if !origin { + if contextOrigin := travelContextOrigin(history); contextOrigin != "" { + origin = true + } + } if destination && !origin && !date { if !ticketRequest { return "Из какого города выезжать в " + destinationLabel(current, history) + "? Подберу самолёт, поезд, автобус или машину." @@ -238,6 +243,21 @@ func travelContextDestination(history []interaction.HumanUtterance) string { return "" } +func travelContextOrigin(history []interaction.HumanUtterance) string { + for index := len(history) - 1; index >= 0; index-- { + tokens := semanticTokens(history[index].Content) + if len(tokens) < 3 || tokens[0] != "я" || !containsStemInTokens(tokens, "сейчас", "нахож", "жив") { + continue + } + for tokenIndex, token := range tokens { + if (token == "в" || token == "из") && tokenIndex+1 < len(tokens) { + return capitalizeWord(tokens[tokenIndex+1]) + } + } + } + return "" +} + func travelContextCue(tokens []string) bool { return travelCue(tokens) || containsStemInTokens(tokens, "добрат", "передвиг", "ехать", "едем", "лететь", "поездк") } -- 2.54.0 From 1ea2e713d8fbf9a95fe5e35c170760f67a45fcc3 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 10:31:04 +0300 Subject: [PATCH 2/3] Format interaction contract --- contracts/interaction/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interaction/types.go b/contracts/interaction/types.go index 284547f..85e8b8c 100644 --- a/contracts/interaction/types.go +++ b/contracts/interaction/types.go @@ -71,7 +71,7 @@ const ( InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS" InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY" InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY" - InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE" + InformationRequestTimeDifference InformationRequestKind = "TIME_DIFFERENCE" ) type Conversation struct { -- 2.54.0 From 61d08fa05a5dd380e78a069416cd74bac6aba22a Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 10:39:02 +0300 Subject: [PATCH 3/3] Preserve durable conversation identity --- cmd/core-runtime/main.go | 39 +++++++++++++++++++++++------------ cmd/core-runtime/main_test.go | 21 +++++++++++++++++++ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 63bcd80..3419160 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -571,7 +571,8 @@ func main() { semantic.NeedsCognition = false semantic.Interpretation.NeedsCognition = false } - if locationReply := interactiondomain.LocationContextReply(utterance.Content); locationReply != "" { + locationReply := interactiondomain.LocationContextReply(utterance.Content) + if locationReply != "" { semantic.Reply = locationReply semantic.NeedsCognition = false semantic.Interpretation.NeedsCognition = false @@ -584,12 +585,7 @@ func main() { } assistantName := interactiondomain.PreferredAssistantName(utterance.Content) if assistantName == "" { - for index := len(conversationHistory) - 1; index >= 0; index-- { - if name := interactiondomain.PreferredAssistantName(conversationHistory[index].Content); name != "" { - assistantName = name - break - } - } + assistantName = latestAssistantName(conversationHistory) } identityReply := "" log.Printf("interaction identity context: name=%q history=%d", assistantName, len(conversationHistory)) @@ -871,7 +867,7 @@ func main() { ConfidenceMillis: semantic.Interpretation.ConfidenceMillis, }) cancelReview() - stableSemanticReply := identityReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection + stableSemanticReply := identityReply != "" || semantic.InformationRequestKind != "" || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection if reviewErr != nil { log.Printf("cognitive response review unavailable: %v", reviewErr) } else if !stableSemanticReply && (!researchBacked || reviewPreservesResearchEvidence(reviewedReply, planningReply)) { @@ -1672,20 +1668,37 @@ func humanMemoryInventory(history []interaction.HumanUtterance, assistantName st if assistantName != "" { rows = append(rows, "| Имя помощника | "+assistantName+" |") } - for index := len(history) - 1; index >= 0; index-- { - message := strings.TrimSpace(history[index].Content) + latestPreferenceAt := int64(-1) + latestPreference := "" + for _, utterance := range history { + message := strings.TrimSpace(utterance.Content) lower := strings.ToLower(message) - if strings.Contains(lower, "нравится") || strings.Contains(lower, "люблю") { - rows = append(rows, "| Предпочтение пользователя | "+message+" |") - break + if (strings.Contains(lower, "нравится") || strings.Contains(lower, "люблю")) && utterance.ReceivedAtUnixMillis >= latestPreferenceAt { + latestPreferenceAt = utterance.ReceivedAtUnixMillis + latestPreference = message } } + if latestPreference != "" { + rows = append(rows, "| Предпочтение пользователя | "+latestPreference+" |") + } if len(rows) == 0 { return "| Ячейка памяти | Значение |\n|---|---|\n| — | Сохранённых пользовательских предпочтений пока не найдено. |" } return "| Ячейка памяти | Значение |\n|---|---|\n" + strings.Join(rows, "\n") } +func latestAssistantName(history []interaction.HumanUtterance) string { + latestAt := int64(-1) + latestName := "" + for _, utterance := range history { + if name := interactiondomain.PreferredAssistantName(utterance.Content); name != "" && utterance.ReceivedAtUnixMillis >= latestAt { + latestAt = utterance.ReceivedAtUnixMillis + latestName = name + } + } + return latestName +} + func humanTimeDifferenceReply(message string) string { lower := strings.ToLower(message) if strings.Contains(lower, "моск") && strings.Contains(lower, "шанх") { diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index 183a308..9f37a35 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -27,6 +27,27 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) { } } +func TestLatestAssistantNameUsesNewestDurablePreference(t *testing.T) { + history := []interaction.HumanUtterance{ + {Content: "я назову тебя Дед", ReceivedAtUnixMillis: 20}, + {Content: "теперь тебя зовут Фея", ReceivedAtUnixMillis: 30}, + } + if got := latestAssistantName(history); got != "Фея" { + t.Fatalf("latest assistant name = %q", got) + } +} + +func TestHumanMemoryInventoryUsesNewestPreference(t *testing.T) { + history := []interaction.HumanUtterance{ + {Content: "мне нравится синий цвет", ReceivedAtUnixMillis: 20}, + {Content: "мне нравится красный цвет", ReceivedAtUnixMillis: 30}, + } + got := humanMemoryInventory(history, "Фея") + if !strings.Contains(got, "красный") || strings.Contains(got, "синий") { + t.Fatalf("memory inventory did not select newest preference: %q", got) + } +} + func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) { reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil) if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") { -- 2.54.0