Handle memory identity and travel follow-ups #333

Merged
m merged 2 commits from codex/chat-semantic-inventory into main 2026-08-25 07:24:46 +00:00
6 changed files with 85 additions and 9 deletions
+29
View File
@@ -510,6 +510,7 @@ func main() {
}
var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
continuationMessage := ""
travelContinuation := false
var previousTask *interaction.HumanUtterance
var conversationHistory []interaction.HumanUtterance
if request.ConversationID != "" {
@@ -534,6 +535,7 @@ func main() {
effectiveTaskMessage = continuation
} else if continuation := interactiondomain.ContinueTravelOrigin(effectiveTaskMessage, conversationHistory); continuation != "" {
continuationMessage = continuation
travelContinuation = true
effectiveTaskMessage = continuation
}
if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) {
@@ -564,6 +566,11 @@ func main() {
semantic = continuedSemantic
}
}
if travelContinuation && interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory) == "" {
semantic.CapabilityHints = append(semantic.CapabilityHints, string(capability.CapabilityKindWebResearch))
semantic.NeedsCognition = false
semantic.Interpretation.NeedsCognition = false
}
if previousTask != nil {
previousSemantic, previousErr := interactiondomain.InterpretUtterance(*previousTask, projects, previousFocus, now)
if previousErr == nil {
@@ -700,6 +707,9 @@ func main() {
helpPath = work.HelpPath{}
}
switch semantic.InformationRequestKind {
case interaction.InformationRequestMemoryInventory:
planningReply = humanMemoryInventory(conversationHistory, assistantName)
needsCognition = false
case interaction.InformationRequestCapabilityInventory:
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
if err == nil && len(kinds) > 0 {
@@ -1649,6 +1659,25 @@ func reviewPreservesResearchEvidence(reviewed, draft string) bool {
return true
}
func humanMemoryInventory(history []interaction.HumanUtterance, assistantName string) string {
rows := make([]string, 0, 4)
if assistantName != "" {
rows = append(rows, "| Имя помощника | "+assistantName+" |")
}
for index := len(history) - 1; index >= 0; index-- {
message := strings.TrimSpace(history[index].Content)
lower := strings.ToLower(message)
if strings.Contains(lower, "нравится") || strings.Contains(lower, "люблю") {
rows = append(rows, "| Предпочтение пользователя | "+message+" |")
break
}
}
if len(rows) == 0 {
return "| Ячейка памяти | Значение |\n|---|---|\n| — | Сохранённых пользовательских предпочтений пока не найдено. |"
}
return "| Ячейка памяти | Значение |\n|---|---|\n" + strings.Join(rows, "\n")
}
func researchSourceLabel(finding epistemics.ResearchFinding) string {
title := strings.TrimSpace(finding.Source.Title)
locator := strings.TrimSpace(finding.Source.Locator)
+1
View File
@@ -70,6 +70,7 @@ const (
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY"
)
type Conversation struct {
+12 -2
View File
@@ -133,7 +133,7 @@ func PreferredAssistantName(message string) string {
continue
}
for _, candidate := range tokens[index+1:] {
if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" {
if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" || candidate == "в" || candidate == "теперь" || candidate == "же" {
continue
}
if candidate == "буду" || candidate == "будет" || candidate == "запомни" || candidate == "помни" || candidate == "ты" || candidate == "это" {
@@ -144,12 +144,19 @@ func PreferredAssistantName(message string) string {
}
runes := []rune(candidate)
runes[0] = []rune(strings.ToUpper(string(runes[0])))[0]
return string(runes)
return normalizeAssistantName(string(runes))
}
}
return ""
}
func normalizeAssistantName(value string) string {
if strings.HasSuffix(value, "ю") {
return strings.TrimSuffix(value, "ю") + "я"
}
return value
}
// IsIdentityInquiry identifies a request about the assistant itself by its
// interrogative meaning. Punctuation is optional and wording may vary.
func IsIdentityInquiry(message string) bool {
@@ -592,6 +599,9 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid
if len(tokens) == 0 {
return ""
}
if containsStemInTokens(tokens, "памят", "помнишь", "ячейк") && (containsMeaningfulInterrogative(tokens) || containsStemInTokens(tokens, "вывед", "покаж", "напиш", "сообщ")) {
return interaction.InformationRequestMemoryInventory
}
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, "настро"))
+12
View File
@@ -198,11 +198,23 @@ func TestNamingPreferenceIsSemanticAndProducesNaturalAcceptance(t *testing.T) {
if name := PreferredAssistantName("я назову тебя дед, запомни"); name != "Дед" {
t.Fatalf("expected conversational name form to be extracted, got %q", name)
}
if name := PreferredAssistantName("давай переименуем тебя в фею. теперь тебя зовут фея"); name != "Фея" {
t.Fatalf("rename phrase returned %q", name)
}
if !IsIdentityInquiry("ты кто?") || !IsIdentityInquiry("как тебя зовут") {
t.Fatal("identity variants should be recognized")
}
}
func TestMemoryInventoryRequestIsSemantic(t *testing.T) {
for _, message := range []string{"выведи свою память", "напиши что ты помнишь в виде таблицы"} {
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
if got := informationRequestKind(message, candidate); got != interaction.InformationRequestMemoryInventory {
t.Fatalf("%q: expected memory inventory, got %q", message, got)
}
}
}
func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) {
candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActAssertion {
+7 -1
View File
@@ -103,12 +103,18 @@ func TestContinueTravelOrigin(t *testing.T) {
if got := ContinueTravelOrigin("я в Казани", nil); got != "" {
t.Fatalf("location must not create a travel task without context: %q", got)
}
if got := ContinueTravelOrigin("Москва", history); got != "Как можно добраться до Сочи из города Москва" {
t.Fatalf("bare city should complete travel origin: %q", got)
}
}
func TestTravelSearchClarification(t *testing.T) {
if got := TravelSearchClarification("Найди билеты", nil); got == "" {
t.Fatal("underspecified travel request must ask for missing route and date")
}
if got := TravelSearchClarification("Покажи ближайшие рейсы до Сочи", nil); got != "Из какого города вылетать?" {
t.Fatalf("nearest-flight request should default date to now, got %q", got)
}
if got := TravelSearchClarification("Найди билеты из Москвы в Сочи на 12 сентября", nil); got != "" {
t.Fatalf("complete travel request must proceed, got %q", got)
}
@@ -126,7 +132,7 @@ func TestTravelSearchClarification(t *testing.T) {
t.Fatalf("complete general travel route must proceed, got %q", got)
}
contextHistory := []interaction.HumanUtterance{{Content: "Как можно добраться до Сочи?"}}
if got := TravelSearchClarification("Найди билеты, какой ближайший самолёт", contextHistory); got != "Из какого города вылетать в Сочи и на какую дату?" {
if got := TravelSearchClarification("Найди билеты, какой ближайший самолёт", contextHistory); got != "Из какого города вылетать в Сочи?" {
t.Fatalf("travel destination should be inherited from context, got %q", got)
}
noisyContext := []interaction.HumanUtterance{
+24 -6
View File
@@ -97,15 +97,22 @@ func ContinueLocalSearch(message string, history []interaction.HumanUtterance) s
// completed for routing and research.
func ContinueTravelOrigin(message string, history []interaction.HumanUtterance) string {
current := semanticTokens(StripAssistantVocative(message))
if len(current) < 3 || len(current) > 6 || current[0] != "я" {
if len(current) == 0 || len(current) > 6 {
return ""
}
location := ""
for index, token := range current {
if (token == "в" || token == "из") && index+1 < len(current) {
location = capitalizeWord(current[index+1])
break
if current[0] == "я" {
if len(current) < 3 {
return ""
}
for index, token := range current {
if (token == "в" || token == "из") && index+1 < len(current) {
location = capitalizeWord(current[index+1])
break
}
}
} else if len(current) <= 2 && !isTravelDateWord(current[0]) && len([]rune(current[0])) >= 3 {
location = capitalizeWord(current[0])
}
if location == "" {
return ""
@@ -124,7 +131,11 @@ func ContinueTravelOrigin(message string, history []interaction.HumanUtterance)
return ""
}
previous := strings.TrimRight(strings.TrimSpace(StripAssistantVocative(latest.Content)), "?!.,;:")
return strings.TrimSpace(previous + " из " + location)
preposition := " из "
if current[0] != "я" {
preposition = " из города "
}
return strings.TrimSpace(previous + preposition + location)
}
// TravelSearchClarification keeps an incomplete travel request in dialogue
@@ -149,6 +160,9 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera
origin := containsStemInTokens(current, "из", "откуда", "от")
destination := containsStemInTokens(current, "в", "куда", "до")
date := containsStemInTokens(current, "сегодня", "завтра", "послезавтр", "январ", "феврал", "март", "апрел", "мая", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр")
if containsStemInTokens(current, "ближ") {
date = true
}
if !date {
for _, token := range current {
if hasDigit(token) {
@@ -190,6 +204,10 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera
return "Уточните маршрут и дату: откуда, куда и на какой день нужны билеты?"
}
func isTravelDateWord(value string) bool {
return containsStem(value, "сегодня", "завтра", "послезавтр", "январ", "феврал", "март", "апрел", "мая", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр")
}
func destinationLabel(current []string, history []interaction.HumanUtterance) string {
for index, token := range current {
if (token == "в" || token == "до") && index+1 < len(current) {