614 lines
20 KiB
Go
614 lines
20 KiB
Go
package ctxmgr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/storage"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// ContextManager 多级与多类型上下文总管理器
|
|
type ContextManager struct {
|
|
pool *pgxpool.Pool
|
|
cas *storage.CAS
|
|
reflector *PreferenceReflector
|
|
decayCfg TimeDecayConfig
|
|
embedder VectorEmbedder
|
|
|
|
// 离线降级内存缓存
|
|
mu sync.RWMutex
|
|
users map[int64]*UserPreference
|
|
projects map[int64]*ProjectCollectiveContext
|
|
messages map[string][]ChatMessageRecord // session_id -> messages
|
|
sessions map[string]*ChatSessionSummary // session_id -> summary
|
|
activities map[int64][]ActivityItem // project_id -> activities
|
|
memoryChunks []MemoryChunkItem // 语义记忆切片缓存
|
|
summaries map[string]*ChatSummary // session_id -> rolling summary
|
|
digests map[string]map[string]*DailyDigest // session_id -> day_key -> DailyDigest
|
|
cache *MultiLevelCache // 多级统一缓存系统 (C1-C5)
|
|
nextActivityID int64
|
|
nextMessageID int64
|
|
nextChunkID int64
|
|
}
|
|
|
|
// NewContextManager 创建上下文管理器
|
|
func NewContextManager(pool *pgxpool.Pool, cas *storage.CAS, decayCfg TimeDecayConfig) *ContextManager {
|
|
if decayCfg.HalfLifeDays <= 0 {
|
|
decayCfg = DefaultTimeDecayConfig()
|
|
}
|
|
return &ContextManager{
|
|
pool: pool,
|
|
cas: cas,
|
|
reflector: NewPreferenceReflector(),
|
|
decayCfg: decayCfg,
|
|
embedder: NewDeterministicEmbedder(1024),
|
|
users: make(map[int64]*UserPreference),
|
|
projects: make(map[int64]*ProjectCollectiveContext),
|
|
messages: make(map[string][]ChatMessageRecord),
|
|
sessions: make(map[string]*ChatSessionSummary),
|
|
activities: make(map[int64][]ActivityItem),
|
|
memoryChunks: make([]MemoryChunkItem, 0),
|
|
summaries: make(map[string]*ChatSummary),
|
|
digests: make(map[string]map[string]*DailyDigest),
|
|
cache: NewMultiLevelCache(),
|
|
nextActivityID: 1,
|
|
nextMessageID: 1,
|
|
nextChunkID: 1,
|
|
}
|
|
}
|
|
|
|
// Cache 获取上下文管理器的多级缓存实例
|
|
func (m *ContextManager) Cache() *MultiLevelCache {
|
|
if m == nil {
|
|
return GetGlobalCache()
|
|
}
|
|
if m.cache == nil {
|
|
m.cache = GetGlobalCache()
|
|
}
|
|
return m.cache
|
|
}
|
|
|
|
// SetEmbedder 设置自定义向量嵌入器 (如生产环境 bge-m3)
|
|
func (m *ContextManager) SetEmbedder(emb VectorEmbedder) {
|
|
if emb != nil {
|
|
m.embedder = emb
|
|
}
|
|
}
|
|
|
|
// RecordUserMessage 记录单轮对话消息并自动驱动工作习惯偏好更新
|
|
func (m *ContextManager) RecordUserMessage(ctx context.Context, sessionID string, userID int64, role, content, intent, branch string, meta map[string]any) error {
|
|
_, err := m.RecordUserMessageWithID(ctx, sessionID, userID, role, content, intent, branch, meta)
|
|
return err
|
|
}
|
|
|
|
// RecordUserMessageWithID 记录单轮对话消息并返回生成的消息真实自增 ID
|
|
func (m *ContextManager) RecordUserMessageWithID(ctx context.Context, sessionID string, userID int64, role, content, intent, branch string, meta map[string]any) (int64, error) {
|
|
if sessionID == "" || content == "" {
|
|
return 0, errors.New("sessionID and content required")
|
|
}
|
|
|
|
shanghaiTZ := time.FixedZone("CST", 8*3600)
|
|
now := time.Now()
|
|
dayKey := now.In(shanghaiTZ).Format("2006-01-02")
|
|
if meta != nil {
|
|
if customTime, ok := meta["created_at"].(time.Time); ok && !customTime.IsZero() {
|
|
now = customTime
|
|
dayKey = now.In(shanghaiTZ).Format("2006-01-02")
|
|
} else if customTimeStr, ok := meta["created_at"].(string); ok && customTimeStr != "" {
|
|
if tParsed, err := time.Parse(time.RFC3339, customTimeStr); err == nil {
|
|
now = tParsed
|
|
dayKey = now.In(shanghaiTZ).Format("2006-01-02")
|
|
}
|
|
}
|
|
if customDayKey, ok := meta["day_key"].(string); ok && customDayKey != "" {
|
|
dayKey = customDayKey
|
|
}
|
|
}
|
|
category := DeriveMessageCategory(role, intent, branch, meta)
|
|
|
|
rec := ChatMessageRecord{
|
|
SessionID: sessionID,
|
|
UserID: userID,
|
|
Role: role,
|
|
Content: content,
|
|
Intent: intent,
|
|
Branch: branch,
|
|
Metadata: meta,
|
|
DayKey: dayKey,
|
|
Category: category,
|
|
CreatedAt: now,
|
|
}
|
|
|
|
var messageID int64
|
|
|
|
if m.pool != nil {
|
|
var uid *int64
|
|
if userID > 0 {
|
|
uid = &userID
|
|
}
|
|
var pid *int64
|
|
if meta != nil {
|
|
if pVal, ok := meta["project_id"].(int64); ok && pVal > 0 {
|
|
pid = &pVal
|
|
} else if pValFloat, ok := meta["project_id"].(float64); ok && pValFloat > 0 {
|
|
p := int64(pValFloat)
|
|
pid = &p
|
|
}
|
|
}
|
|
|
|
// 确保会话存在
|
|
_, _ = m.pool.Exec(ctx, `
|
|
INSERT INTO chat_sessions (session_id, user_id, project_id, title, created_at, updated_at)
|
|
VALUES ($1, $2, $3, '对话协同会话', now(), now())
|
|
ON CONFLICT (session_id) DO UPDATE SET
|
|
updated_at = now(),
|
|
user_id = COALESCE($2, chat_sessions.user_id),
|
|
project_id = COALESCE($3, chat_sessions.project_id)
|
|
`, sessionID, uid, pid)
|
|
|
|
metaJSON, _ := json.Marshal(meta)
|
|
err := m.pool.QueryRow(ctx, `
|
|
INSERT INTO chat_messages (session_id, user_id, role, content, intent, branch, metadata, day_key, category, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
RETURNING id
|
|
`, sessionID, uid, role, content, intent, branch, metaJSON, dayKey, category, now).Scan(&messageID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("insert chat message failed: %w", err)
|
|
}
|
|
rec.ID = messageID
|
|
} else {
|
|
m.mu.Lock()
|
|
rec.ID = m.nextMessageID
|
|
messageID = rec.ID
|
|
m.nextMessageID++
|
|
m.messages[sessionID] = append(m.messages[sessionID], rec)
|
|
|
|
sessSum, exists := m.sessions[sessionID]
|
|
if !exists {
|
|
sessSum = &ChatSessionSummary{
|
|
SessionID: sessionID,
|
|
Title: "对话协同会话",
|
|
UserID: userID,
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
m.sessions[sessionID] = sessSum
|
|
}
|
|
sessSum.MessageCount = len(m.messages[sessionID])
|
|
sessSum.LastMessage = content
|
|
sessSum.UpdatedAt = time.Now()
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// 若为用户输入,触发工作习惯偏好异步更新
|
|
if role == "user" && userID > 0 {
|
|
_ = m.ReflectUserPreference(ctx, userID, sessionID)
|
|
}
|
|
|
|
return messageID, nil
|
|
}
|
|
|
|
// RecordActivity 记录项目协同操作/决策/上传/下载活动
|
|
func (m *ContextManager) RecordActivity(ctx context.Context, projectID, userID int64, userName, actType, title string, detail map[string]any, weight float64, isMilestone bool) (*ActivityItem, error) {
|
|
if projectID <= 0 || title == "" {
|
|
return nil, errors.New("projectID and title required")
|
|
}
|
|
if weight <= 0 {
|
|
weight = 1.00
|
|
}
|
|
|
|
item := &ActivityItem{
|
|
ProjectID: projectID,
|
|
UserID: userID,
|
|
UserName: userName,
|
|
ActivityType: actType,
|
|
Title: title,
|
|
Detail: detail,
|
|
RawWeight: weight,
|
|
IsMilestone: isMilestone,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if m.pool != nil {
|
|
detailJSON, _ := json.Marshal(detail)
|
|
var actID int64
|
|
var createdAt time.Time
|
|
err := m.pool.QueryRow(ctx, `
|
|
INSERT INTO project_activities (project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, now())
|
|
RETURNING id, created_at
|
|
`, projectID, userID, userName, actType, title, detailJSON, weight, isMilestone).Scan(&actID, &createdAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("insert activity failed: %w", err)
|
|
}
|
|
item.ID = actID
|
|
item.CreatedAt = createdAt
|
|
} else {
|
|
m.mu.Lock()
|
|
item.ID = m.nextActivityID
|
|
m.nextActivityID++
|
|
m.activities[projectID] = append(m.activities[projectID], *item)
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
item.DecayedWeight = CalculateDecayedWeight(item.RawWeight, item.CreatedAt, item.IsMilestone, m.decayCfg)
|
|
return item, nil
|
|
}
|
|
|
|
// GetUserContext 装配单用户私有上下文 (包含个人偏好画像、近期会话、个人操作与经时间衰减的文件资料)
|
|
func (m *ContextManager) GetUserContext(ctx context.Context, projectID, userID int64, sessionID string) (*SingleUserContext, error) {
|
|
pref, _ := m.GetUserPreference(ctx, userID)
|
|
|
|
userCtx := &SingleUserContext{
|
|
UserID: userID,
|
|
Preference: pref,
|
|
SessionID: sessionID,
|
|
RecentChats: make([]ChatMessageRecord, 0),
|
|
MyActivities: make([]ActivityItem, 0),
|
|
MyFiles: make([]*storage.FileReference, 0),
|
|
AssembledAt: time.Now(),
|
|
}
|
|
|
|
// 1. 获取近期会话
|
|
if sessionID != "" {
|
|
if m.pool != nil {
|
|
rows, err := m.pool.Query(ctx, `
|
|
SELECT id, session_id, COALESCE(user_id, 0), role, content, intent, branch, metadata, created_at
|
|
FROM chat_messages
|
|
WHERE session_id = $1
|
|
ORDER BY created_at ASC LIMIT 30
|
|
`, sessionID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var r ChatMessageRecord
|
|
var metaJSON []byte
|
|
if err := rows.Scan(&r.ID, &r.SessionID, &r.UserID, &r.Role, &r.Content, &r.Intent, &r.Branch, &metaJSON, &r.CreatedAt); err == nil {
|
|
_ = json.Unmarshal(metaJSON, &r.Metadata)
|
|
userCtx.RecentChats = append(userCtx.RecentChats, r)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
m.mu.RLock()
|
|
userCtx.RecentChats = append(userCtx.RecentChats, m.messages[sessionID]...)
|
|
m.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// 2. 获取用户个人在项目内的活动并应用时间衰减
|
|
var rawActs []ActivityItem
|
|
if m.pool != nil {
|
|
rows, err := m.pool.Query(ctx, `
|
|
SELECT id, project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at
|
|
FROM project_activities
|
|
WHERE project_id = $1 AND user_id = $2
|
|
ORDER BY created_at DESC LIMIT 50
|
|
`, projectID, userID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var it ActivityItem
|
|
var detJSON []byte
|
|
if err := rows.Scan(&it.ID, &it.ProjectID, &it.UserID, &it.UserName, &it.ActivityType, &it.Title, &detJSON, &it.RawWeight, &it.IsMilestone, &it.CreatedAt); err == nil {
|
|
_ = json.Unmarshal(detJSON, &it.Detail)
|
|
rawActs = append(rawActs, it)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
m.mu.RLock()
|
|
for _, it := range m.activities[projectID] {
|
|
if it.UserID == userID {
|
|
rawActs = append(rawActs, it)
|
|
}
|
|
}
|
|
m.mu.RUnlock()
|
|
}
|
|
|
|
userCtx.MyActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg)
|
|
return userCtx, nil
|
|
}
|
|
|
|
// GetProjectContext 装配项目级多方协同上下文 (黑板事实、里程碑决策、团队活动流与共享文件)
|
|
func (m *ContextManager) GetProjectContext(ctx context.Context, projectID int64) (*ProjectCollectiveContext, error) {
|
|
pCtx := &ProjectCollectiveContext{
|
|
ProjectID: projectID,
|
|
BlackboardFacts: make(map[string]string),
|
|
AllActivities: make([]ActivityItem, 0),
|
|
MilestoneDecisions: make([]ActivityItem, 0),
|
|
SharedFiles: make([]*storage.FileReference, 0),
|
|
ParticipatingUsers: make([]UserSummary, 0),
|
|
AssembledAt: time.Now(),
|
|
}
|
|
|
|
if m.pool != nil {
|
|
// 1. 获取项目基准与黑板事实
|
|
var pCode, pName, cStage string
|
|
_ = m.pool.QueryRow(ctx, "SELECT code, name, current_stage FROM projects WHERE id = $1", projectID).Scan(&pCode, &pName, &cStage)
|
|
pCtx.ProjectCode = pCode
|
|
pCtx.ProjectName = pName
|
|
pCtx.CurrentStage = cStage
|
|
|
|
var factsJSON []byte
|
|
err := m.pool.QueryRow(ctx, "SELECT facts FROM project_blackboards WHERE project_id = $1", projectID).Scan(&factsJSON)
|
|
if err == nil {
|
|
_ = json.Unmarshal(factsJSON, &pCtx.BlackboardFacts)
|
|
}
|
|
|
|
// 2. 获取全项目活动
|
|
rows, err := m.pool.Query(ctx, `
|
|
SELECT id, project_id, user_id, user_name, activity_type, title, detail, weight, is_milestone, created_at
|
|
FROM project_activities
|
|
WHERE project_id = $1
|
|
ORDER BY created_at DESC LIMIT 100
|
|
`, projectID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
var rawActs []ActivityItem
|
|
for rows.Next() {
|
|
var it ActivityItem
|
|
var detJSON []byte
|
|
if err := rows.Scan(&it.ID, &it.ProjectID, &it.UserID, &it.UserName, &it.ActivityType, &it.Title, &detJSON, &it.RawWeight, &it.IsMilestone, &it.CreatedAt); err == nil {
|
|
_ = json.Unmarshal(detJSON, &it.Detail)
|
|
rawActs = append(rawActs, it)
|
|
if it.IsMilestone || it.ActivityType == "decision" {
|
|
pCtx.MilestoneDecisions = append(pCtx.MilestoneDecisions, it)
|
|
}
|
|
}
|
|
}
|
|
pCtx.AllActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg)
|
|
}
|
|
} else {
|
|
m.mu.RLock()
|
|
if existing, ok := m.projects[projectID]; ok {
|
|
pCtx.ProjectName = existing.ProjectName
|
|
pCtx.CurrentStage = existing.CurrentStage
|
|
for k, v := range existing.BlackboardFacts {
|
|
pCtx.BlackboardFacts[k] = v
|
|
}
|
|
}
|
|
rawActs := m.activities[projectID]
|
|
for _, it := range rawActs {
|
|
if it.IsMilestone || it.ActivityType == "decision" {
|
|
pCtx.MilestoneDecisions = append(pCtx.MilestoneDecisions, it)
|
|
}
|
|
}
|
|
pCtx.AllActivities = ApplyTimeDecayToActivities(rawActs, m.decayCfg)
|
|
m.mu.RUnlock()
|
|
}
|
|
|
|
return pCtx, nil
|
|
}
|
|
|
|
// UpdateProjectBlackboard 更新并持久化项目事实黑板数据
|
|
func (m *ContextManager) UpdateProjectBlackboard(ctx context.Context, projectID int64, facts map[string]string) error {
|
|
if projectID <= 0 {
|
|
return errors.New("invalid projectID")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
proj, ok := m.projects[projectID]
|
|
if !ok {
|
|
proj = &ProjectCollectiveContext{
|
|
ProjectID: projectID,
|
|
BlackboardFacts: make(map[string]string),
|
|
}
|
|
m.projects[projectID] = proj
|
|
}
|
|
if proj.BlackboardFacts == nil {
|
|
proj.BlackboardFacts = make(map[string]string)
|
|
}
|
|
for k, v := range facts {
|
|
proj.BlackboardFacts[k] = v
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
if m.pool != nil {
|
|
factsJSON, _ := json.Marshal(facts)
|
|
_, err := m.pool.Exec(ctx, `
|
|
INSERT INTO project_blackboards (project_id, facts, version, updated_at)
|
|
VALUES ($1, $2, 1, now())
|
|
ON CONFLICT (project_id) DO UPDATE SET
|
|
facts = project_blackboards.facts || EXCLUDED.facts,
|
|
version = project_blackboards.version + 1,
|
|
updated_at = now()
|
|
`, projectID, factsJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert project blackboard failed: %w", err)
|
|
}
|
|
}
|
|
m.Cache().InvalidateLayer(LayerC4Context)
|
|
return nil
|
|
}
|
|
|
|
// UpdateProjectStage 更新项目当前成熟度阶段
|
|
func (m *ContextManager) UpdateProjectStage(ctx context.Context, projectID int64, stage string) error {
|
|
if projectID <= 0 || stage == "" {
|
|
return errors.New("invalid projectID or stage")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
if proj, ok := m.projects[projectID]; ok {
|
|
proj.CurrentStage = stage
|
|
} else {
|
|
m.projects[projectID] = &ProjectCollectiveContext{
|
|
ProjectID: projectID,
|
|
CurrentStage: stage,
|
|
BlackboardFacts: make(map[string]string),
|
|
}
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
if m.pool != nil {
|
|
_, err := m.pool.Exec(ctx, `
|
|
UPDATE projects
|
|
SET current_stage = $2, updated_at = now()
|
|
WHERE id = $1
|
|
`, projectID, stage)
|
|
if err != nil {
|
|
return fmt.Errorf("update project stage failed: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildBlendedSquadContext 混合上下文装配引擎 (Context Blender)
|
|
// 将项目事实、里程碑决策、触发用户的工作习惯偏好合成最适合目标 Agent 角色执行的混合上下文
|
|
func (m *ContextManager) BuildBlendedSquadContext(ctx context.Context, projectID, triggerUserID int64, taskKey, role string) (*BlendedSquadContext, error) {
|
|
projCtx, err := m.GetProjectContext(ctx, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get project context failed: %w", err)
|
|
}
|
|
|
|
userPref, _ := m.GetUserPreference(ctx, triggerUserID)
|
|
guidance := m.reflector.GeneratePromptGuidance(userPref)
|
|
|
|
var decisions []string
|
|
for _, d := range projCtx.MilestoneDecisions {
|
|
decisions = append(decisions, fmt.Sprintf("[%s] %s (%s)", d.ActivityType, d.Title, d.UserName))
|
|
}
|
|
|
|
return &BlendedSquadContext{
|
|
ProjectName: projCtx.ProjectName,
|
|
CurrentStage: projCtx.CurrentStage,
|
|
TaskKey: taskKey,
|
|
AgentRole: role,
|
|
BlackboardFacts: projCtx.BlackboardFacts,
|
|
KeyDecisions: decisions,
|
|
TriggerUserPreference: userPref,
|
|
PersonalizedGuidance: guidance,
|
|
AssembledAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// CompilePromptGuidance 编译指定用户的个性化偏好指导语 (供对话 LLM 或智能体 Prompt 注入)
|
|
func (m *ContextManager) CompilePromptGuidance(ctx context.Context, userID int64) (string, error) {
|
|
if userID <= 0 {
|
|
return "", errors.New("invalid userID")
|
|
}
|
|
pref, err := m.GetUserPreference(ctx, userID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if pref == nil || m.reflector == nil {
|
|
return "", nil
|
|
}
|
|
return m.reflector.GeneratePromptGuidance(pref), nil
|
|
}
|
|
|
|
// GetUserPreference 获取用户偏好画像
|
|
func (m *ContextManager) GetUserPreference(ctx context.Context, userID int64) (*UserPreference, error) {
|
|
if userID <= 0 {
|
|
return nil, errors.New("invalid userID")
|
|
}
|
|
|
|
if m.pool != nil {
|
|
var pref UserPreference
|
|
var focusArr []string
|
|
var factsJSON []byte
|
|
err := m.pool.QueryRow(ctx, `
|
|
SELECT u.id, u.account_id, u.name, u.role, p.communication_style, p.focus_areas, p.output_format_pref, p.custom_instructions, p.memory_facts, p.updated_at
|
|
FROM users u
|
|
LEFT JOIN user_preferences p ON u.id = p.user_id
|
|
WHERE u.id = $1
|
|
`, userID).Scan(&pref.UserID, &pref.AccountID, &pref.Name, &pref.Role, &pref.CommunicationStyle, &focusArr, &pref.OutputFormatPref, &pref.CustomInstructions, &factsJSON, &pref.UpdatedAt)
|
|
|
|
if err == nil {
|
|
pref.FocusAreas = focusArr
|
|
_ = json.Unmarshal(factsJSON, &pref.MemoryFacts)
|
|
if pref.CommunicationStyle == "" {
|
|
pref.CommunicationStyle = StyleConciseStructured
|
|
}
|
|
return &pref, nil
|
|
}
|
|
}
|
|
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
if pref, ok := m.users[userID]; ok {
|
|
return pref, nil
|
|
}
|
|
|
|
return &UserPreference{
|
|
UserID: userID,
|
|
CommunicationStyle: StyleConciseStructured,
|
|
FocusAreas: []string{"reg_compliance", "timeline"},
|
|
OutputFormatPref: "standard_report",
|
|
MemoryFacts: make(map[string]string),
|
|
UpdatedAt: time.Now(),
|
|
}, nil
|
|
}
|
|
|
|
// UpdateUserPreference 保存用户偏好画像
|
|
func (m *ContextManager) UpdateUserPreference(ctx context.Context, pref *UserPreference) error {
|
|
if pref == nil || pref.UserID <= 0 {
|
|
return errors.New("invalid user preference")
|
|
}
|
|
|
|
pref.UpdatedAt = time.Now()
|
|
|
|
if m.pool != nil {
|
|
factsJSON, _ := json.Marshal(pref.MemoryFacts)
|
|
_, err := m.pool.Exec(ctx, `
|
|
INSERT INTO user_preferences (user_id, communication_style, focus_areas, output_format_pref, custom_instructions, memory_facts, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, now())
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
communication_style = EXCLUDED.communication_style,
|
|
focus_areas = EXCLUDED.focus_areas,
|
|
output_format_pref = EXCLUDED.output_format_pref,
|
|
custom_instructions = EXCLUDED.custom_instructions,
|
|
memory_facts = EXCLUDED.memory_facts,
|
|
updated_at = now()
|
|
`, pref.UserID, string(pref.CommunicationStyle), pref.FocusAreas, pref.OutputFormatPref, pref.CustomInstructions, factsJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert user preference failed: %w", err)
|
|
}
|
|
}
|
|
|
|
m.mu.Lock()
|
|
m.users[pref.UserID] = pref
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// ReflectUserPreference 根据用户在指定会话的历史记录智能总结工作习惯与偏好
|
|
func (m *ContextManager) ReflectUserPreference(ctx context.Context, userID int64, sessionID string) error {
|
|
var msgs []ChatMessageRecord
|
|
if m.pool != nil {
|
|
rows, err := m.pool.Query(ctx, `
|
|
SELECT id, session_id, user_id, role, content, intent, branch, metadata, created_at
|
|
FROM chat_messages
|
|
WHERE user_id = $1 OR session_id = $2
|
|
ORDER BY created_at DESC LIMIT 20
|
|
`, userID, sessionID)
|
|
if err == nil {
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var r ChatMessageRecord
|
|
var metaJSON []byte
|
|
if err := rows.Scan(&r.ID, &r.SessionID, &r.UserID, &r.Role, &r.Content, &r.Intent, &r.Branch, &metaJSON, &r.CreatedAt); err == nil {
|
|
_ = json.Unmarshal(metaJSON, &r.Metadata)
|
|
msgs = append(msgs, r)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
m.mu.RLock()
|
|
msgs = append(msgs, m.messages[sessionID]...)
|
|
m.mu.RUnlock()
|
|
}
|
|
|
|
if len(msgs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
currentPref, _ := m.GetUserPreference(ctx, userID)
|
|
updatedPref := m.reflector.ReflectFromMessages(msgs, currentPref)
|
|
return m.UpdateUserPreference(ctx, updatedPref)
|
|
}
|