mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-24 19:08:10 +00:00
feat(memory): Graph 原生一等记忆块节点与结构边(§13.12)
媒体/文本在 L3 不再是正文标记反解出的代理实体,而是带原生 modality/digest/MIME/size/vector/fingerprint 的 memory_blocks 节点; contains/depicts/derived_from 等语义边落在 memory_block_edges, 端点必须是真实图节点(block/entity/sentence),不复用 owner 字符串。 本步只建立存储与查询能力,不接入 media_refs,也不改变 L0/L2 路径。
This commit is contained in:
@ -2,8 +2,8 @@ package memory
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -434,6 +434,84 @@ func TestMergeEntitiesNonexistent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryBlocksAreFirstClassGraphNodes(t *testing.T) {
|
||||
g := newTestGraph(t)
|
||||
defer os.Remove(g.dbPath)
|
||||
defer g.Close()
|
||||
|
||||
image := MemoryBlock{
|
||||
ID: "block_image_1", Modality: BlockImage,
|
||||
PayloadDigest: "0123456789abcdef", MIME: "image/png", Size: 1234,
|
||||
Width: 768, Height: 512, Vector: []float64{0.1, 0.2, 0.3},
|
||||
Fingerprint: "qwen:test", Source: "qq", Tool: "upload",
|
||||
}
|
||||
text := MemoryBlock{ID: "block_text_1", Modality: BlockText, Text: "用户上传了一张架构图"}
|
||||
if err := g.PutMemoryBlocks([]MemoryBlock{image, text}); err != nil {
|
||||
t.Fatalf("PutMemoryBlocks: %v", err)
|
||||
}
|
||||
if err := g.AddMemoryBlockEdge("block", text.ID, "block", image.ID, "contains"); err != nil {
|
||||
t.Fatalf("AddMemoryBlockEdge: %v", err)
|
||||
}
|
||||
|
||||
blocks, err := g.MemoryBlocks()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(blocks) != 2 {
|
||||
t.Fatalf("memory blocks=%d, want 2", len(blocks))
|
||||
}
|
||||
var gotImage *MemoryBlock
|
||||
for i := range blocks {
|
||||
if blocks[i].ID == image.ID {
|
||||
gotImage = &blocks[i]
|
||||
}
|
||||
}
|
||||
if gotImage == nil || gotImage.Modality != BlockImage || gotImage.PayloadDigest != image.PayloadDigest || gotImage.Fingerprint != image.Fingerprint || len(gotImage.Vector) != 3 {
|
||||
t.Fatalf("image block not round-tripped: %+v", gotImage)
|
||||
}
|
||||
|
||||
edges, err := g.MemoryBlockEdges()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(edges) != 1 || edges[0].Type != "contains" || edges[0].SourceID != text.ID || edges[0].TargetID != image.ID {
|
||||
t.Fatalf("memory block edges=%+v", edges)
|
||||
}
|
||||
|
||||
graph, err := g.GraphData()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
graphBlocks, ok := graph["memory_blocks"].([]MemoryBlock)
|
||||
if !ok || len(graphBlocks) != 2 {
|
||||
t.Fatalf("GraphData memory_blocks=%T %+v", graph["memory_blocks"], graph["memory_blocks"])
|
||||
}
|
||||
graphEdges, ok := graph["memory_block_edges"].([]MemoryBlockEdge)
|
||||
if !ok || len(graphEdges) != 1 {
|
||||
t.Fatalf("GraphData memory_block_edges=%T %+v", graph["memory_block_edges"], graph["memory_block_edges"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemoryBlockEdgeRejectsMissingEndpoint(t *testing.T) {
|
||||
g := newTestGraph(t)
|
||||
defer os.Remove(g.dbPath)
|
||||
defer g.Close()
|
||||
|
||||
if err := g.PutMemoryBlocks([]MemoryBlock{{ID: "known", Modality: BlockText, Text: "known"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := g.AddMemoryBlockEdge("block", "known", "block", "missing", "derived_from"); err == nil {
|
||||
t.Fatal("edge to missing node must fail")
|
||||
}
|
||||
edges, err := g.MemoryBlockEdges()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(edges) != 0 {
|
||||
t.Fatalf("failed transaction left edges: %+v", edges)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceholders(t *testing.T) {
|
||||
if placeholders(0) != "NULL" {
|
||||
t.Errorf("expected NULL for n=0, got %s", placeholders(0))
|
||||
|
||||
Reference in New Issue
Block a user