chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package retrieval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"data-terminal-cloud/internal/developmentembedding"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestSelectMemoryForContextAppliesEligibilityBoundary(t *testing.T) {
|
||||
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
t.Skip("TEST_DATABASE_URL is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
type fixture struct{ memoryID, memoryPublicID, versionID, versionPublicID, sensitivity, content string }
|
||||
fixtures := []fixture{
|
||||
{uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), "normal", "长者在河边小学教书。"},
|
||||
{uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), "sensitive", "合成敏感记忆不得进入语音上下文。"},
|
||||
}
|
||||
defer func() {
|
||||
for _, item := range fixtures {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM memory_index_entry WHERE memory_id=$1::uuid`, item.memoryID)
|
||||
_, _ = pool.Exec(ctx, `UPDATE curated_memory SET current_version_id=NULL WHERE id=$1::uuid`, item.memoryID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM memory_version WHERE memory_id=$1::uuid`, item.memoryID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM curated_memory WHERE id=$1::uuid`, item.memoryID)
|
||||
}
|
||||
}()
|
||||
for _, item := range fixtures {
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO curated_memory(id,public_id,tenant_id,subject_id,status,sensitivity,valid_from)
|
||||
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000401','ACTIVE',$3,now())`,
|
||||
item.memoryID, item.memoryPublicID, item.sensitivity); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO memory_version(id,public_id,memory_id,version_no,content,source_refs,confirmed_by,content_digest)
|
||||
VALUES($1::uuid,$2::uuid,$3::uuid,1,$4,'[{"type":"integration"}]','10000000-0000-0000-0000-000000000101',$5)`,
|
||||
item.versionID, item.versionPublicID, item.memoryID, item.content, strings.Repeat("b", 64)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `UPDATE curated_memory SET current_version_id=$1::uuid WHERE id=$2::uuid`, item.versionID, item.memoryID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO memory_index_entry(tenant_id,subject_id,memory_id,memory_version_id,index_type,embedding,status,provider_ref)
|
||||
VALUES('10000000-0000-0000-0000-000000000001','10000000-0000-0000-0000-000000000401',$1::uuid,$2::uuid,'pgvector',$3::vector,'ACTIVE','integration')`,
|
||||
item.memoryID, item.versionID, developmentembedding.Literal(developmentembedding.Vector(item.content))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := SelectMemoryForContext(ctx, tx, "10000000-0000-0000-0000-000000000001", "10000000-0000-0000-0000-000000000401", "河边小学", 0); err == nil {
|
||||
t.Fatal("invalid retrieval limit was accepted")
|
||||
}
|
||||
items, err := SelectMemoryForContext(ctx, tx, "10000000-0000-0000-0000-000000000001", "10000000-0000-0000-0000-000000000401", "河边小学", 5)
|
||||
if err != nil || len(items) != 1 {
|
||||
t.Fatalf("eligible memories=%+v err=%v", items, err)
|
||||
}
|
||||
if items[0].MemoryID != fixtures[0].memoryPublicID || items[0].MemoryVersionID != fixtures[0].versionPublicID || items[0].Content != fixtures[0].content || items[0].Sensitivity != "normal" {
|
||||
t.Fatalf("retrieved memory=%+v", items[0])
|
||||
}
|
||||
otherSubjectItems, err := SelectMemoryForContext(ctx, tx, "10000000-0000-0000-0000-000000000001", "10000000-0000-0000-0000-000000000402", "河边小学", 5)
|
||||
if err != nil || len(otherSubjectItems) != 0 {
|
||||
t.Fatalf("cross-subject memories=%+v err=%v", otherSubjectItems, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user