mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
refactor: output channel interface (payload/meta/type) + memory fixes
- Redesign output_send__ tools: content JSON string -> structured payload/meta/type params for LLM reliability - executeOutputSendTool: route by type with capability check - executeOutputSendHelp: show meta format + type enum - Updated system prompt rules for new interface - docToTriples: use jieba exact mode adjacent co-occurrence - Unify vector space: Doc.Vector field, ContextToDoc vectorizer, ReindexWithVectorizer on startup
This commit is contained in:
2
go.mod
2
go.mod
@ -3,7 +3,7 @@ module gitcode.com/JianFeeeee/HomeAgent
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.48
|
||||||
github.com/yuin/gopher-lua v1.1.2
|
github.com/yuin/gopher-lua v1.1.2
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -1,5 +1,7 @@
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||||
github.com/yanyiwu/gojieba v1.4.7 h1:2YkXELcYLTE0SJetq6xv4MjpEikWga6VpFn4jIFFQ/k=
|
github.com/yanyiwu/gojieba v1.4.7 h1:2YkXELcYLTE0SJetq6xv4MjpEikWga6VpFn4jIFFQ/k=
|
||||||
github.com/yanyiwu/gojieba v1.4.7/go.mod h1:JUq4DddFVGdHXJHxxepxRmhrKlDpaBxR8O28v6fKYLY=
|
github.com/yanyiwu/gojieba v1.4.7/go.mod h1:JUq4DddFVGdHXJHxxepxRmhrKlDpaBxR8O28v6fKYLY=
|
||||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||||
|
|||||||
@ -165,6 +165,13 @@ func New(cfg AgentConfig) *Agent {
|
|||||||
if cfg.MaxContextSize <= 0 {
|
if cfg.MaxContextSize <= 0 {
|
||||||
cfg.MaxContextSize = 30
|
cfg.MaxContextSize = 30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
embedder := memory.NewStaticEmbedder(strings.Split(cfg.EmbeddingModelPath, ",")...)
|
||||||
|
if cfg.DocStore != nil {
|
||||||
|
cfg.DocStore.SetVectorizer(embedder)
|
||||||
|
cfg.DocStore.ReindexWithVectorizer(embedder)
|
||||||
|
}
|
||||||
|
|
||||||
return &Agent{
|
return &Agent{
|
||||||
id: cfg.ID,
|
id: cfg.ID,
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
@ -175,7 +182,7 @@ func New(cfg AgentConfig) *Agent {
|
|||||||
indexer: cfg.Indexer,
|
indexer: cfg.Indexer,
|
||||||
skills: cfg.Skills,
|
skills: cfg.Skills,
|
||||||
tracker: cfg.Tracker,
|
tracker: cfg.Tracker,
|
||||||
context: NewRelevanceContext(cfg.ContextSavePath, memory.NewStaticEmbedder(strings.Split(cfg.EmbeddingModelPath, ",")...)),
|
context: NewRelevanceContext(cfg.ContextSavePath, embedder),
|
||||||
systemPrompt: cfg.SystemPrompt,
|
systemPrompt: cfg.SystemPrompt,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
@ -734,6 +741,10 @@ func (a *Agent) process(input string, stageCtx *sdk.StageContext) (response stri
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(llmErr, context.Canceled) {
|
||||||
|
// 打断不是 provider 故障,不标记不可用,直接让外层循环重试
|
||||||
|
break
|
||||||
|
}
|
||||||
var pe *agentAPI.ProviderError
|
var pe *agentAPI.ProviderError
|
||||||
if errors.As(llmErr, &pe) && (pe.StatusCode == 401 || pe.StatusCode == 403) {
|
if errors.As(llmErr, &pe) && (pe.StatusCode == 401 || pe.StatusCode == 403) {
|
||||||
a.providerManager.ReportStatus(fbProvider.Name(), pe.StatusCode)
|
a.providerManager.ReportStatus(fbProvider.Name(), pe.StatusCode)
|
||||||
@ -1612,8 +1623,8 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
|
|||||||
|
|
||||||
// 输出指令:使用 output_send__{channel} 作为回复手段
|
// 输出指令:使用 output_send__{channel} 作为回复手段
|
||||||
prompt += "\n\n【输出规则】你有多组输出门工具(type=output),每个对应一个输出通道。回复用户时必须调用对应的 output_send__{通道名} 工具。\n"
|
prompt += "\n\n【输出规则】你有多组输出门工具(type=output),每个对应一个输出通道。回复用户时必须调用对应的 output_send__{通道名} 工具。\n"
|
||||||
prompt += "- content 参数是 JSON 字符串,包含要发送的内容。具体格式因通道而异,用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。\n"
|
prompt += "- payload 参数是消息载荷(文本直接填文字),type 指定载荷类型(text/voice/image/file),meta 是 JSON 发送元数据(群号/用户号等)。\n"
|
||||||
prompt += "- output_send__{通道名}_help 是普通 function 类型工具,调用后返回该通道的 JSON 格式详情和示例。\n"
|
prompt += "- 用 output_send__{通道名}_help 查看该通道的 meta 格式和 type 枚举。\n"
|
||||||
prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n"
|
prompt += "- 同一轮对话中可多次调用输出门工具。长消息应当分多次发出,而不是一口气发完。\n"
|
||||||
prompt += "- 直接返回纯文本不会到达任何用户端。"
|
prompt += "- 直接返回纯文本不会到达任何用户端。"
|
||||||
|
|
||||||
@ -2081,16 +2092,24 @@ func (a *Agent) buildToolDefs() []interface{} {
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "output_send__" + ch.Name,
|
"name": "output_send__" + ch.Name,
|
||||||
"description": desc + "。能力: " + capStr + "。content 参数为 JSON 字符串,具体格式请调用 output_send__" + ch.Name + "_help 查看。",
|
"description": desc + "。能力: " + capStr + "。payload 为消息载荷,meta 为 JSON 发送元数据,type 为载荷类型。用 _help 查看 meta 格式和 type 枚举。",
|
||||||
"parameters": map[string]interface{}{
|
"parameters": map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"content": map[string]interface{}{
|
"payload": map[string]interface{}{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "JSON 字符串,包含要发送的内容和路由信息。格式因通道而异,用 _help 工具查看详情。",
|
"description": "消息载荷。type=text 时填文字,type=file/image 时填 URL 或路径",
|
||||||
|
},
|
||||||
|
"meta": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "JSON 对象,包含发送所需的元数据。用 output_send__" + ch.Name + "_help 查看 meta 格式",
|
||||||
|
},
|
||||||
|
"type": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "载荷类型,用 channel._help 查看支持的枚举值",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"content"},
|
"required": []string{"payload", "type"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -2100,7 +2119,7 @@ func (a *Agent) buildToolDefs() []interface{} {
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
"name": "output_send__" + ch.Name + "_help",
|
"name": "output_send__" + ch.Name + "_help",
|
||||||
"description": "查看 " + ch.Name + " 输出通道的 JSON 格式说明和示例",
|
"description": "查看 " + ch.Name + " 输出通道的 meta 格式说明和 type 枚举",
|
||||||
"parameters": map[string]interface{}{
|
"parameters": map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{},
|
"properties": map[string]interface{}{},
|
||||||
@ -2425,12 +2444,16 @@ func (a *Agent) evaluateGraphQuality() {
|
|||||||
// 选出低质量候选:generic 关系(如 distiller 自动生成的泛化关系)
|
// 选出低质量候选:generic 关系(如 distiller 自动生成的泛化关系)
|
||||||
var lowQuality []string
|
var lowQuality []string
|
||||||
for _, r := range result.Relations {
|
for _, r := range result.Relations {
|
||||||
// 自动蒸馏生成的 (用户, 提及, ...) 和 (AI, 回应, ...) 通常是噪音
|
// 自动蒸馏生成的 (用户, 提及, ...), (AI, 回应, ...) 和 jieba 共现 (..., 关联, ...) 通常是噪音
|
||||||
if (r.SourceName == "用户" || r.SourceName == "AI") &&
|
if (r.SourceName == "用户" || r.SourceName == "AI") &&
|
||||||
(r.RelationType == "提及" || r.RelationType == "回应") {
|
(r.RelationType == "提及" || r.RelationType == "回应") {
|
||||||
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」", r.SourceName, r.RelationType, r.TargetName))
|
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」", r.SourceName, r.RelationType, r.TargetName))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if r.RelationType == "关联" {
|
||||||
|
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」(jieba 共现)", r.SourceName, r.RelationType, r.TargetName))
|
||||||
|
continue
|
||||||
|
}
|
||||||
// 极低 mention 的实体+generic 关系
|
// 极低 mention 的实体+generic 关系
|
||||||
if r.Confidence < 0.3 && r.RelationType != "" {
|
if r.Confidence < 0.3 && r.RelationType != "" {
|
||||||
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」(confidence=%.1f)", r.SourceName, r.RelationType, r.TargetName, r.Confidence))
|
lowQuality = append(lowQuality, fmt.Sprintf("「%s」-「%s」→「%s」(confidence=%.1f)", r.SourceName, r.RelationType, r.TargetName, r.Confidence))
|
||||||
@ -2519,31 +2542,41 @@ func docToTriples(doc *document.Doc) []memory.Triple {
|
|||||||
|
|
||||||
triples = append(triples, memory.Triple{
|
triples = append(triples, memory.Triple{
|
||||||
Subject: "文档",
|
Subject: "文档",
|
||||||
Relation: "包含内容",
|
SubjectType: "Concept",
|
||||||
|
Relation: "主题",
|
||||||
Object: doc.Summary,
|
Object: doc.Summary,
|
||||||
|
ObjectType: "Topic",
|
||||||
|
Confidence: 1.0,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, entity := range doc.Entities {
|
// 用 jieba 精确模式逐句分词 → 相邻词共现三元组
|
||||||
|
lines := strings.Split(doc.Content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
terms := memory.CutExact(line)
|
||||||
|
for i := 0; i < len(terms)-1; i++ {
|
||||||
triples = append(triples, memory.Triple{
|
triples = append(triples, memory.Triple{
|
||||||
Subject: "文档",
|
Subject: terms[i],
|
||||||
Relation: "提及实体",
|
SubjectType: "Concept",
|
||||||
Object: entity,
|
Relation: "关联",
|
||||||
|
Object: terms[i+1],
|
||||||
|
ObjectType: "Concept",
|
||||||
|
Confidence: 0.8,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tag := range doc.Tags {
|
|
||||||
triples = append(triples, memory.Triple{
|
|
||||||
Subject: "文档",
|
|
||||||
Relation: "标签",
|
|
||||||
Object: tag,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if doc.Source != "" {
|
if doc.Source != "" {
|
||||||
triples = append(triples, memory.Triple{
|
triples = append(triples, memory.Triple{
|
||||||
Subject: "文档",
|
Subject: "文档",
|
||||||
|
SubjectType: "Concept",
|
||||||
Relation: "来源",
|
Relation: "来源",
|
||||||
Object: doc.Source,
|
Object: doc.Source,
|
||||||
|
ObjectType: "Source",
|
||||||
|
Confidence: 1.0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2601,55 +2634,77 @@ func (a *Agent) processConsolidation(evt *agentIO.InputEvent, input string) {
|
|||||||
|
|
||||||
// executeOutputSendTool — AI 通过指定通道发送消息(校验通道能力)
|
// executeOutputSendTool — AI 通过指定通道发送消息(校验通道能力)
|
||||||
func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string {
|
func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string {
|
||||||
// tool name is "output_send__{channel}"
|
|
||||||
channel := strings.TrimPrefix(tc.Name, "output_send__")
|
channel := strings.TrimPrefix(tc.Name, "output_send__")
|
||||||
content, _ := tc.Arguments["content"].(string)
|
payload, _ := tc.Arguments["payload"].(string)
|
||||||
if channel == "" || content == "" {
|
rawType, _ := tc.Arguments["type"].(string)
|
||||||
return "工具名称格式: output_send__{channel},content 不能为空"
|
if channel == "" || payload == "" || rawType == "" {
|
||||||
|
return "工具名称格式: output_send__{channel},payload 和 type 不能为空"
|
||||||
}
|
}
|
||||||
|
meta, _ := tc.Arguments["meta"].(string)
|
||||||
// content 是一个 JSON 字符串,插件通过解析它确定如何发送消息
|
|
||||||
// === Stage: before_output — 输出前插件可审查/改写/拦截 ===
|
|
||||||
stageCtx := &sdk.StageContext{
|
|
||||||
FinalText: content,
|
|
||||||
Phase: sdk.StageBeforeOutput,
|
|
||||||
}
|
|
||||||
a.runStage(sdk.StageBeforeOutput, stageCtx)
|
|
||||||
if stageCtx.Response != nil {
|
|
||||||
return fmt.Sprintf("输出被插件拦截: %s", *stageCtx.Response)
|
|
||||||
}
|
|
||||||
content = stageCtx.FinalText
|
|
||||||
if content == "" {
|
|
||||||
return "输出被插件清空"
|
|
||||||
}
|
|
||||||
tc.Arguments["content"] = content
|
|
||||||
|
|
||||||
// 通道能力检查
|
// 通道能力检查
|
||||||
caps := a.io.GetChannelCapabilities(channel)
|
caps := a.io.GetChannelCapabilities(channel)
|
||||||
if caps == 0 {
|
if caps == 0 {
|
||||||
return fmt.Sprintf("通道 [%s] 不存在或不可用。可用输出工具列表见 output_list_channels", channel)
|
return fmt.Sprintf("通道 [%s] 不存在或不可用。可用输出工具列表见 output_list_channels", channel)
|
||||||
}
|
}
|
||||||
|
switch rawType {
|
||||||
|
case "text":
|
||||||
if !caps.Supports(agentIO.CapText) {
|
if !caps.Supports(agentIO.CapText) {
|
||||||
return fmt.Sprintf("通道 [%s] 不支持文本输出(能力: %s)", channel, caps.String())
|
return fmt.Sprintf("通道 [%s] 不支持文本输出(能力: %s)", channel, caps.String())
|
||||||
}
|
}
|
||||||
|
case "voice", "audio":
|
||||||
|
if !caps.Supports(agentIO.CapAudio) {
|
||||||
|
return fmt.Sprintf("通道 [%s] 不支持语音输出(能力: %s)", channel, caps.String())
|
||||||
|
}
|
||||||
|
case "image":
|
||||||
|
if !caps.Supports(agentIO.CapImage) {
|
||||||
|
return fmt.Sprintf("通道 [%s] 不支持图片输出(能力: %s)", channel, caps.String())
|
||||||
|
}
|
||||||
|
case "file":
|
||||||
|
if !caps.Supports(agentIO.CapFile) {
|
||||||
|
return fmt.Sprintf("通道 [%s] 不支持文件输出(能力: %s)", channel, caps.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组装参数传给设备处理器
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"payload": payload,
|
||||||
|
"type": rawType,
|
||||||
|
}
|
||||||
|
if meta != "" {
|
||||||
|
args["meta"] = meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Stage: before_output — 输出前插件可审查/改写/拦截 ===
|
||||||
|
stageCtx := &sdk.StageContext{
|
||||||
|
FinalText: payload,
|
||||||
|
Phase: sdk.StageBeforeOutput,
|
||||||
|
}
|
||||||
|
a.runStage(sdk.StageBeforeOutput, stageCtx)
|
||||||
|
if stageCtx.Response != nil {
|
||||||
|
return fmt.Sprintf("输出被插件拦截: %s", *stageCtx.Response)
|
||||||
|
}
|
||||||
|
if stageCtx.FinalText == "" {
|
||||||
|
return "输出被插件清空"
|
||||||
|
}
|
||||||
|
args["payload"] = stageCtx.FinalText
|
||||||
|
|
||||||
// 通过设备处理器投递
|
// 通过设备处理器投递
|
||||||
if dev := a.io.GetDevice(channel); dev != nil {
|
if dev := a.io.GetDevice(channel); dev != nil {
|
||||||
result, err := dev.Execute("output", tc.Arguments)
|
result, err := dev.Execute("output", args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf("通过 [%s] 通道发送失败: %v", channel, err)
|
return fmt.Sprintf("通过 [%s] 通道发送失败: %v", channel, err)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result)
|
return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 降级:发送到 outputCh(供 OutputChan 消费者)
|
// 降级
|
||||||
a.io.EmitTextTo("agent_io", channel, content)
|
a.io.EmitTextTo("agent_io", channel, payload)
|
||||||
return fmt.Sprintf("已通过 [%s] 通道发送", channel)
|
return fmt.Sprintf("已通过 [%s] 通道发送", channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeOutputSendHelp — 返回指定通道的 JSON 格式说明
|
// executeOutputSendHelp — 返回指定通道的 meta 格式和 type 枚举
|
||||||
func (a *Agent) executeOutputSendHelp(tc agentAPI.ToolCall) string {
|
func (a *Agent) executeOutputSendHelp(tc agentAPI.ToolCall) string {
|
||||||
// tool name is "output_send__{channel}_help"
|
|
||||||
suffix := strings.TrimPrefix(tc.Name, "output_send__")
|
suffix := strings.TrimPrefix(tc.Name, "output_send__")
|
||||||
channel := strings.TrimSuffix(suffix, "_help")
|
channel := strings.TrimSuffix(suffix, "_help")
|
||||||
if channel == "" {
|
if channel == "" {
|
||||||
@ -2676,13 +2731,24 @@ func (a *Agent) executeOutputSendHelp(tc agentAPI.ToolCall) string {
|
|||||||
描述: %s
|
描述: %s
|
||||||
能力: %s
|
能力: %s
|
||||||
|
|
||||||
【content JSON 格式说明】
|
【参数说明】
|
||||||
发送到此通道时 content 必须是 JSON 字符串,包含以下字段:
|
payload — 消息载荷(必填)。type=text 时直接填文字,type=file/image 时填 URL 或路径
|
||||||
- "content": 消息正文(必填)
|
meta — JSON 对象,发送所需的元数据(可选,取决于通道是否需要路由信息)
|
||||||
- 根据通道不同可能还需要路由字段(如 "group_id", "user_id" 等)
|
type — 载荷类型(必填),枚举值见下方
|
||||||
|
|
||||||
请在通道描述中查看具体字段要求。
|
【type 枚举】
|
||||||
示例: {"content":"你好"}`, channel, desc, capStr)
|
- text — 文本消息
|
||||||
|
- voice — 语音消息
|
||||||
|
- image — 图片
|
||||||
|
- file — 文件
|
||||||
|
|
||||||
|
【meta JSON 格式】
|
||||||
|
由通道描述定义,通常包含:
|
||||||
|
- "group_id" 群号(群聊时必填)
|
||||||
|
- "user_id" 目标用户 QQ 号(私聊时必填)
|
||||||
|
- "reply_to" 回复某条消息 ID(可选)
|
||||||
|
|
||||||
|
示例: output_send__%s(payload="你好", meta="{\"group_id\": 123456789}", type="text")`, channel, desc, capStr, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// executeOutputListChannels — 列出所有可用通道及其能力
|
// executeOutputListChannels — 列出所有可用通道及其能力
|
||||||
|
|||||||
@ -29,48 +29,34 @@ func TestDocToTriples(t *testing.T) {
|
|||||||
doc := &document.Doc{
|
doc := &document.Doc{
|
||||||
Summary: "用户喜欢编程",
|
Summary: "用户喜欢编程",
|
||||||
Content: "用户提到喜欢Go和Python",
|
Content: "用户提到喜欢Go和Python",
|
||||||
Tags: []string{"编程", "Go"},
|
|
||||||
Entities: []string{"Go", "Python"},
|
|
||||||
Source: "context",
|
Source: "context",
|
||||||
}
|
}
|
||||||
|
|
||||||
triples := docToTriples(doc)
|
triples := docToTriples(doc)
|
||||||
if len(triples) == 0 {
|
|
||||||
t.Fatal("expected non-empty triples")
|
|
||||||
}
|
|
||||||
|
|
||||||
foundSummary := false
|
foundSummary := false
|
||||||
foundEntity := false
|
foundRel := false
|
||||||
foundTag := false
|
|
||||||
foundSource := false
|
foundSource := false
|
||||||
|
|
||||||
for _, tr := range triples {
|
for _, tr := range triples {
|
||||||
if tr.Subject == "文档" && tr.Relation == "包含内容" {
|
switch {
|
||||||
|
case tr.Subject == "文档" && tr.Relation == "主题":
|
||||||
foundSummary = true
|
foundSummary = true
|
||||||
}
|
case tr.Relation == "关联":
|
||||||
if tr.Subject == "文档" && tr.Relation == "提及实体" {
|
foundRel = true
|
||||||
foundEntity = true
|
case tr.Subject == "文档" && tr.Relation == "来源":
|
||||||
}
|
|
||||||
if tr.Subject == "文档" && tr.Relation == "标签" {
|
|
||||||
foundTag = true
|
|
||||||
}
|
|
||||||
if tr.Subject == "文档" && tr.Relation == "来源" {
|
|
||||||
foundSource = true
|
foundSource = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !foundSummary {
|
if !foundSummary {
|
||||||
t.Error("missing '包含内容' triple")
|
t.Error("missing '主题' triple")
|
||||||
}
|
|
||||||
if !foundEntity {
|
|
||||||
t.Error("missing '提及实体' triple")
|
|
||||||
}
|
|
||||||
if !foundTag {
|
|
||||||
t.Error("missing '标签' triple")
|
|
||||||
}
|
}
|
||||||
if !foundSource {
|
if !foundSource {
|
||||||
t.Error("missing '来源' triple")
|
t.Error("missing '来源' triple")
|
||||||
}
|
}
|
||||||
|
if needJieba() && !foundRel {
|
||||||
|
t.Error("missing '关联' triple with jieba available")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDocToTriplesNil(t *testing.T) {
|
func TestDocToTriplesNil(t *testing.T) {
|
||||||
@ -97,13 +83,32 @@ func TestDocToTriplesTypes(t *testing.T) {
|
|||||||
doc := &document.Doc{
|
doc := &document.Doc{
|
||||||
Summary: "测试三元组类型",
|
Summary: "测试三元组类型",
|
||||||
Content: "用于验证 SubjectType 和 ObjectType",
|
Content: "用于验证 SubjectType 和 ObjectType",
|
||||||
Entities: []string{"Go"},
|
Source: "test",
|
||||||
}
|
}
|
||||||
|
|
||||||
triples := docToTriples(doc)
|
triples := docToTriples(doc)
|
||||||
|
|
||||||
|
// 主题 and 来源 triples have Subject=文档
|
||||||
for _, tr := range triples {
|
for _, tr := range triples {
|
||||||
if tr.Subject != "文档" {
|
if tr.Subject == "文档" {
|
||||||
t.Errorf("expected subject '文档', got %q", tr.Subject)
|
if tr.SubjectType != "Concept" {
|
||||||
|
t.Errorf("文档 subject_type should be Concept, got %q", tr.SubjectType)
|
||||||
|
}
|
||||||
|
if tr.Confidence != 1.0 {
|
||||||
|
t.Errorf("文档 triple confidence should be 1.0, got %f", tr.Confidence)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 关联 triples use extracted terms as subject/object
|
||||||
|
if tr.Relation != "关联" {
|
||||||
|
t.Errorf("non-文档 triple should have 关联 relation, got %q", tr.Relation)
|
||||||
|
}
|
||||||
|
if tr.Confidence != 0.8 {
|
||||||
|
t.Errorf("关联 triple confidence should be 0.8, got %f", tr.Confidence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// all should have SubjectType/ObjectType set
|
||||||
|
if tr.SubjectType == "" || tr.ObjectType == "" {
|
||||||
|
t.Errorf("triple %+v missing SubjectType or ObjectType", tr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,106 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||||
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func needJieba() bool {
|
||||||
|
return memory.GetJieba() != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocToTriplesEmpty(t *testing.T) {
|
||||||
|
doc := &document.Doc{
|
||||||
|
Summary: "empty doc",
|
||||||
|
Content: "",
|
||||||
|
Source: "test",
|
||||||
|
}
|
||||||
|
triples := docToTriples(doc)
|
||||||
|
if len(triples) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 triples (主题+来源), got %d", len(triples))
|
||||||
|
}
|
||||||
|
|
||||||
|
if triples[0].Subject != "文档" || triples[0].Relation != "主题" || triples[0].Object != "empty doc" {
|
||||||
|
t.Errorf("first triple mismatch: %+v", triples[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
last := triples[len(triples)-1]
|
||||||
|
if last.Subject != "文档" || last.Relation != "来源" || last.Object != "test" {
|
||||||
|
t.Errorf("last triple mismatch: %+v", last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocToTriplesConversation(t *testing.T) {
|
||||||
|
doc := &document.Doc{
|
||||||
|
Summary: "测试对话 (qq) 涉及: 天气",
|
||||||
|
Content: "[15:04] qq: 今天天气怎么样\n[15:05] agent: 今天天气很好",
|
||||||
|
Source: "qq",
|
||||||
|
}
|
||||||
|
triples := docToTriples(doc)
|
||||||
|
|
||||||
|
minLen := 2
|
||||||
|
hasJieba := needJieba()
|
||||||
|
|
||||||
|
if hasJieba && len(triples) <= minLen {
|
||||||
|
t.Errorf("expected more than %d triples with jieba, got %d", minLen, len(triples))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tr := range triples {
|
||||||
|
if tr.Subject == "" || tr.Relation == "" || tr.Object == "" {
|
||||||
|
t.Errorf("triple[%d] has empty field: %+v", i, tr)
|
||||||
|
}
|
||||||
|
if tr.Confidence <= 0 {
|
||||||
|
t.Errorf("triple[%d] has non-positive confidence: %+v", i, tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relCount := 0
|
||||||
|
for _, tr := range triples {
|
||||||
|
if tr.Relation == "关联" {
|
||||||
|
relCount++
|
||||||
|
if tr.Subject == tr.Object {
|
||||||
|
t.Errorf("关联 triple has same subject and object: %+v", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasJieba && relCount == 0 {
|
||||||
|
t.Errorf("expected 关联 triples with jieba enabled, got 0 in %+v", triples)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocToTriplesMultiLine(t *testing.T) {
|
||||||
|
doc := &document.Doc{
|
||||||
|
Summary: "多轮对话",
|
||||||
|
Content: "[10:00] user: 你好\n[10:01] agent: 你好,有什么可以帮助你的\n[10:02] user: 今天天气如何\n[10:03] agent: 今天天气很好",
|
||||||
|
Source: "qq",
|
||||||
|
}
|
||||||
|
triples := docToTriples(doc)
|
||||||
|
if len(triples) < 2 {
|
||||||
|
t.Fatalf("expected at least 2 triples, got %d", len(triples))
|
||||||
|
}
|
||||||
|
|
||||||
|
if triples[0].Subject != "文档" || triples[0].Relation != "主题" {
|
||||||
|
t.Errorf("first triple should be 主题, got %+v", triples[0])
|
||||||
|
}
|
||||||
|
last := triples[len(triples)-1]
|
||||||
|
if last.Subject != "文档" || last.Relation != "来源" {
|
||||||
|
t.Errorf("last triple should be 来源, got %+v", last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDocToTriplesEmptyContent(t *testing.T) {
|
||||||
|
doc := &document.Doc{
|
||||||
|
Summary: "空内容",
|
||||||
|
Content: "",
|
||||||
|
Source: "test",
|
||||||
|
}
|
||||||
|
triples := docToTriples(doc)
|
||||||
|
if len(triples) != 2 {
|
||||||
|
t.Fatalf("expected exactly 2 triples (主题+来源) for empty content, got %d", len(triples))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTruncateStr(t *testing.T) {
|
func TestTruncateStr(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
|
|||||||
@ -200,7 +200,7 @@ func (c *RelevanceContext) Prune(currentInput string, topK int, docStore *docume
|
|||||||
Response: s.event.Response,
|
Response: s.event.Response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doc, err := docStore.ContextToDoc("context_archived", entries)
|
doc, err := docStore.ContextToDoc("context_archived", entries, c.embedder)
|
||||||
if err == nil && doc != nil {
|
if err == nil && doc != nil {
|
||||||
archived = len(archive)
|
archived = len(archive)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,3 +117,26 @@ func ExtractKeywords(text string) []string {
|
|||||||
}
|
}
|
||||||
return keywords
|
return keywords
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CutExact 精确模式分词:返回去停用词后的所有有义项(不限数量),用于 doc→graph 蒸馏
|
||||||
|
func CutExact(text string) []string {
|
||||||
|
text = CleanTemplateText(text)
|
||||||
|
x := GetJieba()
|
||||||
|
if x == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
words := x.Cut(text, false)
|
||||||
|
var result []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, w := range words {
|
||||||
|
if stopWords[w] || seen[w] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !validEntityName(w) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[w] = true
|
||||||
|
result = append(result, w)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
107
internal/memory/cut_test.go
Normal file
107
internal/memory/cut_test.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func needJieba(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if GetJieba() == nil {
|
||||||
|
t.Skip("jieba dictionaries not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCutExact(t *testing.T) {
|
||||||
|
needJieba(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
min int
|
||||||
|
not []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "chinese_sentence",
|
||||||
|
text: "今天天气怎么样",
|
||||||
|
min: 2,
|
||||||
|
not: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stop_words_removed",
|
||||||
|
text: "和天气地",
|
||||||
|
min: 1,
|
||||||
|
not: []string{"的", "和"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "short_words_filtered",
|
||||||
|
text: "今天好天气",
|
||||||
|
min: 1,
|
||||||
|
not: []string{"好"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty_text",
|
||||||
|
text: "",
|
||||||
|
min: 0,
|
||||||
|
not: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "qq_conversation",
|
||||||
|
text: "今天天气怎么样 → 今天天气很好",
|
||||||
|
min: 2,
|
||||||
|
not: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all_stop_words",
|
||||||
|
text: "的了呢吗",
|
||||||
|
min: 0,
|
||||||
|
not: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := CutExact(tt.text)
|
||||||
|
if len(got) < tt.min {
|
||||||
|
t.Errorf("CutExact(%q) = %v (len=%d), want at least %d terms", tt.text, got, len(got), tt.min)
|
||||||
|
}
|
||||||
|
for _, forbid := range tt.not {
|
||||||
|
for _, g := range got {
|
||||||
|
if g == forbid {
|
||||||
|
t.Errorf("CutExact(%q) = %v, should not contain %q", tt.text, got, forbid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCutExactNoDuplicates(t *testing.T) {
|
||||||
|
needJieba(t)
|
||||||
|
|
||||||
|
got := CutExact("天气天气天气")
|
||||||
|
if len(got) > 1 {
|
||||||
|
t.Errorf("expected deduplicated result, got %v (len=%d)", got, len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCutExactValidEntityName(t *testing.T) {
|
||||||
|
needJieba(t)
|
||||||
|
|
||||||
|
got := CutExact("a b c")
|
||||||
|
for _, g := range got {
|
||||||
|
if !validEntityName(g) {
|
||||||
|
t.Errorf("CutExact returned invalid entity name %q", g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCutExactRemoveTimestamp(t *testing.T) {
|
||||||
|
needJieba(t)
|
||||||
|
|
||||||
|
got := CutExact("[15:04] 今天天气不错")
|
||||||
|
for _, g := range got {
|
||||||
|
if g == "15" || g == "04" || g == "15:04" {
|
||||||
|
t.Errorf("timestamp should be removed by CleanTemplateText, got %q in %v", g, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ type Doc struct {
|
|||||||
Meta map[string]string `json:"meta,omitempty"`
|
Meta map[string]string `json:"meta,omitempty"`
|
||||||
AccessCount int `json:"access_count"` // 访问次数
|
AccessCount int `json:"access_count"` // 访问次数
|
||||||
LastAccess time.Time `json:"last_access"` // 最后访问时间
|
LastAccess time.Time `json:"last_access"` // 最后访问时间
|
||||||
|
Vector vector.Vector `json:"vector,omitempty"` // 预计算向量(与 context 同空间),nil 则用 TF-IDF 兜底
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store — 文档记忆存储,包含向量索引
|
// Store — 文档记忆存储,包含向量索引
|
||||||
@ -39,10 +40,29 @@ type Store struct {
|
|||||||
|
|
||||||
docs map[string]*Doc
|
docs map[string]*Doc
|
||||||
summaries []string // 用于训练向量化器,最大 10000 条
|
summaries []string // 用于训练向量化器,最大 10000 条
|
||||||
|
vectorizer vector.Vectorizer // 可选:与 context 同空间的向量化器
|
||||||
|
|
||||||
dirty bool
|
dirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) SetVectorizer(v vector.Vectorizer) {
|
||||||
|
s.vectorizer = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReindexWithVectorizer 用给定的向量化器重建所有文档的向量索引
|
||||||
|
func (s *Store) ReindexWithVectorizer(v vector.Vectorizer) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
log.Printf("[document memory] reindex with vectorizer (%d docs)", len(s.docs))
|
||||||
|
s.vec = vector.NewStore()
|
||||||
|
for _, doc := range s.docs {
|
||||||
|
doc.Vector = v.Vectorize(doc.Summary + " " + doc.Content)
|
||||||
|
s.vec.Insert(doc.ID, doc.Summary, doc.Vector, doc.Meta)
|
||||||
|
}
|
||||||
|
log.Printf("[document memory] reindex with vectorizer complete (%d vectors)", s.vec.Size())
|
||||||
|
}
|
||||||
|
|
||||||
const maxSummaries = 10000
|
const maxSummaries = 10000
|
||||||
|
|
||||||
func NewStore(dir string) *Store {
|
func NewStore(dir string) *Store {
|
||||||
@ -88,7 +108,10 @@ func (s *Store) Insert(doc *Doc) error {
|
|||||||
|
|
||||||
// 增量训练向量化器并加入向量索引
|
// 增量训练向量化器并加入向量索引
|
||||||
s.addSummary(doc.Summary)
|
s.addSummary(doc.Summary)
|
||||||
vec := s.veczer.Vectorize(doc.Summary + " " + doc.Content)
|
vec := doc.Vector
|
||||||
|
if vec == nil {
|
||||||
|
vec = s.veczer.Vectorize(doc.Summary + " " + doc.Content)
|
||||||
|
}
|
||||||
s.vec.Insert(doc.ID, doc.Summary, vec, doc.Meta)
|
s.vec.Insert(doc.ID, doc.Summary, vec, doc.Meta)
|
||||||
|
|
||||||
// 立即写盘
|
// 立即写盘
|
||||||
@ -101,7 +124,7 @@ func (s *Store) Insert(doc *Doc) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ContextToDoc — 将一段上下文对话历史提炼为文档(带内容去重)
|
// ContextToDoc — 将一段上下文对话历史提炼为文档(带内容去重)
|
||||||
func (s *Store) ContextToDoc(source string, entries []ContextEntry) (*Doc, error) {
|
func (s *Store) ContextToDoc(source string, entries []ContextEntry, vec vector.Vectorizer) (*Doc, error) {
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -140,6 +163,12 @@ func (s *Store) ContextToDoc(source string, entries []ContextEntry) (*Doc, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := fmt.Sprintf("doc_%d", time.Now().UnixNano())
|
id := fmt.Sprintf("doc_%d", time.Now().UnixNano())
|
||||||
|
var docVec vector.Vector
|
||||||
|
if vec != nil {
|
||||||
|
docVec = vec.Vectorize(summary + " " + content)
|
||||||
|
} else {
|
||||||
|
docVec = s.veczer.Vectorize(summary + " " + content)
|
||||||
|
}
|
||||||
doc := &Doc{
|
doc := &Doc{
|
||||||
ID: id,
|
ID: id,
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
@ -152,13 +181,13 @@ func (s *Store) ContextToDoc(source string, entries []ContextEntry) (*Doc, error
|
|||||||
AccessCount: 1,
|
AccessCount: 1,
|
||||||
Source: source,
|
Source: source,
|
||||||
Meta: map[string]string{"content_hash": contentHash},
|
Meta: map[string]string{"content_hash": contentHash},
|
||||||
|
Vector: docVec,
|
||||||
}
|
}
|
||||||
s.docs[id] = doc
|
s.docs[id] = doc
|
||||||
|
|
||||||
// 增量训练向量化器并加入向量索引
|
// 加入向量索引
|
||||||
s.addSummary(summary)
|
s.addSummary(summary)
|
||||||
vec := s.veczer.Vectorize(summary + " " + content)
|
s.vec.Insert(id, summary, doc.Vector, nil)
|
||||||
s.vec.Insert(id, summary, vec, nil)
|
|
||||||
|
|
||||||
s.dirty = true
|
s.dirty = true
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
@ -180,7 +209,7 @@ func (s *Store) Consume(text string, topK int) []*Doc {
|
|||||||
topK = 5
|
topK = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
vec := s.veczer.Vectorize(text)
|
vec := s.vectorizeQuery(text)
|
||||||
results := s.vec.Search(vec, topK)
|
results := s.vec.Search(vec, topK)
|
||||||
|
|
||||||
var docs []*Doc
|
var docs []*Doc
|
||||||
@ -194,6 +223,14 @@ func (s *Store) Consume(text string, topK int) []*Doc {
|
|||||||
return docs
|
return docs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vectorizeQuery 用语义向量化器(首选)或 TF-IDF(兜底)处理查询文本
|
||||||
|
func (s *Store) vectorizeQuery(text string) vector.Vector {
|
||||||
|
if s.vectorizer != nil {
|
||||||
|
return s.vectorizer.Vectorize(text)
|
||||||
|
}
|
||||||
|
return s.veczer.Vectorize(text)
|
||||||
|
}
|
||||||
|
|
||||||
// Query — 向量相似度查询文档
|
// Query — 向量相似度查询文档
|
||||||
func (s *Store) Query(text string, topK int) []*Doc {
|
func (s *Store) Query(text string, topK int) []*Doc {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
@ -203,7 +240,7 @@ func (s *Store) Query(text string, topK int) []*Doc {
|
|||||||
topK = 5
|
topK = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
vec := s.veczer.Vectorize(text)
|
vec := s.vectorizeQuery(text)
|
||||||
results := s.vec.Search(vec, topK)
|
results := s.vec.Search(vec, topK)
|
||||||
|
|
||||||
var docs []*Doc
|
var docs []*Doc
|
||||||
@ -228,7 +265,10 @@ func (s *Store) Reindex() {
|
|||||||
|
|
||||||
s.vec = vector.NewStore()
|
s.vec = vector.NewStore()
|
||||||
for _, doc := range s.docs {
|
for _, doc := range s.docs {
|
||||||
vec := s.veczer.Vectorize(doc.Summary + " " + doc.Content)
|
vec := doc.Vector
|
||||||
|
if vec == nil {
|
||||||
|
vec = s.veczer.Vectorize(doc.Summary + " " + doc.Content)
|
||||||
|
}
|
||||||
s.vec.Insert(doc.ID, doc.Summary, vec, doc.Meta)
|
s.vec.Insert(doc.ID, doc.Summary, vec, doc.Meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +388,10 @@ func (s *Store) loadAll() error {
|
|||||||
|
|
||||||
// 重建向量索引
|
// 重建向量索引
|
||||||
for _, doc := range s.docs {
|
for _, doc := range s.docs {
|
||||||
vec := s.veczer.Vectorize(doc.Summary + " " + doc.Content)
|
vec := doc.Vector
|
||||||
|
if vec == nil {
|
||||||
|
vec = s.veczer.Vectorize(doc.Summary + " " + doc.Content)
|
||||||
|
}
|
||||||
s.vec.Insert(doc.ID, doc.Summary, vec, nil)
|
s.vec.Insert(doc.ID, doc.Summary, vec, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ func TestContextToDoc(t *testing.T) {
|
|||||||
{Timestamp: time.Now(), Source: "user", Content: "特别是Go语言", Response: "Go很棒"},
|
{Timestamp: time.Now(), Source: "user", Content: "特别是Go语言", Response: "Go很棒"},
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := s.ContextToDoc("test", entries)
|
doc, err := s.ContextToDoc("test", entries, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user