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) {
|
2026-10-08 10:06:44 +08:00
|
|
|
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) {
|
2026-08-26 06:41:07 +08:00
|
|
|
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
|
2026-10-08 10:06:44 +08:00
|
|
|
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')
|
2026-08-26 06:41:07 +08:00
|
|
|
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
|
2026-10-08 10:06:44 +08:00
|
|
|
LIMIT $4`, tenantID, subjectID, queryVector, limit, responding)
|
2026-08-26 06:41:07 +08:00
|
|
|
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
|
|
|
|
|
}
|