Files
Cyrene/backend/memory-service/internal/model/memory.go
T
AskaEth 78e3f450c2 feat: Round 5 - Memory Service, Tool Engine, Call Records, Thinking Logs
- Fix: Session history flash (race condition + WS guard)
- Fix: Chat background overlay + sidebar transparency
- Fix: IoT device control (Chinese action names, status field)
- Feat: Independent memory-service (port 8091, 13 endpoints)
- Feat: Independent tool-engine service (port 8092, 13 tools)
- Feat: Tool call logs with paginated DevTools panel
- Feat: Thinking log records with DevTools panel
- Feat: Future development roadmap document
- Chore: Updated .gitignore, go.work, DevTools config
- Chore: 5-service health check, project review docs
2026-05-18 20:05:14 +08:00

228 lines
6.3 KiB
Go

package model
import (
"encoding/json"
"time"
)
// MemoryPriority 记忆优先级
type MemoryPriority int
const (
MemoryTemp MemoryPriority = 0 // 临时记忆 (会话内)
MemoryNormal MemoryPriority = 1 // 普通记忆
MemoryImportant MemoryPriority = 2 // 重要记忆
MemoryCore MemoryPriority = 3 // 核心记忆 (永远保留)
)
// String 返回优先级的中文描述
func (p MemoryPriority) String() string {
switch p {
case MemoryCore:
return "核心"
case MemoryImportant:
return "重要"
case MemoryNormal:
return "普通"
case MemoryTemp:
return "临时"
default:
return "未知"
}
}
// MemoryCategory 记忆分类
type MemoryCategory string
const (
CategoryUserPreference MemoryCategory = "user_preference" // 用户偏好 (食物、颜色、习惯)
CategoryPersonalInfo MemoryCategory = "personal_info" // 个人信息 (姓名、年龄、职业)
CategoryConversation MemoryCategory = "conversation" // 对话摘要
CategoryKnowledge MemoryCategory = "knowledge" // 知识性信息
CategoryEvent MemoryCategory = "event" // 事件记录
CategoryTask MemoryCategory = "task" // 任务/计划
CategoryRelationship MemoryCategory = "relationship" // 关系信息
// 向后兼容的旧分类别名
CategoryPreference = CategoryUserPreference
CategoryFact = CategoryPersonalInfo
CategoryHabit = CategoryUserPreference
CategoryOther = CategoryKnowledge
)
// CategoryDisplayName 返回分类的中文显示名
func (c MemoryCategory) DisplayName() string {
switch c {
case CategoryUserPreference:
return "用户偏好"
case CategoryPersonalInfo:
return "个人信息"
case CategoryConversation:
return "对话摘要"
case CategoryKnowledge:
return "知识信息"
case CategoryEvent:
return "事件记录"
case CategoryTask:
return "任务计划"
case CategoryRelationship:
return "关系情感"
default:
return "其他"
}
}
// MemoryEntry 记忆条目
type MemoryEntry struct {
ID string `json:"id" db:"id"`
UserID string `json:"user_id" db:"user_id"`
Content string `json:"content" db:"content"`
Summary string `json:"summary" db:"summary"` // 简短摘要
Category MemoryCategory `json:"category" db:"category"`
Priority MemoryPriority `json:"priority" db:"priority"`
Importance int `json:"importance" db:"importance"` // 重要程度 1-10
Keywords []string `json:"keywords" db:"keywords"` // 关键词标签
SessionID string `json:"session_id" db:"session_id"` // 来源会话
Source string `json:"source" db:"source"` // 来源 (conversation/thinking)
Embedding []float32 `json:"-" db:"embedding"` // 向量 (pgvector)
AccessCount int `json:"access_count" db:"access_count"`
LastAccess time.Time `json:"last_access" db:"last_access"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"` // 最后更新时间
ExpiresAt *time.Time `json:"expires_at,omitempty" db:"expires_at"` // 临时记忆过期时间
}
// KeywordsJSON 将关键词序列化为 JSON 字符串(用于数据库存储)
func (e *MemoryEntry) KeywordsJSON() string {
if len(e.Keywords) == 0 {
return "[]"
}
data, _ := json.Marshal(e.Keywords)
return string(data)
}
// ParseKeywords 从 JSON 字符串解析关键词
func ParseKeywords(raw string) []string {
if raw == "" || raw == "[]" {
return nil
}
var keywords []string
if err := json.Unmarshal([]byte(raw), &keywords); err != nil {
return nil
}
return keywords
}
// SimilarityScore 计算两个记忆条目的简单文本相似度(基于词汇重叠)
// 返回值 0.0 - 1.0
func (e *MemoryEntry) SimilarityScore(other *MemoryEntry) float64 {
if e.Content == other.Content {
return 1.0
}
// 基于关键词的重叠度
if len(e.Keywords) > 0 && len(other.Keywords) > 0 {
keywordSet := make(map[string]bool, len(e.Keywords))
for _, k := range e.Keywords {
keywordSet[k] = true
}
overlap := 0
for _, k := range other.Keywords {
if keywordSet[k] {
overlap++
}
}
keywordScore := float64(overlap) / float64(max(len(e.Keywords), len(other.Keywords)))
if keywordScore > 0.6 {
return keywordScore
}
}
// 基于内容的字符级 Jaccard 相似度
return jaccardSimilarity(e.Content, other.Content)
}
// jaccardSimilarity 计算两个字符串的 Jaccard 相似度
func jaccardSimilarity(a, b string) float64 {
if a == b {
return 1.0
}
if len(a) == 0 || len(b) == 0 {
return 0.0
}
// 使用 bigram 分词
bigramsA := make(map[string]int)
runesA := []rune(a)
for i := 0; i < len(runesA)-1; i++ {
bigramsA[string(runesA[i:i+2])]++
}
bigramsB := make(map[string]int)
runesB := []rune(b)
for i := 0; i < len(runesB)-1; i++ {
bigramsB[string(runesB[i:i+2])]++
}
intersection := 0
for bg, countA := range bigramsA {
if countB, ok := bigramsB[bg]; ok {
intersection += min(countA, countB)
}
}
union := 0
allBigrams := make(map[string]bool)
for bg := range bigramsA {
allBigrams[bg] = true
}
for bg := range bigramsB {
allBigrams[bg] = true
}
for bg := range allBigrams {
union += max(bigramsA[bg], bigramsB[bg])
}
if union == 0 {
return 0.0
}
return float64(intersection) / float64(union)
}
// MemoryQuery 记忆查询参数
type MemoryQuery struct {
UserID string
Query string // 查询文本
Category MemoryCategory
Priority MemoryPriority
MinImportance int // 最低重要程度筛选
Limit int
Offset int
}
// ThinkingLog 自主思考日志
type ThinkingLog struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Content string `json:"content"`
ToolCalls string `json:"tool_calls"` // JSON 数组
ToolCallCount int `json:"tool_call_count"`
ContentLength int `json:"content_length"`
CreatedAt time.Time `json:"created_at"`
}
// ThinkingQuery 思考日志查询参数
type ThinkingQuery struct {
UserID string
Limit int
Offset int
}
// ThinkingStats 思考日志统计
type ThinkingStats struct {
TotalLogs int `json:"total_logs"`
TotalToolCalls int `json:"total_tool_calls"`
AvgContentLen float64 `json:"avg_content_length"`
LatestAt string `json:"latest_at"`
}