package domain import ( "sort" "strings" "git.cin.su/m/Assistant/contracts/interaction" ) // PersonalMemory is a rebuildable context projection. Its source of truth is // the append-only human utterance history; it is deliberately not a second // canonical store. Each fact retains the source utterance and confidence so a // later cognitive or capability route can distinguish a human statement from // an externally verified fact. type PersonalMemory struct { Facts []PersonalMemoryFact } type PersonalMemoryFact struct { Kind string Subject string Value string ConfidenceMillis int64 SourceUtteranceID interaction.HumanUtteranceID ObservedAtUnixMillis int64 } const personalMemoryCurrentLocation = "CURRENT_LOCATION" // personalMemoryMaxValueRunes keeps the compatibility projection bounded. // Canonical utterances remain complete in storage; only the rebuildable // cognitive context is capped so old long-form instructions cannot become a // provider prompt or crowd out a current household fact. const personalMemoryMaxValueRunes = 512 // CompilePersonalMemory turns direct, first-person human statements into a // small typed context. It deliberately preserves preferences as statements // rather than pretending that an arbitrary preference is a verified fact. func CompilePersonalMemory(history []interaction.HumanUtterance) PersonalMemory { latestLocation := PersonalMemoryFact{} preferences := make([]PersonalMemoryFact, 0, 4) for _, utterance := range history { if location := personalCurrentLocation(utterance.Content); location != "" { fact := PersonalMemoryFact{ Kind: personalMemoryCurrentLocation, Value: location, ConfidenceMillis: 950, SourceUtteranceID: utterance.ID, ObservedAtUnixMillis: utterance.ReceivedAtUnixMillis, } if fact.ObservedAtUnixMillis >= latestLocation.ObservedAtUnixMillis { latestLocation = fact } } if preference := personalPreference(utterance.Content); preference != "" { preferences = append(preferences, PersonalMemoryFact{ Kind: "PREFERENCE", Subject: preferenceSubject(preference), Value: preference, ConfidenceMillis: 900, SourceUtteranceID: utterance.ID, ObservedAtUnixMillis: utterance.ReceivedAtUnixMillis, }) } } result := PersonalMemory{Facts: make([]PersonalMemoryFact, 0, 1+len(preferences))} if latestLocation.Kind != "" { result.Facts = append(result.Facts, latestLocation) } sort.SliceStable(preferences, func(left, right int) bool { return preferences[left].ObservedAtUnixMillis > preferences[right].ObservedAtUnixMillis }) if len(preferences) > 8 { preferences = preferences[:8] } result.Facts = append(result.Facts, preferences...) return result } // LatestPreference returns the current value for a semantic subject while // retaining older facts for historical evidence. func (m PersonalMemory) LatestPreference(subject string) (PersonalMemoryFact, bool) { var latest PersonalMemoryFact for _, fact := range m.Facts { if fact.Kind != "PREFERENCE" || fact.Subject != subject { continue } if latest.Kind == "" || fact.ObservedAtUnixMillis >= latest.ObservedAtUnixMillis { latest = fact } } return latest, latest.Kind != "" } func (m PersonalMemory) CurrentLocation() string { for _, fact := range m.Facts { if fact.Kind == personalMemoryCurrentLocation { return fact.Value } } return "" } // ContextText is compact, source-aware context for a cognitive or skill // request. It contains facts rather than a copied chat transcript. func (m PersonalMemory) ContextText() string { lines := make([]string, 0, len(m.Facts)) for _, fact := range m.Facts { value := strings.TrimSpace(fact.Value) if value == "" { continue } switch fact.Kind { case personalMemoryCurrentLocation: lines = append(lines, "Текущее место пользователя (его сообщение): "+value) case "PREFERENCE": lines = append(lines, "Предпочтение пользователя (его сообщение): "+value) } } return strings.Join(lines, "\n") } // RecallPersonalPreference answers the narrow, read-only memory question from // the rebuildable projection. It is intentionally conservative: only an // explicit question about the remembered test color is handled here; other // questions continue through the normal cognitive route. func RecallPersonalPreference(history []interaction.HumanUtterance, question string) string { lowerQuestion := strings.ToLower(question) if !strings.Contains(lowerQuestion, "цвет") || (!strings.Contains(lowerQuestion, "какой") && !strings.Contains(lowerQuestion, "назвал")) { return "" } memory := CompilePersonalMemory(history) for _, fact := range memory.Facts { if fact.Kind != "PREFERENCE" || fact.Subject != "тестовый цвет" { continue } current, ok := memory.LatestPreference(fact.Subject) if !ok || current.SourceUtteranceID != fact.SourceUtteranceID { continue } value := strings.TrimSpace(current.Value) if separator := strings.Index(value, "—"); separator >= 0 { value = strings.TrimSpace(value[separator+len("—"):]) } if value != "" { return "Вы называли тестовый цвет: " + value + "." } } return "" } func preferenceSubject(preference string) string { if separator := strings.Index(preference, "—"); separator >= 0 { preference = preference[:separator] } subject := strings.TrimSpace(preference) // Operational markers belong to the preference value, not a new subject. if strings.HasPrefix(strings.ToLower(subject), "тестовый цвет") { return "тестовый цвет" } return subject } func personalCurrentLocation(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 personalPreference(message string) string { lower := strings.ToLower(strings.TrimSpace(message)) for _, cue := range []string{"мне нравится", "я люблю"} { index := strings.Index(lower, cue) if index < 0 { continue } value := strings.TrimSpace(lower[index+len(cue):]) value = strings.Trim(value, " .,!?:;—-") if len([]rune(value)) >= 2 && len([]rune(value)) <= personalMemoryMaxValueRunes { return value } } // Explicit remembered assertions are still human-provided context, not // verified facts. Project the compact key/value statement without storing a // second copy outside the canonical utterance history. if index := strings.Index(lower, "запомни"); index >= 0 { remembered := strings.TrimSpace(lower[index+len("запомни"):]) remembered = strings.TrimLeft(remembered, " :—-") for _, prefix := range []string{"мой ", "моя ", "моё ", "мое "} { if !strings.HasPrefix(remembered, prefix) { continue } statement := strings.TrimSpace(remembered[len(prefix):]) separator := strings.IndexAny(statement, "—-:") if separator <= 0 || separator >= len(statement)-1 { continue } key := strings.TrimSpace(statement[:separator]) separatorWidth := 1 if strings.HasPrefix(statement[separator:], "—") { separatorWidth = len("—") } value := strings.Trim(strings.TrimSpace(statement[separator+separatorWidth:]), " .,!?:;—-") if key != "" && len([]rune(value)) >= 2 && len([]rune(value)) <= personalMemoryMaxValueRunes { return key + " — " + value } } } return "" }