Files
story/internal/retrieval/memory.go
T

77 lines
3.2 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
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) {
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.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)
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
}