Files
Assistant/modules/interaction/domain/personal_memory.go
T
2026-08-29 15:07:47 +03:00

154 lines
5.1 KiB
Go

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
}
}
// 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 {
return key + " — " + value
}
}
}
return ""
}