mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
feat(knowledge): 分类过滤与多模态打通到工具/插件边界
- internal/sdk:KnowledgeAPI 增加 SearchIn/AddWithMedia/AttachMedia/ ReindexDense/DenseStats;新增 MediaAPI(Put/Stat/Get)与 PluginSDK.Media(), registry 装配。**公开 SDK 契约一字未动** —— third_party/homeagent-sdk/sdk/ 的 diff 恒为 0(发布纪律的硬约束),走 internal/sdk 这条明确不受冻结约束 的内部扩展路径。 - proc:新增 knowledge.addWithMedia(正文与媒体清单都走共享内存,与 doc.insertWithMedia 同形);knowledge.search 支持 category。 - corehandler 用**局部接口 + 类型断言**取扩展能力,而非直接引 internal/sdk: 后者已依赖 internal/plugin 的类型,直接引会成环(CoreSDK 注释已警示)。 断言失败明确报"能力不可用",不静默退化成"媒体已写入"。 - agent 工具面:knowledge_search 增加 category;knowledge_create 增加 media_digests(复用 doc_commit 的 digest 前缀解析 + Stat 回读 MIME)。 顺带修 knowledge_search 的分类前缀重复(Name 已含分类,旧代码又拼一次, 实测输出 tech/go/tech/go/并发)。 - agent 启动接线多模态空间,顺序为先接线再 ReindexDense(否则首次启动 算出的向量因 MediaStore 未就绪而不落盘)。
This commit is contained in:
@ -335,6 +335,22 @@ func New(cfg AgentConfig) *Agent {
|
||||
cfg.DocStore.SetDenseSpace(cfg.MultimodalSpace)
|
||||
cfg.DocStore.BuildDenseIndex(cfg.MultimodalSpace)
|
||||
}
|
||||
// 知识库接入同一多模态空间:媒体作为一等节点参与稠密召回,
|
||||
// 于是「按图搜知识」「按文搜含图知识」成立。
|
||||
//
|
||||
// 稀疏两路(词向量 + TF-IDF)**保持启用**且仍是主召回路径:多模态
|
||||
// 空间未配置时知识库行为与此前逐字一致(退化为 0.5/0.5 两路融合)。
|
||||
if cfg.Knowledge != nil {
|
||||
// 顺序要紧:先接线(含 MediaStore),再重建。ReindexDense 会
|
||||
// 尝试从 .dense.json 缓存恢复,恢复不了才重算,最后把结果落盘。
|
||||
// 若先重建后接线,首次启动算出的向量会被丢掉而不落盘。
|
||||
cfg.Knowledge.SetDenseSpace(cfg.MultimodalSpace)
|
||||
cfg.Knowledge.SetMediaGetter(cfg.MediaStore)
|
||||
built, skipped := cfg.Knowledge.ReindexDense()
|
||||
if built > 0 || skipped > 0 {
|
||||
log.Printf("[knowledge] 多模态稠密索引: 新建 %d 跳过 %d(其余命中缓存)", built, skipped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a := &Agent{
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/text"
|
||||
@ -536,6 +537,30 @@ func (a *Agent) executeSocialTool(tc agentAPI.ToolCall) string {
|
||||
}
|
||||
}
|
||||
|
||||
// knowledgeMediaRefs 把模型给的 digest 列表解析成知识条目的媒体引用。
|
||||
//
|
||||
// 复用 doc_commit 的既有约定:digest 可传前缀(ResolvePrefix),解析不了
|
||||
// 的跳过而不是报错——模型偶尔会把 digest 记错,不该让整次写入失败。
|
||||
// MIME 从媒体存储回读,嵌入时需要(EmbedImageDense 靠它判定模态)。
|
||||
func (a *Agent) knowledgeMediaRefs(digests []string) []knowledge.KnowledgeMediaRef {
|
||||
if a.mediaStore == nil || len(digests) == 0 {
|
||||
return nil
|
||||
}
|
||||
var out []knowledge.KnowledgeMediaRef
|
||||
for _, d := range a.resolveMediaDigests(digests) {
|
||||
it, err := a.mediaStore.Stat(d)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, knowledge.KnowledgeMediaRef{
|
||||
Digest: it.Digest,
|
||||
MIME: it.MIME,
|
||||
Kind: string(it.Kind),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Agent) executeKnowledgeTool(tc agentAPI.ToolCall) string {
|
||||
if a.knowledge == nil {
|
||||
return "知识库不可用"
|
||||
@ -550,7 +575,10 @@ func (a *Agent) executeKnowledgeTool(tc agentAPI.ToolCall) string {
|
||||
if query == "" {
|
||||
return "请输入查询关键词"
|
||||
}
|
||||
results := a.knowledge.Search(query, topK)
|
||||
// 可选分类限定:把召回限制在某棵分类子树内(前缀匹配,见
|
||||
// knowledge.Store.SearchIn)。不传 = 全库。
|
||||
category, _ := tc.Arguments["category"].(string)
|
||||
results := a.knowledge.SearchIn(query, category, topK)
|
||||
if len(results) == 0 {
|
||||
return "未找到相关知识"
|
||||
}
|
||||
@ -559,11 +587,9 @@ func (a *Agent) executeKnowledgeTool(tc agentAPI.ToolCall) string {
|
||||
if i >= topK {
|
||||
break
|
||||
}
|
||||
label := k.Name
|
||||
if k.Category != "" {
|
||||
label = k.Category + "/" + k.Name
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("[%s]\n%s", label, truncateStr(k.Content, 200)))
|
||||
// Name 已是含分类的规范名("tech/go/并发"),分类前缀就在里面。
|
||||
// 曾经这里再拼一次 Category,输出成 "tech/go/tech/go/并发"(实测)。
|
||||
parts = append(parts, fmt.Sprintf("[%s]\n%s", k.Name, truncateStr(k.Content, 200)))
|
||||
}
|
||||
return strings.Join(parts, "\n---\n")
|
||||
|
||||
@ -573,9 +599,16 @@ func (a *Agent) executeKnowledgeTool(tc agentAPI.ToolCall) string {
|
||||
if name == "" || content == "" {
|
||||
return "name 和 content 不能为空"
|
||||
}
|
||||
if err := a.knowledge.Add(name, content); err != nil {
|
||||
// 模型可显式关联已入库的媒体(与 doc_commit 的 media_digests 同形)。
|
||||
// 这些媒体成为知识条目的一等节点:其向量会与正文向量融合,
|
||||
// 使该条目能按图本身被召回,而不依赖任何生成的描述文本。
|
||||
media := a.knowledgeMediaRefs(getStringSlice(tc.Arguments, "media_digests"))
|
||||
if err := a.knowledge.AddWithMedia(name, content, media); err != nil {
|
||||
return fmt.Sprintf("知识创建失败: %v", err)
|
||||
}
|
||||
if len(media) > 0 {
|
||||
return fmt.Sprintf("知识「%s」已创建并向量化索引(%d 字符,%d 个媒体参与跨模态召回)", name, len(content), len(media))
|
||||
}
|
||||
return fmt.Sprintf("知识「%s」已创建并向量化索引(%d 字符)", name, len(content))
|
||||
|
||||
case "knowledge_list":
|
||||
|
||||
@ -373,17 +373,23 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
}
|
||||
|
||||
if a.knowledge != nil {
|
||||
tools = append(tools, toolDef("knowledge_search", "搜索知识库。输入查询关键词,返回相关知识内容。", map[string]interface{}{
|
||||
"query": map[string]interface{}{"type": "string", "description": "查询关键词"},
|
||||
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 5},
|
||||
tools = append(tools, toolDef("knowledge_search", "搜索知识库。输入查询关键词,返回相关知识内容。可用 category 把搜索限定在某个分类子树内。", map[string]interface{}{
|
||||
"query": map[string]interface{}{"type": "string", "description": "查询关键词"},
|
||||
"top_k": map[string]interface{}{"type": "integer", "description": "返回数量", "default": 5},
|
||||
"category": map[string]interface{}{"type": "string", "description": "可选:限定在某个分类内(前缀匹配子树,如 tech 会搜 tech/go、tech/rust)。留空则搜全库"},
|
||||
}, "query"))
|
||||
tools = append(tools, toolDef("knowledge_list", "列出知识库中所有知识分类。", map[string]interface{}{}))
|
||||
}
|
||||
|
||||
if a.knowledge != nil {
|
||||
tools = append(tools, toolDef("knowledge_create", "创建新知识。将知识写入知识库(knowledge/目录),自动向量化索引。", map[string]interface{}{
|
||||
tools = append(tools, toolDef("knowledge_create", "创建新知识。将知识写入知识库(knowledge/目录),自动向量化索引。可关联已入库媒体(附图/音视频)使该知识能被图本身检索到。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "知识名称(用作目录名)"},
|
||||
"content": map[string]interface{}{"type": "string", "description": "知识内容,支持 Markdown"},
|
||||
"media_digests": map[string]interface{}{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{"type": "string"},
|
||||
"description": "可选:关联的媒体 digest(可传前缀)。媒体作为一等节点参与跨模态检索——知识能按图本身被搜到,而不依赖生成的描述文本",
|
||||
},
|
||||
}, "name", "content"))
|
||||
tools = append(tools, toolDef("knowledge_delete", "删除知识库中的指定知识条目。", map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "要删除的知识名称"},
|
||||
|
||||
Reference in New Issue
Block a user