diff --git a/cmd/homed/main.go b/cmd/homed/main.go index fdb5ee4..8049cb1 100644 --- a/cmd/homed/main.go +++ b/cmd/homed/main.go @@ -81,6 +81,7 @@ func main() { } memIdx := memory.NewIndexer(memDB) + memIdx.Sync() // 启动时立即同步,避免前30分钟空窗 socialStore := social.New(memDB) distiller := pipeline.NewDistiller(memDB, *dataDir, pipeline.DistillerConfig{ @@ -194,14 +195,14 @@ func main() { if err := textMem.Append(te); err != nil { log.Printf("[homed] text memory append: %v", err) } - } + } - if input != "" { - distiller.Append("agent", "user", input) - } - if response != "" { - distiller.Append("agent", "assistant", response) - } + if input != "" && memDB != nil { + distiller.Append("agent", "user", input) + } + if response != "" && memDB != nil { + distiller.Append("agent", "assistant", response) + } } } } @@ -323,20 +324,16 @@ func main() { 如需异步发送消息或通知,使用 output_send 指定通道和内容。 使用 output_list_channels 查看可用通道及其能力。 -你有以下核心工具: -1. memory_recall — 查询图记忆(历史/个人信息) -2. memory_commit — 写入图记忆(记住新信息) -3. memory_introspect — 查看记忆统计 -4. person_query — 查询人物特质与社交关系网 -5. person_set_trait — 记录人物特质(性格/喜好等) -6. person_relate — 建立人物间社交关系 -7. person_network — 查看社交网络 -8. knowledge_search — 搜索知识库 -9. doc_query — 查询文档记忆 -10. doc_commit — 写入文档记忆 -11. plgreload — 热重载插件 -12. llm_list_sources — 列出所有可用的 LLM 源 -13. llm_set_source — 切换到指定 LLM 源 +可用工具列表会由系统自动传入,按需使用即可。以下是你尤其需要关注的几类工具: +- memory_* — 图记忆(长期记忆,记录和查询个人信息/事实) +- knowledge_* — 知识库(查阅预设知识文档) +- doc_* — 文档记忆(近期对话的存档,查询后自动清除) +- person_* — 人物特质与社交关系网 +- llm_* — LLM 源管理(列出/切换模型提供商) +- output_* — 输出通道管理(切换/发送消息) +- timer_set — 设置定时提醒 +- plgreload — 热重载插件 +- spawn_child — 生成子 Agent 执行独立任务 回复你的真实想法,用自然语言与用户交流。`, Provider: provider, diff --git a/internal/agent/core/agent.go b/internal/agent/core/agent.go index 9e2bd98..0b1f04c 100644 --- a/internal/agent/core/agent.go +++ b/internal/agent/core/agent.go @@ -986,12 +986,10 @@ func (a *Agent) executeDocTool(tc agentAPI.ToolCall) string { if query == "" { return "请输入查询内容" } - docs := a.docStore.Query(query, topK) + docs := a.docStore.Consume(query, topK) if len(docs) == 0 { return "未找到相关文档记忆" } - // 返回内容后从冷层移除,避免后续自动注入重复 - a.docStore.Consume(query, topK) var parts []string for i, d := range docs { parts = append(parts, fmt.Sprintf("[%d] %s (来源: %s)", i+1, d.Summary, d.Source)) diff --git a/internal/memory/pipeline/pipeline.go b/internal/memory/pipeline/pipeline.go index 432ec9b..26a4815 100644 --- a/internal/memory/pipeline/pipeline.go +++ b/internal/memory/pipeline/pipeline.go @@ -6,6 +6,7 @@ import ( "log" "os" "path/filepath" + "strings" "sync" "time" @@ -213,15 +214,179 @@ func (d *Distiller) cleanupRawFiles() { func extractKeyTriples(userContent, assistantContent string) []memory.Triple { var triples []memory.Triple - if len(userContent) > 0 && len(userContent) < 500 { - triples = append(triples, memory.Triple{Subject: "用户", Relation: "提及", Object: truncate(userContent, 200)}) + + // 提取对话中的关键信息,而不是直接 dump 原文 + // 规则1: "我的名字是X" / "我叫X" → (用户, 姓名, X) + if name := extractName(userContent); name != "" { + triples = append(triples, memory.Triple{Subject: "用户", Relation: "姓名", Object: name}) } - if len(assistantContent) > 0 && len(assistantContent) < 500 { - triples = append(triples, memory.Triple{Subject: "AI", Relation: "回应", Object: truncate(assistantContent, 200)}) + // 规则2: "我住在X" / "我家在X" → (用户, 居住地, X) + if loc := extractLocation(userContent); loc != "" { + triples = append(triples, memory.Triple{Subject: "用户", Relation: "居住地", Object: loc}) } + // 规则3: "我喜欢X" / "我爱X" → (用户, 喜好, X) + if like := extractLike(userContent); like != "" { + triples = append(triples, memory.Triple{Subject: "用户", Relation: "喜好", Object: like}) + } + // 规则4: "我X岁" / "我的年龄是X" → (用户, 年龄, X) + if age := extractAge(userContent); age != "" { + triples = append(triples, memory.Triple{Subject: "用户", Relation: "年龄", Object: age}) + } + // 规则5: "我的工作是X" / "我在X工作" → (用户, 职业, X) + if job := extractJob(userContent); job != "" { + triples = append(triples, memory.Triple{Subject: "用户", Relation: "职业", Object: job}) + } + return triples } +func extractName(s string) string { + patterns := []struct { + prefix string + suffix string + }{ + {"我叫", ""}, + {"我的名字是", ""}, + {"我是", ""}, + {"名字是", ""}, + } + s = strings.TrimSpace(s) + for _, p := range patterns { + if strings.HasPrefix(s, p.prefix) { + candidate := strings.TrimPrefix(s, p.prefix) + if p.suffix != "" && strings.Contains(candidate, p.suffix) { + candidate = candidate[:strings.Index(candidate, p.suffix)] + } + candidate = strings.TrimSpace(candidate) + // 取第一个空格/逗号/句号前的内容 + for _, sep := range []string{",", "。", " ", ","} { + if idx := strings.Index(candidate, sep); idx > 0 { + candidate = candidate[:idx] + } + } + if len(candidate) > 0 && len(candidate) < 20 { + return candidate + } + } + } + return "" +} + +func extractLocation(s string) string { + s = strings.TrimSpace(s) + after := "" + switch { + case strings.HasPrefix(s, "我住在"): + after = strings.TrimPrefix(s, "我住在") + case strings.HasPrefix(s, "我家在"): + after = strings.TrimPrefix(s, "我家在") + case strings.HasPrefix(s, "我居住在"): + after = strings.TrimPrefix(s, "我居住在") + case strings.HasPrefix(s, "住在"): + after = strings.TrimPrefix(s, "住在") + default: + return "" + } + for _, sep := range []string{"。", ",", " ", ","} { + if idx := strings.Index(after, sep); idx > 0 { + after = after[:idx] + } + } + if len(after) > 0 && len(after) < 50 { + return strings.TrimSpace(after) + } + return "" +} + +func extractLike(s string) string { + s = strings.TrimSpace(s) + after := "" + switch { + case strings.HasPrefix(s, "我喜欢"): + after = strings.TrimPrefix(s, "我喜欢") + case strings.HasPrefix(s, "我爱"): + after = strings.TrimPrefix(s, "我爱") + case strings.HasPrefix(s, "我最喜欢"): + after = strings.TrimPrefix(s, "我最喜欢") + default: + return "" + } + for _, sep := range []string{"。", ",", " ", ","} { + if idx := strings.Index(after, sep); idx > 0 { + after = after[:idx] + } + } + if len(after) > 0 && len(after) < 50 { + return strings.TrimSpace(after) + } + return "" +} + +func extractAge(s string) string { + s = strings.TrimSpace(s) + after := "" + switch { + case strings.HasPrefix(s, "我"): + rest := strings.TrimPrefix(s, "我") + if strings.Contains(rest, "岁") { + after = rest[:strings.Index(rest, "岁")] + } else if strings.HasPrefix(rest, "的年龄是") { + after = strings.TrimPrefix(rest, "的年龄是") + } else { + return "" + } + default: + return "" + } + for _, sep := range []string{"。", ",", " ", ","} { + if idx := strings.Index(after, sep); idx > 0 { + after = after[:idx] + } + } + if len(after) > 0 && len(after) < 5 { + return strings.TrimSpace(after) + } + return "" +} + +func extractJob(s string) string { + s = strings.TrimSpace(s) + after := "" + switch { + case strings.HasPrefix(s, "我的工作是"): + after = strings.TrimPrefix(s, "我的工作是") + case strings.HasPrefix(s, "我在"): + rest := strings.TrimPrefix(s, "我在") + if strings.Contains(rest, "工作") { + after = rest[:strings.Index(rest, "工作")] + } else { + return "" + } + case strings.HasPrefix(s, "我是"): + rest := strings.TrimPrefix(s, "我是") + // "我是一个程序员" / "我是老师" + for _, keyword := range []string{"一个", "一名", "一位"} { + if strings.HasPrefix(rest, keyword) { + rest = strings.TrimPrefix(rest, keyword) + break + } + } + // 职业通常较短,先看看 + after = rest + default: + return "" + } + for _, sep := range []string{"。", ",", " ", ",", "。"} { + if idx := strings.Index(after, sep); idx > 0 { + after = after[:idx] + } + } + if len(after) > 0 && len(after) < 20 { + return strings.TrimSpace(after) + } + return "" +} + func truncate(s string, max int) string { if len(s) > max { return s[:max] + "..."