package memory import ( "context" "encoding/json" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) // Service manages user memory and preferences type Service interface { GetProfile(ctx context.Context, userID, tenantID string) (*UserProfile, error) SaveProfile(ctx context.Context, profile *UserProfile) error GetHistory(ctx context.Context, userID, tenantID string, limit int) ([]ConversationHistory, error) SaveHistory(ctx context.Context, history *ConversationHistory) error } // UserProfile represents user preferences and memory type UserProfile struct { UserID string TenantID string Preferences map[string]interface{} Context map[string]interface{} CreatedAt string UpdatedAt string } // ConversationHistory represents a conversation history entry type ConversationHistory struct { ID string UserID string TenantID string SessionID string Messages []Message CreatedAt string } // Message represents a message in history type Message struct { Role string Content string Timestamp string } // MemoryService implements memory using PostgreSQL type MemoryService struct { db *pgxpool.Pool } // NewMemoryService creates a new memory service func NewMemoryService(db *pgxpool.Pool) *MemoryService { return &MemoryService{ db: db, } } // GetProfile gets user profile func (s *MemoryService) GetProfile(ctx context.Context, userID, tenantID string) (*UserProfile, error) { query := ` SELECT user_id, tenant_id, preferences, context, created_at, updated_at FROM user_profiles WHERE user_id = $1 AND tenant_id = $2 ` var profile UserProfile var prefsJSON, contextJSON []byte err := s.db.QueryRow(ctx, query, userID, tenantID).Scan( &profile.UserID, &profile.TenantID, &prefsJSON, &contextJSON, &profile.CreatedAt, &profile.UpdatedAt, ) if err != nil { // Return default profile if not found return &UserProfile{ UserID: userID, TenantID: tenantID, Preferences: make(map[string]interface{}), Context: make(map[string]interface{}), }, nil } if err := json.Unmarshal(prefsJSON, &profile.Preferences); err != nil { profile.Preferences = make(map[string]interface{}) } if err := json.Unmarshal(contextJSON, &profile.Context); err != nil { profile.Context = make(map[string]interface{}) } return &profile, nil } // SaveProfile saves user profile func (s *MemoryService) SaveProfile(ctx context.Context, profile *UserProfile) error { prefsJSON, _ := json.Marshal(profile.Preferences) contextJSON, _ := json.Marshal(profile.Context) query := ` INSERT INTO user_profiles (user_id, tenant_id, preferences, context, created_at, updated_at) VALUES ($1, $2, $3, $4, NOW(), NOW()) ON CONFLICT (user_id, tenant_id) DO UPDATE SET preferences = $3, context = $4, updated_at = NOW() ` _, err := s.db.Exec(ctx, query, profile.UserID, profile.TenantID, prefsJSON, contextJSON) return err } // GetHistory gets conversation history func (s *MemoryService) GetHistory(ctx context.Context, userID, tenantID string, limit int) ([]ConversationHistory, error) { if limit <= 0 { limit = 10 } query := ` SELECT id, user_id, tenant_id, session_id, messages, created_at FROM conversation_history WHERE user_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT $3 ` rows, err := s.db.Query(ctx, query, userID, tenantID, limit) if err != nil { return nil, fmt.Errorf("failed to query: %w", err) } defer rows.Close() var histories []ConversationHistory for rows.Next() { var history ConversationHistory var messagesJSON []byte if err := rows.Scan(&history.ID, &history.UserID, &history.TenantID, &history.SessionID, &messagesJSON, &history.CreatedAt); err != nil { continue } if err := json.Unmarshal(messagesJSON, &history.Messages); err != nil { history.Messages = []Message{} } histories = append(histories, history) } return histories, nil } // SaveHistory saves conversation history func (s *MemoryService) SaveHistory(ctx context.Context, history *ConversationHistory) error { messagesJSON, _ := json.Marshal(history.Messages) query := ` INSERT INTO conversation_history (id, user_id, tenant_id, session_id, messages, created_at) VALUES ($1, $2, $3, $4, $5, NOW()) ` _, err := s.db.Exec(ctx, query, history.ID, history.UserID, history.TenantID, history.SessionID, messagesJSON) return err }