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 Value string ConfidenceMillis int64 SourceUtteranceID interaction.HumanUtteranceID ObservedAtUnixMillis int64 } const personalMemoryCurrentLocation = "CURRENT_LOCATION" // 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", 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 } 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") } 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 { return value } } return "" }