Files
HomeAgent/internal/sdk/indexer_impl.go
JianFeeeee b850feaef2 feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边

命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据

安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据

验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
2026-10-29 23:53:48 +08:00

132 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
import (
"errors"
"strings"
"time"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/memory/coldcontext"
)
// indexerImpl 桥接 memory.Indexer 到中立的 IndexerAPI。
type indexerImpl struct {
idx *memory.Indexer
docs *coldcontext.Store // 可选:给「上下文」诊断面提供 L2 那一路
}
func NewIndexer(idx *memory.Indexer) IndexerAPI {
return &indexerImpl{idx: idx}
}
// NewIndexerWithDocs 让诊断面同时覆盖 L2 冷上下文与 L3 图记忆(双路召回)。
func NewIndexerWithDocs(idx *memory.Indexer, docs *coldcontext.Store) IndexerAPI {
return &indexerImpl{idx: idx, docs: docs}
}
func (i *indexerImpl) BuildContext(q string) (*IndexContext, error) {
if i.idx == nil {
return nil, errors.New("indexer not available")
}
ctx := i.idx.BuildContext(q)
if ctx == nil {
return &IndexContext{}, nil
}
entities := make([]Entity, len(ctx.Entities))
for j, e := range ctx.Entities {
entities[j] = Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount}
}
relations := make([]Relation, len(ctx.Relations))
for j, r := range ctx.Relations {
relations[j] = Relation{SourceName: r.SourceName, TargetName: r.TargetName, RelationType: r.RelationType}
}
out := &IndexContext{
Entities: entities,
Relations: relations,
Summary: ctx.Summary,
TokenEstimate: ctx.TokenEstimate,
}
out.Docs = i.queryDocs(q, 8)
return out, nil
}
func (i *indexerImpl) FormatContext(ctx *IndexContext) string {
if i.idx == nil || ctx == nil {
return ""
}
entities := make([]memory.Entity, len(ctx.Entities))
for j, e := range ctx.Entities {
entities[j] = memory.Entity{Name: e.Name, Type: e.Type, MentionCount: e.MentionCount}
}
return i.idx.FormatContext(&memory.InjectedContext{
Entities: entities,
Summary: ctx.Summary,
TokenEstimate: ctx.TokenEstimate,
})
}
// queryDocs 取 L2 那一路的命中片段(与模型预填充同一检索路径)。
func (i *indexerImpl) queryDocs(q string, limit int) []DocSnippet {
if i.docs == nil || strings.TrimSpace(q) == "" {
return nil
}
hits := i.docs.QueryScored(q, limit)
out := make([]DocSnippet, 0, len(hits))
for _, h := range hits {
if h.Doc == nil {
continue
}
when := h.Doc.CreatedAt.Format(time.DateTime)
if h.Doc.HasEvents() {
when = h.Doc.Events[0].Timestamp.Format(time.DateTime)
}
// 正文在事件的**文本块**里(DocEvent 只有 Blocks,没有 Input/Response)。
text := coldcontextFirstText(h.Doc)
out = append(out, DocSnippet{ID: h.Doc.ID, When: when, Text: truncateRunes(text, 120), Score: h.Score})
}
return out
}
// coldcontextFirstText 取文档可用于展示的首段正文(跳过媒体块)。
func coldcontextFirstText(d *coldcontext.ColdDoc) string {
if d == nil {
return ""
}
for _, ev := range d.Events {
for _, b := range ev.Blocks {
if b.Modality != "" && b.Modality != "text" {
continue
}
if t := strings.TrimSpace(b.Text); t != "" {
return t
}
}
}
return d.Content
}
func truncateRunes(s string, n int) string {
s = strings.Join(strings.Fields(s), " ")
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[:n]) + "…"
}
func (i *indexerImpl) GetToolDefinitions() []map[string]interface{} {
if i.idx == nil {
return nil
}
return i.idx.GetToolDefinitions()
}
func (i *indexerImpl) BuildToolPrompt() string {
if i.idx == nil {
return ""
}
return i.idx.BuildToolPrompt()
}
var _ IndexerAPI = (*indexerImpl)(nil)