Files
story/internal/retrieval/memory.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

88 lines
3.9 KiB
Go

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
}