154 lines
5.1 KiB
Go
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 ""
|
|
}
|