package retrieval import ( "context" "encoding/json" "fmt" "data-terminal-cloud/internal/developmentembedding" "github.com/jackc/pgx/v5" ) type Memory struct { MemoryID string `json:"memory_id"` MemoryVersionID string `json:"memory_version_id"` VersionNo int64 `json:"version_no"` Content string `json:"-"` SourceRefs json.RawMessage `json:"source_refs"` Sensitivity string `json:"sensitivity"` Relevance float64 `json:"relevance"` } // SelectMemoryForContext fixes the tenant and subject boundary in the SQL // itself. Callers cannot supply a free-form filter. The first production // baseline admits only normal-sensitivity memories into a voice context. func SelectMemoryForContext(ctx context.Context, tx pgx.Tx, tenantID, subjectID, query string, limit int) ([]Memory, error) { queryVector := developmentembedding.Literal(developmentembedding.Vector(query)) return SelectMemoryForContextVector(ctx, tx, tenantID, subjectID, queryVector, limit) } // SelectMemoryForContextVector uses a vector generated before the caller's // transaction. This keeps production embedding network calls out of the // conversation creation transaction while retaining the SQL tenant, subject, // consent and lifecycle boundaries below. func SelectMemoryForContextVector(ctx context.Context, tx pgx.Tx, tenantID, subjectID, queryVector string, limit int) ([]Memory, error) { return selectMemory(ctx, tx, tenantID, subjectID, queryVector, limit, false) } // RESPOND_ONLY and closed items can answer an explicit question, but cannot // seed a proactive greeting or follow-up prompt. func SelectMemoryForUserQueryVector(ctx context.Context, tx pgx.Tx, tenantID, subjectID, queryVector string, limit int) ([]Memory, error) { return selectMemory(ctx, tx, tenantID, subjectID, queryVector, limit, true) } func selectMemory(ctx context.Context, tx pgx.Tx, tenantID, subjectID, queryVector string, limit int, responding bool) ([]Memory, error) { if limit < 1 || limit > 10 { return nil, fmt.Errorf("memory retrieval limit must be between 1 and 10") } if queryVector == "" { return nil, fmt.Errorf("memory retrieval vector is required") } rows, err := tx.Query(ctx, ` SELECT cm.public_id::text,mv.public_id::text,mv.version_no,mv.content,mv.source_refs, cm.sensitivity,1-(mi.embedding <=> $3::vector) AS relevance FROM memory_index_entry mi JOIN curated_memory cm ON cm.id=mi.memory_id AND cm.tenant_id=mi.tenant_id AND cm.subject_id=mi.subject_id JOIN memory_version mv ON mv.id=mi.memory_version_id AND mv.memory_id=cm.id WHERE mi.tenant_id=$1::uuid AND mi.subject_id=$2::uuid AND mi.index_type='pgvector' AND mi.status='ACTIVE' AND mi.embedding IS NOT NULL AND cm.status='ACTIVE' AND cm.current_version_id=mv.id AND cm.proactive_policy<>'NEVER' AND ($5 OR cm.proactive_policy='ALLOWED') AND ($5 OR cm.memory_type<>'OPEN_ITEM' OR cm.item_status='OPEN') AND cm.sensitivity='normal' AND (cm.valid_from IS NULL OR cm.valid_from<=now()) AND (cm.valid_to IS NULL OR cm.valid_to>now()) AND EXISTS(SELECT 1 FROM consent_record cr WHERE cr.tenant_id=cm.tenant_id AND cr.subject_id=cm.subject_id AND cr.consent_type='personalized_memory' AND cr.withdrawn_at IS NULL AND (cr.expires_at IS NULL OR cr.expires_at>now())) ORDER BY mi.embedding <=> $3::vector,mv.confirmed_at DESC,mv.id LIMIT $4`, tenantID, subjectID, queryVector, limit, responding) if err != nil { return nil, fmt.Errorf("query eligible memory: %w", err) } defer rows.Close() items := make([]Memory, 0, limit) for rows.Next() { var item Memory if err := rows.Scan(&item.MemoryID, &item.MemoryVersionID, &item.VersionNo, &item.Content, &item.SourceRefs, &item.Sensitivity, &item.Relevance); err != nil { return nil, fmt.Errorf("scan eligible memory: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate eligible memory: %w", err) } return items, nil }