diff --git a/docs4/auto_cognition_design.md b/docs4/old/auto_cognition_design.md similarity index 100% rename from docs4/auto_cognition_design.md rename to docs4/old/auto_cognition_design.md diff --git a/docs4/auto_consolidate_design.md b/docs4/old/auto_consolidate_design.md similarity index 100% rename from docs4/auto_consolidate_design.md rename to docs4/old/auto_consolidate_design.md diff --git a/docs4/auto_dream_design.md b/docs4/old/auto_dream_design.md similarity index 100% rename from docs4/auto_dream_design.md rename to docs4/old/auto_dream_design.md diff --git a/docs4/auto_memory_design.md b/docs4/old/auto_memory_design.md similarity index 100% rename from docs4/auto_memory_design.md rename to docs4/old/auto_memory_design.md diff --git a/docs4/auto_recall_design.md b/docs4/old/auto_recall_design.md similarity index 100% rename from docs4/auto_recall_design.md rename to docs4/old/auto_recall_design.md diff --git a/docs4/background_backup.md b/docs4/old/background_backup.md similarity index 100% rename from docs4/background_backup.md rename to docs4/old/background_backup.md diff --git a/docs4/reme4_index.md b/docs4/old/reme4_index.md similarity index 100% rename from docs4/reme4_index.md rename to docs4/old/reme4_index.md diff --git a/docs4/reme4_report.md b/docs4/old/reme4_report.md similarity index 100% rename from docs4/reme4_report.md rename to docs4/old/reme4_report.md diff --git a/docs4/old/reme4_resource.md b/docs4/old/reme4_resource.md new file mode 100644 index 00000000..6ce5e481 --- /dev/null +++ b/docs4/old/reme4_resource.md @@ -0,0 +1,55 @@ +- 增加agent 的component +- 增加全局 时区time_zone,全局作用 + + +数据结构 +- resource + - YYYYMMDD + - xxxx.html + - xxxx.txt + - xxxx.md +- dialog + - YYYYMMDD + - session_{session_id}.jsonl msg->dict格式 +- daily + - YYYYMMDD.md 索引 + - YYYYMMDD + - session_{session_id}.md 日志本 + - resource_{resource_id}.md +- digest + - personal 个性化信息 + - xxx.md + - procedural 程序化记忆 + - xxx.md + - wiki 知识化记忆 + - xxx.md + + +监控任务【后台】: +- 构建bm25+emb的index【5s】 + - 目录:daily+digest 的 所有md,使用markdown的ast解析 + - 可选:jsonl,要求保存的时候对工具结果截断,使用rag方案解析 +- daily md监控:【1min】 + - 暂无 +- digest md监控:【1min】 + - 暂无 +- resource 监控:【间隔5min】 + - 针对每一个文件,生成一个hashid,作为session_id + - 生成一个agent,解读文件(读前1MB内容)防止上下文炸了 + - 把抽取的内容写到daily/YYYYMMDD/resource_{resource_id}.md + - 调用推送工具: + - qwenpaw推送收件箱/agent + - cc可以直接推送agent @sen + + +hook任务: +- auto-memory:在上下文满/每隔多少轮/session_end + - qwenpaw:直接传message session_id date,直接append session_{session_id}.jsonl @jinli + - cc:给出新的path,对比我们保存的session_{session_id}.jsonl,看到增量msg,接着解析path的内容,保存到session_{session_id}.jsonl @sen + - 注意保存的时候截断工具调用结构,防止太长 + + +定时任务: +- auto-dream:每天夜晚触发 + - 记忆整理:按照session/resource_id去做for循环整理:把YYYYMMDD.md + YYYYMMDD/* 消化更新到 digest下的personal/procedural/wiki + - 记忆link:markdown之间link,digest内部链接,可以链接到外面。 diff --git a/docs4/old/reme_design.md b/docs4/old/reme_design.md new file mode 100644 index 00000000..d2e69904 --- /dev/null +++ b/docs4/old/reme_design.md @@ -0,0 +1,192 @@ +# 快速测试 + +```bash +# 终端 A:启动服务 +reme4 start + +# 终端 B:调用 version 验证服务可用 +reme4 version +# 预期输出:✅ ReMe v{__version__} +``` + +# 基础Job + +@jinli + +入口:`reme4/reme.py::main()` → `parse_args(*sys.argv[1:])` 解析首个位置参数为 `action`,后续 `key=value` 解析为 kwargs(支持 +`service.port=8080` 的 dot notation;自动剥离 `--` / `-` 前缀;值会做 bool / int / float / JSON 转换)。 + +调用模式: + +- `start`:本地启动 `ReMe(Application)` 服务(不经过 client) +- `find_reme`:本地探测正在运行的 reme,不调用服务 +- `list`:在 client 端拦截,不转发到服务端,直接返回 action 目录 +- 其他 action:通过 `call_server(action, **kwargs)` → `R.get(ComponentEnum.CLIENT, backend)` 实例化客户端并流式打印(任意未列出的 + step register name 都按本规则透传) + +通用可选参数 `backend:str=http`(取值 `http` / `mcp`,对应 `reme4/components/client/{http_client,mcp_client}.py` 中 +`@R.register` 注册名);服务端默认 host/port 见 `reme4/constants.py`,可由 `start` 端通过 `service.host=` / `service.port=` +覆盖。 + +说明:📥 输入参数 | 📤 输出 | ⭐ 必填 | 🎚️ 默认值 | 🛠️ 内部行为 | 📊 metadata + +| 分类 | 指令 (register name) | 入口 | 参数 & 行为 | +|------------|--------------------------------------------------|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 🚀 本地 | 🟢 `start` | `reme.py:30` → `ReMe(**kwargs).run_app()` | 📥 可选 `config=`(默认加载 `reme4/config/default.yaml`,`.yaml/.yml/.json` 都支持,含 `${ENV:-default}` 占位符)| 可选 `service.host=` / `service.port=` 等任意 dot-notation 覆盖 | 🛠️ 流程:`load_env()` → `resolve_app_config(**kwargs)` deep merge → `precheck_start(svc)`(`utils/service_utils.py:72`:目标 host:port 已有 reme → 打印 `reme already running ...` 直接返回;端口被其他进程占用 → stderr 提示 `port {port} occupied. Start on another port: reme4 start service.port=` 并 `sys.exit(1)`)→ 启动服务 | +| 🚀 本地 | 🧭 `find_reme` | `reme.py:36` → `utils/service_utils.py:89` | 📥 无 | 📤 发现服务则 stdout 打印 `HOST={host} PORT={port} PID={pid or 'unknown'}`;未发现则 stderr 提示 `reme not started. Try: reme start` 并 `sys.exit(1)` | 🛠️ 流程:先探 `REME_DEFAULT_HOST:REME_DEFAULT_PORT`(`health_check` 命中算 `reme`),再 `pgrep -af "reme.* start"` 扫描其他端口 | +| 🛰️ 客户端 | 📜 `list` | `components/client/base_client.py:36` | 📥 无 | 📤 服务端可用 action 目录(JSON,`indent=2 ensure_ascii=False`)| 🛠️ 在 `BaseClient.__call__` 中拦截,不进入 `_execute`,直接调用 `list_actions()`(HTTP/MCP backend 各自实现) | +| 🌐 通用 step | 🆘 `help` (`help_step`) | `call_server("help")` | 📥 无 | 📤 `answer` 一行一个 job:`🛠️ \`{name}\` — {description} 📥 {params}`,参数渲染为 `name:type*`(必填) / `name:type={default}` / `name:type` | 📊 `metadata.job_count` | 🛠️ 自动跳过名为 `help` 的 job | +| 🌐 通用 step | 🩺 `health_check` (`health_check_step`) | `call_server("health_check")` | 📥 无 | 📤 `answer = "✅/❌ ReMe v{version} - healthy/unhealthy"` | 📊 `metadata.health = {version, healthy, components}` | 🧩 覆盖组件:`embedding_model`(🟢 is_started/is_healthy/model_name/dimensions/cache_size/memory) · `file_graph`(🕸️ n_nodes/n_edges/n_virtual\|n_pending/memory) · `file_store`(📦 n_chunks/n_chunks_with_embedding/memory) · `file_watcher`(👀 background_running/watch_paths) · `keyword_index`(🔤 n_docs/vocab_size/memory) | 🛠️ deep sizeof(含 numpy.nbytes),未启动 / 后台未跑 / embedding 不健康 → ❌ | +| 🌐 通用 step | 🏷️ `version` (`version_step`) | `call_server("version")` | 📥 无 | 📤 `answer = reme4.__version__` | 📊 `metadata.version` | +| 🌐 通用 step | 🔄 `reindex` (`reindex_step`) | `call_server("reindex")` | 📥 无 | 📤 `answer = "🔄 Reindexed {added} file(s)"` | 📊 `metadata.counts = {added, ...}` | 🛠️ 流程:`file_watcher.close()` → `file_store.clear()` → `file_watcher.update_store()` → `file_watcher.start()`(finally 保证重启) | +| 🔎 search | 🔍 `search` (`search_step`) | `call_server("search", query=…, …)` | 📥 `query:str` ⭐ | 🎚️ `limit:int=5`(>0) | 🎚️ `min_score:float=0.0` | ⚖️ `vector_weight:float=0.7` ∈[0,1](keyword 权 = 1-vw)| 🔀 `candidate_multiplier:float=3.0`(candidates = min(200, limit×mult))| 🔗 `expand_links:bool=True` | 🔢 `max_links_per_direction:int=10` | 🎚️ `search_filter:dict={}` | 📤 `answer` 每命中一行 `path:start-end [score=… vector=… keyword=…] text` + 缩进的 `→ outlinks (n)` / `← inlinks (n)` + `via predicate=… anchor=#…` | 📊 `metadata.results` / `metadata.link_expansion` / `metadata.counts={vector,keyword,returned,hybrid}` | 🛠️ 并行 `vector_search` + `keyword_search` → RRF 融合(K=60,按 chunk.id 合并)→ `min_score` 过滤 → `limit` 截断 → 邻居 meta 注入 | +| 🧪 demo | 🪄 `demo_echo` (`demo_echo_step1` + `step2`) | `call_server("demo_echo", query=…, min_score=…)` | 📥 `query:str=""` | 🎚️ `min_score:float=0.5` | 🛠️ step1:`processed_query = query.strip().lower()`,`adjusted_min_score = min_score * 0.9`,写回 context | 📤 step2:`answer = "echo: {processed_query} (min_score={adjusted_min_score})"` | 📊 `metadata = {step, query, min_score, processed_query, adjusted_min_score}` | +| 🌊 demo | 🌊 `stream_demo` (`stream_demo_step1` + `step2`) | `call_server("stream_demo", query=…, repeat=…, interval=…)` | 📥 `query:str=""` | 🎚️ `repeat:int=10` | 🎚️ `interval:float=0.1`(秒/字符)| 🛠️ step1:`stream_text = query * repeat` 写回 context | 📤 step2:按字符 `add_stream_string(ch, ChunkEnum.CONTENT)` 流式输出,`asyncio.sleep(interval)` 节流 | +| 📂 crud | 📖 `read` (`read_step`) | `call_server("read", path=…, …)` | 📥 `path:str` ⭐(**完整相对路径**,相对于 vault;绝对路径会被拒绝;非 `.md` 后缀拒绝)| 🎚️ `start_line:int=null`(1-based, 含端点)| 🎚️ `end_line:int=null`(1-based, 含端点)| 🎚️ `max_bytes:int=51200`(截断阈值)| 📤 `answer = 选中的行内容`,超过 `max_bytes` 时附加 `--- TRUNCATED ---` 续读指引(`start_line=…`)| 📊 `metadata.path` / `metadata.total_lines`(出错路径才会附带)| 🛠️ 流程:`BaseStep.resolve_path(raw, require_md=True)` → `aiofiles.os.stat` → `read_file_safe`(utf-8-sig BOM 容忍、UnicodeDecodeError fallback `errors=ignore`)→ `split("\n")` 切片 `[s-1:e]` → `truncate_text_output` 按字节截断保行 | + +使用示例: + +```bash +# 启动(默认 default.yaml) +reme4 start + +# 指定 config 与服务端口 +reme4 start config=paw.yaml service.port=8181 + +# 查找在跑的 reme +reme4 find_reme +# HOST=127.0.0.1 PORT=8000 PID=12345 + +# 列出所有可用 action(client 端处理,不转服务端) +reme4 list + +# 转发到服务端的 step:所有 key=value 透传为 step kwargs +reme4 help +reme4 health_check +reme4 version +reme4 reindex +reme4 search query="latency 问题" limit=10 min_score=0.2 vector_weight=0.6 + +# 读取 vault 下的 markdown(完整相对路径;无后缀自动补 .md;可按行切片或限制字节) +reme4 read path=Templates/Recipe.md +reme4 read path=Notes start_line=1 end_line=20 +reme4 read path=Big.md max_bytes=4096 + +# 通过 MCP backend 调用 +reme4 search query="..." backend=mcp +``` + +@sen +| file | upload/download/move/delete/stat/list | 文件操作CRUD | +| property | read/update/delete | frontmatter CRUD | | +| graph | traverse/retarget | path="My Note" directtion=forward/backward depth=1 predicat=xxx | + +@wangce +| crud | write | path="New Note" name="xxx" description="xxx" metadata={}, content="# Hello" (4 字段都必填,frontmatter 只写 name/description) | +| crud | read | path="Templates/Recipe.md" | +| crud | edit | path="Templates/Recipe.md" old="xxx" new="xxx" | +| crud | append | path="My Note" content="New line" | + +| crud | delete | path="My Note +| daily_crud | daily_xxx | 与 crud 参数保持一致 | + +- daily_resolve name=xxxx (符合一定规范 win下要求) +- daily_list date=xxxx 返回path +- daily_index + +frontmatter read path +frontmatter update path metadata={} +frontmatter delete path keys=[] + +delete path +download path=xxx(内部相对路径)download_path=(外部绝对路径,可选) +upload path=xxx(外部绝对路径)description="xxx" metadata=xxx 返回内部相对路径 加metadata +stat path +list path +mv path=xxx new_path=xxx + +traverse path=xxx direction=xxx depth=xxx + +# 日记类型 + +| 类型 | 路径 | 说明 | +|-----------|-----------------------------------------------|-----------------------------| +| daily | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | 按日期归档的原始信息记录 | +| topic | topic/{topic:-personal(agent)}/{xxxx}.md | 按主题聚类的二次加工内容 | +| proactive | todo | 基于 daily / topic 思考后主动推送的消息 | + +# 生成Job + +| 任务 | 输入 | 输出 | 触发时机 | 说明 | +|-------------------------|---------------|-----------------------------------------------|-----------------------------|------------------------------------------------------| +| 日记summary @sen @wangce | msg | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | freq (every_n_turn、compact) | 把 msg 的信息写入 daily 目录 | +| 主题dream + 生成链接 @sen | daily/xxx | knowledge/xxx | /dream | 把 daily 目录的内容按主题聚类合并到 topic 目录, 主动在文档中建立 [[link]] 关联 | +| 主动proactive @wangce | daily / topic | proactive_query | pre_query | 思考 daily / topic 信息,主动决定推送给用户的消息 | + +2. file_parser + a. 抽象基类 parse: @jinli + ⅰ. 输入是path:相对路径 + ⅱ. 输出是FileMetadata & list[FileChunks] & list[FileEdge] + b. default parser 兼容老方案 @jinli + ⅰ. 带overlap的chunking策略 ,不输出FileEdge + c. markdown parser @sen + ⅰ. 根据markdown ast做chunk,不需要overlap + ⅱ. 增加一个索引的chunk chunk_type @锦鲤 file_chunk_type content/index + ⅲ. 增加link的正则解析:predicate:: [[path#anchor]] +3. file_store @sen + a. 抽象存储: + ⅰ. filenode = file + path + st_mtime + metadata + list[FileEdge] + ⅱ. graph=dict[str, filenode] 内存+json + ⅲ. list[FileChunk] 存db + b. 抽象基类 + ⅰ. graph:fellow dict的操作 update/get/set + ⅱ. chunks dict[str, list[chunk]] + 1. delete_chunks_by_path + 2. update_chunks_by_path + 3. list_chunks_by_path + 4. vector_search/keyword_search + ⅲ. 手写一个bm25检索 + ⅳ. 【核心】检索机制 vector bm25 graph 如何进行融合 +4. file_watcher @jinli + a. 抽象基类 + ⅰ. on_start: + 1. file_store 的start 在前,加载graph,file_watcher在后,递归扫描目录 + a. 通过ms_time对比graph,on_change 进行改动 + ⅱ. on_change: + 1. 更新/增加: + a. delete_chunks_by_path 更新数据库 + b. upate_chunks_by_path 更新数据库 + c. 更新graph + 2. 删除 + a. delete_chunks_by_path 更新数据库 + +MemorySchema + +1. markdown文件结构 @sen + a. formatter: + ⅰ. name + ⅱ. desc +2. memory文件结构目录 + a. MEMORY.md + b. msg/files -> daily/YYYYMMDD/YYYYMMDD.md + xxxx.md + ⅰ. YYYYMMDD.md + 1. xxx -> xxxx.md + 2. xxx -> xxxd.md + ⅱ. + c. daily -> topic/topic_l1/topic_l1.md + xxx.md + topic_l2 + d. proactive + +steps: + +1. 治理(算法+LLM): + a. 节点关联P0:现有的链接做补充,挖掘新的LLM的link + ⅰ. /Users/yuli/workspace/ReMe/reme2/component/edge_extractor/llm_edge_extractor.py + ⅱ. 移动到steps + b. 节点整合/节点拆分/节点归档 + c. 健康度检查 +2. retrieve 调用store的检索 +3. 原子steps:reme edit +4. 组合steps:总结: + a. - freq (every_n_turn、compact) -> daily_summarizer + b. topic (/dream ) -> topic_summarizer(daily_xx -> topic_xx) + c. proactive -> proactive_summarizer(personal_xxx -> proactive_query - pre_query diff --git a/docs4/structure.md b/docs4/old/structure.md similarity index 100% rename from docs4/structure.md rename to docs4/old/structure.md diff --git a/docs4/reme_design.md b/docs4/reme_design.md index d2e69904..aa671f1c 100644 --- a/docs4/reme_design.md +++ b/docs4/reme_design.md @@ -1,192 +1,385 @@ -# 快速测试 +# ReMe 设计文档 -```bash -# 终端 A:启动服务 -reme4 start +## 整体定位 -# 终端 B:调用 version 验证服务可用 -reme4 version -# 预期输出:✅ ReMe v{__version__} +> 一句话总结:**自进化的个人知识库**——你只管往里扔东西和对话,它自己长成一张知识图谱。 + +## 特性1:记忆分层 + +记忆按"原始 → 浅加工 → 深加工"三层组织: + +### 1.1 目录结构 + +``` +- resource/【原始素材】 # 外部渠道摄入 / 手动放入 + - YYYY-MM-DD/ # 按日期归档 + - session_{id}.jsonl # 对话原始记录 + - {channel}_{xxxx}.html # 网页抓取、邮件等 + - {channel}_{xxxx}.md # Markdown 资料 +- daily/【日记,浅加工】 # auto-memory 自动写入 + - YYYY-MM-DD.md # 当天索引页,汇总所有事件 + - YYYY-MM-DD/ + - session_{id}.md # 按 session 拆分的日志 + - resource_{id}.md # 对素材的加工笔记 +- digest/【深加工】 # auto-dream 持续打磨 + - personal/ # 用户偏好、习惯、身份 + - procedure/ # 方法论、步骤、工作流 + - wiki/ # 通用知识、决策先例 ``` -# 基础Job +### 1.2 分层详解 -@jinli +| 目录 | 存什么 | 谁写入 | 举例 | +|---------------------|----------------|-------------|-----------------------------------| +| `resource/` | 原始文件(研报、网页、邮件) | upload / 手动 | PDF 研报、对话 JSONL | +| `daily/` | 每天的事件记录 | auto-memory | "调试登录 CSS"、"与 Alice 聚餐" | +| `digest/procedure/` | 方法论、步骤 | auto-dream | "webpack 编译卡死排查路径" | +| `digest/personal/` | 用户画像、偏好 | auto-dream | "用户不爱写注释"、"用户喜欢 pnpm" | +| `digest/wiki/` | 通用知识、决策先例 | auto-dream | "光伏产业链"、"React Server Components" | -入口:`reme4/reme.py::main()` → `parse_args(*sys.argv[1:])` 解析首个位置参数为 `action`,后续 `key=value` 解析为 kwargs(支持 -`service.port=8080` 的 dot notation;自动剥离 `--` / `-` 前缀;值会做 bool / int / float / JSON 转换)。 +`resource/` 和 `daily/` 是只增不删的流水账;`digest/` 下三个桶是反复消费的精华层,各桶有独立的整合 prompt。 -调用模式: +## 特性2:Obsidian 兼容的 Markdown 格式 -- `start`:本地启动 `ReMe(Application)` 服务(不经过 client) -- `find_reme`:本地探测正在运行的 reme,不调用服务 -- `list`:在 client 端拦截,不转发到服务端,直接返回 action 目录 -- 其他 action:通过 `call_server(action, **kwargs)` → `R.get(ComponentEnum.CLIENT, backend)` 实例化客户端并流式打印(任意未列出的 - step register name 都按本规则透传) +所有笔记都是标准 Markdown + Obsidian 语法,可以直接用 Obsidian 打开浏览: -通用可选参数 `backend:str=http`(取值 `http` / `mcp`,对应 `reme4/components/client/{http_client,mcp_client}.py` 中 -`@R.register` 注册名);服务端默认 host/port 见 `reme4/constants.py`,可由 `start` 端通过 `service.host=` / `service.port=` -覆盖。 - -说明:📥 输入参数 | 📤 输出 | ⭐ 必填 | 🎚️ 默认值 | 🛠️ 内部行为 | 📊 metadata - -| 分类 | 指令 (register name) | 入口 | 参数 & 行为 | -|------------|--------------------------------------------------|-------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 🚀 本地 | 🟢 `start` | `reme.py:30` → `ReMe(**kwargs).run_app()` | 📥 可选 `config=`(默认加载 `reme4/config/default.yaml`,`.yaml/.yml/.json` 都支持,含 `${ENV:-default}` 占位符)| 可选 `service.host=` / `service.port=` 等任意 dot-notation 覆盖 | 🛠️ 流程:`load_env()` → `resolve_app_config(**kwargs)` deep merge → `precheck_start(svc)`(`utils/service_utils.py:72`:目标 host:port 已有 reme → 打印 `reme already running ...` 直接返回;端口被其他进程占用 → stderr 提示 `port {port} occupied. Start on another port: reme4 start service.port=` 并 `sys.exit(1)`)→ 启动服务 | -| 🚀 本地 | 🧭 `find_reme` | `reme.py:36` → `utils/service_utils.py:89` | 📥 无 | 📤 发现服务则 stdout 打印 `HOST={host} PORT={port} PID={pid or 'unknown'}`;未发现则 stderr 提示 `reme not started. Try: reme start` 并 `sys.exit(1)` | 🛠️ 流程:先探 `REME_DEFAULT_HOST:REME_DEFAULT_PORT`(`health_check` 命中算 `reme`),再 `pgrep -af "reme.* start"` 扫描其他端口 | -| 🛰️ 客户端 | 📜 `list` | `components/client/base_client.py:36` | 📥 无 | 📤 服务端可用 action 目录(JSON,`indent=2 ensure_ascii=False`)| 🛠️ 在 `BaseClient.__call__` 中拦截,不进入 `_execute`,直接调用 `list_actions()`(HTTP/MCP backend 各自实现) | -| 🌐 通用 step | 🆘 `help` (`help_step`) | `call_server("help")` | 📥 无 | 📤 `answer` 一行一个 job:`🛠️ \`{name}\` — {description} 📥 {params}`,参数渲染为 `name:type*`(必填) / `name:type={default}` / `name:type` | 📊 `metadata.job_count` | 🛠️ 自动跳过名为 `help` 的 job | -| 🌐 通用 step | 🩺 `health_check` (`health_check_step`) | `call_server("health_check")` | 📥 无 | 📤 `answer = "✅/❌ ReMe v{version} - healthy/unhealthy"` | 📊 `metadata.health = {version, healthy, components}` | 🧩 覆盖组件:`embedding_model`(🟢 is_started/is_healthy/model_name/dimensions/cache_size/memory) · `file_graph`(🕸️ n_nodes/n_edges/n_virtual\|n_pending/memory) · `file_store`(📦 n_chunks/n_chunks_with_embedding/memory) · `file_watcher`(👀 background_running/watch_paths) · `keyword_index`(🔤 n_docs/vocab_size/memory) | 🛠️ deep sizeof(含 numpy.nbytes),未启动 / 后台未跑 / embedding 不健康 → ❌ | -| 🌐 通用 step | 🏷️ `version` (`version_step`) | `call_server("version")` | 📥 无 | 📤 `answer = reme4.__version__` | 📊 `metadata.version` | -| 🌐 通用 step | 🔄 `reindex` (`reindex_step`) | `call_server("reindex")` | 📥 无 | 📤 `answer = "🔄 Reindexed {added} file(s)"` | 📊 `metadata.counts = {added, ...}` | 🛠️ 流程:`file_watcher.close()` → `file_store.clear()` → `file_watcher.update_store()` → `file_watcher.start()`(finally 保证重启) | -| 🔎 search | 🔍 `search` (`search_step`) | `call_server("search", query=…, …)` | 📥 `query:str` ⭐ | 🎚️ `limit:int=5`(>0) | 🎚️ `min_score:float=0.0` | ⚖️ `vector_weight:float=0.7` ∈[0,1](keyword 权 = 1-vw)| 🔀 `candidate_multiplier:float=3.0`(candidates = min(200, limit×mult))| 🔗 `expand_links:bool=True` | 🔢 `max_links_per_direction:int=10` | 🎚️ `search_filter:dict={}` | 📤 `answer` 每命中一行 `path:start-end [score=… vector=… keyword=…] text` + 缩进的 `→ outlinks (n)` / `← inlinks (n)` + `via predicate=… anchor=#…` | 📊 `metadata.results` / `metadata.link_expansion` / `metadata.counts={vector,keyword,returned,hybrid}` | 🛠️ 并行 `vector_search` + `keyword_search` → RRF 融合(K=60,按 chunk.id 合并)→ `min_score` 过滤 → `limit` 截断 → 邻居 meta 注入 | -| 🧪 demo | 🪄 `demo_echo` (`demo_echo_step1` + `step2`) | `call_server("demo_echo", query=…, min_score=…)` | 📥 `query:str=""` | 🎚️ `min_score:float=0.5` | 🛠️ step1:`processed_query = query.strip().lower()`,`adjusted_min_score = min_score * 0.9`,写回 context | 📤 step2:`answer = "echo: {processed_query} (min_score={adjusted_min_score})"` | 📊 `metadata = {step, query, min_score, processed_query, adjusted_min_score}` | -| 🌊 demo | 🌊 `stream_demo` (`stream_demo_step1` + `step2`) | `call_server("stream_demo", query=…, repeat=…, interval=…)` | 📥 `query:str=""` | 🎚️ `repeat:int=10` | 🎚️ `interval:float=0.1`(秒/字符)| 🛠️ step1:`stream_text = query * repeat` 写回 context | 📤 step2:按字符 `add_stream_string(ch, ChunkEnum.CONTENT)` 流式输出,`asyncio.sleep(interval)` 节流 | -| 📂 crud | 📖 `read` (`read_step`) | `call_server("read", path=…, …)` | 📥 `path:str` ⭐(**完整相对路径**,相对于 vault;绝对路径会被拒绝;非 `.md` 后缀拒绝)| 🎚️ `start_line:int=null`(1-based, 含端点)| 🎚️ `end_line:int=null`(1-based, 含端点)| 🎚️ `max_bytes:int=51200`(截断阈值)| 📤 `answer = 选中的行内容`,超过 `max_bytes` 时附加 `--- TRUNCATED ---` 续读指引(`start_line=…`)| 📊 `metadata.path` / `metadata.total_lines`(出错路径才会附带)| 🛠️ 流程:`BaseStep.resolve_path(raw, require_md=True)` → `aiofiles.os.stat` → `read_file_safe`(utf-8-sig BOM 容忍、UnicodeDecodeError fallback `errors=ignore`)→ `split("\n")` 切片 `[s-1:e]` → `truncate_text_output` 按字节截断保行 | - -使用示例: - -```bash -# 启动(默认 default.yaml) -reme4 start - -# 指定 config 与服务端口 -reme4 start config=paw.yaml service.port=8181 - -# 查找在跑的 reme -reme4 find_reme -# HOST=127.0.0.1 PORT=8000 PID=12345 - -# 列出所有可用 action(client 端处理,不转服务端) -reme4 list - -# 转发到服务端的 step:所有 key=value 透传为 step kwargs -reme4 help -reme4 health_check -reme4 version -reme4 reindex -reme4 search query="latency 问题" limit=10 min_score=0.2 vector_weight=0.6 - -# 读取 vault 下的 markdown(完整相对路径;无后缀自动补 .md;可按行切片或限制字节) -reme4 read path=Templates/Recipe.md -reme4 read path=Notes start_line=1 end_line=20 -reme4 read path=Big.md max_bytes=4096 - -# 通过 MCP backend 调用 -reme4 search query="..." backend=mcp +``` +┌─────────────────────────────────────────────────────────────┐ +│ 一个 .md 文件的完整结构 │ +├─────────────────────────────────────────────────────────────┤ +│ --- │ +│ name: 宁德时代 ← YAML front matter │ +│ description: 全球动力电池龙头 │ +│ tags: [新能源, 电池] │ +│ --- │ +├─────────────────────────────────────────────────────────────┤ +│ 所属行业:: [[新能源]] ← 语义化链接(Dataview) │ +│ 竞争对手:: [[比亚迪]] │ +│ │ +│ # 基本面 ← Markdown 正文 │ +│ 全球动力电池出货量第一,核心技术为 │ +│ [[CTP]] 和 [[钠离子电池]]…… ← 标准 wikilink │ +│ │ +│ 参考 ![[2026Q1调研纪要]] ← 嵌入引用 │ +├─────────────────────────────────────────────────────────────┤ +│ ↓ AST 语义分块 ↓ │ +│ chunk 1: [标题骨架] + 正文片段 │ +│ chunk 2: [标题骨架] + 正文片段 │ +└─────────────────────────────────────────────────────────────┘ ``` -@sen -| file | upload/download/move/delete/stat/list | 文件操作CRUD | -| property | read/update/delete | frontmatter CRUD | | -| graph | traverse/retarget | path="My Note" directtion=forward/backward depth=1 predicat=xxx | +### 2.1 YAML front matter -@wangce -| crud | write | path="New Note" name="xxx" description="xxx" metadata={}, content="# Hello" (4 字段都必填,frontmatter 只写 name/description) | -| crud | read | path="Templates/Recipe.md" | -| crud | edit | path="Templates/Recipe.md" old="xxx" new="xxx" | -| crud | append | path="My Note" content="New line" | +每个笔记头部的元数据: -| crud | delete | path="My Note -| daily_crud | daily_xxx | 与 crud 参数保持一致 | +```markdown +--- +name: 光伏产业链研究 +description: 从硅料到组件的全链条梳理 +tags: [新能源, 光伏, 产业链] +--- +``` -- daily_resolve name=xxxx (符合一定规范 win下要求) -- daily_list date=xxxx 返回path -- daily_index +`name` / `description` 是约定字段,其余键值对全部保留,不会丢弃任何自定义字段。 -frontmatter read path -frontmatter update path metadata={} -frontmatter delete path keys=[] +### 2.2 四种 wikilink 写法 -delete path -download path=xxx(内部相对路径)download_path=(外部绝对路径,可选) -upload path=xxx(外部绝对路径)description="xxx" metadata=xxx 返回内部相对路径 加metadata -stat path -list path -mv path=xxx new_path=xxx +| 写法 | 示例 | 语义 | +|------|----------------|----------| +| 标准链接 | `[[光伏产业链]]` | 指向目标文件 | +| 锚点链接 | `[[钴#应用]]` | 指向特定章节 | +| 别名链接 | `[[宁德时代\|宁德]]` | 自定义显示文本 | +| 嵌入引用 | `![[钴]]` | 内联嵌入目标内容 | -traverse path=xxx direction=xxx depth=xxx +### 2.3 语义化链接(Dataview 风格) -# 日记类型 +普通 wikilink 只说"A 提到了 B",语义化链接还能表达"A 和 B 是什么关系": -| 类型 | 路径 | 说明 | -|-----------|-----------------------------------------------|-----------------------------| -| daily | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | 按日期归档的原始信息记录 | -| topic | topic/{topic:-personal(agent)}/{xxxx}.md | 按主题聚类的二次加工内容 | -| proactive | todo | 基于 daily / topic 思考后主动推送的消息 | +```markdown +所属行业:: [[新能源]] ← 行级属性(独占一行) +总部:: [[宁德]] +[竞争对手:: [[比亚迪]]] ← 内联属性(嵌入正文中) +``` -# 生成Job +`WikilinkHandler` 是全系统唯一的 wikilink 解析入口,确保 parser、graph、search 各层规则一致。 -| 任务 | 输入 | 输出 | 触发时机 | 说明 | -|-------------------------|---------------|-----------------------------------------------|-----------------------------|------------------------------------------------------| -| 日记summary @sen @wangce | msg | {daily}/xxxx-mm-dd.md + xxxx-mm-dd/{event}.md | freq (every_n_turn、compact) | 把 msg 的信息写入 daily 目录 | -| 主题dream + 生成链接 @sen | daily/xxx | knowledge/xxx | /dream | 把 daily 目录的内容按主题聚类合并到 topic 目录, 主动在文档中建立 [[link]] 关联 | -| 主动proactive @wangce | daily / topic | proactive_query | pre_query | 思考 daily / topic 信息,主动决定推送给用户的消息 | +### 2.4 AST 感知的语义分块 -2. file_parser - a. 抽象基类 parse: @jinli - ⅰ. 输入是path:相对路径 - ⅱ. 输出是FileMetadata & list[FileChunks] & list[FileEdge] - b. default parser 兼容老方案 @jinli - ⅰ. 带overlap的chunking策略 ,不输出FileEdge - c. markdown parser @sen - ⅰ. 根据markdown ast做chunk,不需要overlap - ⅱ. 增加一个索引的chunk chunk_type @锦鲤 file_chunk_type content/index - ⅲ. 增加link的正则解析:predicate:: [[path#anchor]] -3. file_store @sen - a. 抽象存储: - ⅰ. filenode = file + path + st_mtime + metadata + list[FileEdge] - ⅱ. graph=dict[str, filenode] 内存+json - ⅲ. list[FileChunk] 存db - b. 抽象基类 - ⅰ. graph:fellow dict的操作 update/get/set - ⅱ. chunks dict[str, list[chunk]] - 1. delete_chunks_by_path - 2. update_chunks_by_path - 3. list_chunks_by_path - 4. vector_search/keyword_search - ⅲ. 手写一个bm25检索 - ⅳ. 【核心】检索机制 vector bm25 graph 如何进行融合 -4. file_watcher @jinli - a. 抽象基类 - ⅰ. on_start: - 1. file_store 的start 在前,加载graph,file_watcher在后,递归扫描目录 - a. 通过ms_time对比graph,on_change 进行改动 - ⅱ. on_change: - 1. 更新/增加: - a. delete_chunks_by_path 更新数据库 - b. upate_chunks_by_path 更新数据库 - c. 更新graph - 2. 删除 - a. delete_chunks_by_path 更新数据库 +传统 RAG 按固定 token 长度切片,经常切坏文档结构。ReMe 基于 Markdown AST 做语义分块: -MemorySchema +- 按 H1/H2/H3 章节嵌套建树,递归分块 +- **每个 chunk 保留完整标题骨架**——检索到片段后一眼看出它在哪个章节下 +- 表格自动重复表头、代码块保留 fence、列表按项打包 -1. markdown文件结构 @sen - a. formatter: - ⅰ. name - ⅱ. desc -2. memory文件结构目录 - a. MEMORY.md - b. msg/files -> daily/YYYYMMDD/YYYYMMDD.md + xxxx.md - ⅰ. YYYYMMDD.md - 1. xxx -> xxxx.md - 2. xxx -> xxxd.md - ⅱ. - c. daily -> topic/topic_l1/topic_l1.md + xxx.md + topic_l2 - d. proactive +``` +示例 chunk: +───────────────────── +# 光伏产业链 +## 上游:硅料 +### 多晶硅工艺 +[chunk 正文] ← 实际内容 +## 中游:硅片 ← 骨架(只有标题) +## 下游:组件 +───────────────────── +``` -steps: +## 特性3:自进化 -1. 治理(算法+LLM): - a. 节点关联P0:现有的链接做补充,挖掘新的LLM的link - ⅰ. /Users/yuli/workspace/ReMe/reme2/component/edge_extractor/llm_edge_extractor.py - ⅱ. 移动到steps - b. 节点整合/节点拆分/节点归档 - c. 健康度检查 -2. retrieve 调用store的检索 -3. 原子steps:reme edit -4. 组合steps:总结: - a. - freq (every_n_turn、compact) -> daily_summarizer - b. topic (/dream ) -> topic_summarizer(daily_xx -> topic_xx) - c. proactive -> proactive_summarizer(personal_xxx -> proactive_query - pre_query +> **ReMe 的记忆不是被动存的,是主动长成知识图谱的。** + +``` +用户对话 / 外部素材 + │ + ├───────────────────────────────────┐ + ▼ ▼ +┌────────────┐ ┌────────────┐ +│ auto-memory│ │auto-resource│ +│ 对话→日记 │ │ 素材→解析 │ +└─────┬──────┘ └──────┬─────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────┐ +│ daily/ │ +│ (事件日记 + resource 加工笔记) │ +└─────────────────────┬───────────────────────────┘ + │ + ▼ 定时触发 + ┌─────────────┐ + │ auto-dream │ + │ 提炼 + 建图谱 │ + └──────┬──────┘ + │ + ▼ +┌─────────────────────────────────────────────────┐ +│ digest/ │ +│ (知识卡片 + wikilink 互联 = 知识图谱) │ +└─────────────────────────────────────────────────┘ +``` + +用户什么都不用做,Agent 在后台让笔记自己长出结构。 + +### 3.1 auto-resource + +监控 `resource/` 目录,新文件进来后自动解析内容、整理为结构化笔记写入 `daily/` 下。 + +### 3.2 auto-memory + +对话进行时,ReMe 在后台把上下文自动写入当天日记。不是简单的对话摘要——而是一个拥有完整读写能力的 LLM +Agent,自己决定记什么、怎么组织、合并还是新增。 + +### 3.3 auto-dream + auto-link:睡眠式记忆整理 + +借鉴人在睡眠中巩固记忆的机制——把日记和素材提炼成知识卡片,并自动织出图谱关系: + +``` + ┌───────────────────────────┐ + │ daily/2026-05-28/xxx.md │ ← 一篇日记或素材 + └─────────────┬─────────────┘ + │ + ╔═════════════════════════════════════╗ + ║ Phase 1 — Extract(一个 Agent) ║ + ║ "这份材料教了什么道理?" ║ + ║ ║ + ║ 输出 N 个抽象单元,各带 bucket 标签 ║ + ║ (空 → 结束,没东西值得记) ║ + ╚══════════╤══════════╤═══════════════╝ + │ │ + ┌─────────────┘ └──────────────┐ + ▼ ▼ + ╔══════════════════════════════╗ ╔══════════════════════════════╗ + ║ Phase 2 — Integrate ║ ║ Phase 2 — Integrate ║ + ║ (每个 unit 独立一个 Agent) ║ ║ (每个 unit 独立一个 Agent) ║ + ║ ║ ║ ║ + ║ 1. search + traverse 召回 ║ ║ 1. search + traverse 召回 ║ + ║ 2. 决策: CREATE / UPDATE ║ ║ 2. 决策: CREATE / UPDATE ║ + ║ 3. 写入 + 自动织链接 ║ ║ 3. 写入 + 自动织链接 ║ + ╚══════════════╤═══════════════╝ ╚══════════════╤═══════════════╝ + │ │ + ▼ ▼ + ┌──────────────────────────────────────────────────────────────┐ + │ digest/ │ + │ procedure/key-rotation.md ←─ derived_from:: [[daily/..]] │ + │ wiki/credential-compliance.md ─ relates_to:: [[...]] │ + │ personal/user-pr-pref.md │ + └──────────────────────────────────────────────────────────────┘ + 知识图谱自动生长 +``` + +**Phase 1 筛选**——多个事实说明同一个道理就合并为一个 unit,分到三个桶:`procedure`(怎么做)/ `personal`(用户偏好)/ `wiki` +(通用知识)。没东西值得记则流程结束。 + +**Phase 2 先搜后写**——先搜已有 digest,再决策:新建(CREATE)、追加佐证(CORROBORATE)、补充精度(REFINE)、修正矛盾(CORRECT)。 + +**auto-link 是写入的副产品**——写 digest 时自动加 `derived_from:: [[素材]]` 溯源 + `relates_to::` 概念互联,图谱随每次 +dream 自动变密。 + +**CronDreamer 定时批跑**——每天扫描当天所有 daily + resource 文件,逐个执行上述管线。 + +## 特性4:混合索引 + 渐进式展开 + +``` +用户提问: "宁德时代的电池技术?" + │ + ├──────────────────────┬──────────────────────────┐ + ▼ ▼ │ + ┌─────────────────┐ ┌──────────────────┐ │ + │ 全文倒排索引 │ │ 向量索引 │ │ + │ (numpy + jieba) │ │ (faiss) │ │ + │ │ │ │ │ + │ "宁德时代" 精确 │ │ "动力电池龙头" │ │ + │ 命中 │ │ 语义近似命中 │ │ + └────────┬────────┘ └────────┬─────────┘ │ + │ text_weight=0.3 │ vector_weight=0.7 │ + └──────────┬──────────┘ │ + ▼ │ + ┌───────────────┐ │ + │ RRF 融合排序 │ │ + │ score = Σ(w/(k+rank)) │ + └───────┬───────┘ │ + ▼ │ + ┌──────────────────────────────────────┐ │ + │ 第一跳:Top-K chunk 全文 + 评分 │ │ + └───────────────────┬──────────────────┘ │ + ▼ │ + ┌──────────────────────────────────────┐ │ + │ 第二跳:邻居目录(只有标题,不展开正文)│ ← wikilink 图谱 │ + └───────────────────┬──────────────────┘ │ + ▼ │ + ┌──────────────────────────────────────┐ │ + │ 第 N 跳:Agent 按需追问,展开正文 │ │ + └──────────────────────────────────────┘ │ +``` + +### 4.1 混合索引构建 + +两套索引并行维护,各擅其长: + +- **全文倒排索引**(numpy + jieba BM25)——精确匹配专有名词,搜"宁德时代"必须命中。纯 Python 实现,增量更新,无原生扩展依赖。 +- **向量索引**(faiss)——语义相似度,搜"锂电正极原料"能命中"钴"。基于 embedding 模型生成稠密向量。 + +### 4.2 基于 RRF 的混合检索 + +两条通路并行跑(`asyncio.gather`),用 RRF(Reciprocal Rank Fusion)融合排序: + +``` +融合分 = Σ( weight_i / (k + rank_i) ) k=60, vector_weight=0.7, text_weight=0.3 +``` + +为什么要两路?纯向量容易错配名词("苹果公司"≈"水果"),纯关键词抓不到同义改写——融合互补盲区。 + +### 4.3 渐进式链接展开 + +传统 RAG 一次性把 Top-K 全塞进上下文,token 浪费且噪音多。ReMe 分跳展开,按需深入: + +**第一跳** — 返回命中 chunk 全文 + 分数明细 + +**第二跳** — 展开 wikilink 邻居的"目录"(只有标题,不展开正文): + +``` +========== digest/wiki/宁德时代.md:5-22 [score=0.0247 vector=0.0156 keyword=0.0091] ========== +# 宁德时代 +全球动力电池出货量第一,核心技术为 CTP(Cell to Pack)和钠离子电池…… + + outlinks (2): + → digest/wiki/磷酸铁锂.md name="磷酸铁锂正极路线" description="磷酸铁锂与三元路线对比" via predicate=相关技术 + → digest/wiki/固态电池.md name="固态电池技术路线" description="全固态与半固态进展" via predicate=技术演进 + inlinks (2): + ← daily/2026-03-18/宁德调研.md name="宁德时代调研纪要" description="2026Q1产能与订单跟踪" via plain + ← digest/wiki/新能源产业链.md name="新能源产业链全景" description="从锂矿到整车的全链条" via predicate=下游应用 +``` + +**第 N 跳** — Agent 看过"目录"后,自己决定哪些邻居值得深入,再发起 read 拿正文。 + +二跳目录每条只占一行(最多 10 outlink + 10 inlink),Agent 拥有全局视野却不撑爆上下文。 + +## 特性5:多 Agent 框架集成 + +ReMe 不做独立 Agent 产品,而是作为**能力层**被任意框架调用: + +| 集成路径 | 适用对象 | 方式 | +|---------------------|----------------------|---------------------------------------------| +| SDK 深度集成 | AgentScope / Qwenpaw | middleware 注册 tools + prompt,hook 注册 auto-* | +| MCP Tool + skill.md | Claude Code | MCP 注册 Tool,配 skill.md 开箱即用,hook 注册 auto-* | +| HTTP API + CLI | 通用方案 | skill.md + CLI 调用 | + +--- + +# 二、工程架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Service 层(HTTP / MCP 双协议) │ +│ FastAPI + FastMCP,同一套 Job 同时暴露为 REST 和 MCP Tool │ +├─────────────────────────────────────────────────────────────────┤ +│ Application 层 │ +│ 配置加载 → 组件初始化 → Job 注册 → start() / close() 生命周期 │ +├─────────────────────────────────────────────────────────────────┤ +│ Job 层(编排) │ +│ 每个 Job = 一组 Step 的有序管线,YAML 声明式配置 │ +├─────────────────────────────────────────────────────────────────┤ +│ Step 层(业务逻辑) │ +│ 原子操作单元,按功能域分组:file_io / index / evolve / common │ +├─────────────────────────────────────────────────────────────────┤ +│ Component 层(可插拔基础设施) │ +│ 统一注册表 R,一行配置切换实现 │ +│ file_store / embedding / keyword_index / llm / file_graph │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## 2.1 服务层 + +每个 Job 同时暴露为两种协议,写一次逻辑、两种方式调用: + +| 协议 | 传输方式 | 适用场景 | +|---------------|-------------------------------|------------------------------| +| HTTP(FastAPI) | JSON POST / SSE | REST 调用、Web 前端 | +| MCP(FastMCP) | stdio / SSE / streamable-http | Claude Code、Cursor 等 MCP 客户端 | + +- **按需拉起**:Agent 检测到服务未运行时自动后台启动,用户无感知 +- **服务发现**:通过 `REME_SERVICE_INFO` 环境变量广播地址,`find_reme` 一键探活 + +## 2.2 组件系统(Component) + +统一注册表 `R`,所有基础设施都是可插拔的——改一行配置就能切换后端: + +| 组件 | 干什么 | 可选后端 | +|-----------------|---------------|-----------------------| +| file_store | 文件存储 + 索引协调 | local | +| file_graph | wikilink 双向图谱 | local / nx / neo4j | +| keyword_index | 全文倒排索引 | bm25(numpy + jieba) | +| embedding_store | 向量存储与检索 | local(faiss) | +| embedding | 文本转向量 | openai 兼容接口 | +| llm | 大模型调用 | anthropic / openai 兼容 | +| tokenizer | 分词 | regex / jieba | + +## 2.3 Job 列表 + +**Job** 是 ReMe 暴露给外部的操作单元——同一个 Job 可以作为 Python 函数直接调用、作为 MCP Tool 被 Agent 使用、也可以作为 CLI +命令执行。 + +| 类别 | Job | 功能 | +|------|---------------------------|----------------------------------| +| 检索 | `search` | 混合检索(向量 + BM25 + RRF)+ 渐进式图展开 | +| 检索 | `traverse` | 从指定路径遍历 wikilink 图谱 | +| 文件读写 | `read` | 读取 markdown 文件内容 | +| 文件读写 | `read_image` | 读取图片文件(base64) | +| 文件读写 | `write` | 新建或覆写 markdown 文件(含 frontmatter) | +| 文件读写 | `edit` | 文件内查找替换 | +| 文件读写 | `delete` | 删除文件,返回残留入边 | +| 文件读写 | `move` | 移动 / 重命名,自动重写 wikilink | +| 文件读写 | `list` | 列出目录下文件 | +| 文件读写 | `stat` | 文件元信息(大小、修改时间) | +| 文件读写 | `frontmatter_read` | 读取 frontmatter | +| 文件读写 | `frontmatter_update` | 合并更新 frontmatter | +| 文件读写 | `frontmatter_delete` | 删除 frontmatter 字段 | +| 日记管理 | `daily_create` | 幂等创建当天日记文件 | +| 日记管理 | `daily_list` | 列出某天的所有日记 | +| 日记管理 | `daily_reindex` | 重建当天索引页 | +| 索引维护 | `reindex` | 清空并全量重建索引 | +| 索引维护 | `update_store_index_loop` | 后台监听文件变更,增量更新 | +| 自进化 | `auto_memory` | 对话记录写入日记(LLM Agent) | +| 自进化 | `dream` | 单文件记忆提炼到 digest(LLM Agent) | +| 自进化 | `auto-dream` | 批量扫描当天文件,逐个 dream | +| 系统 | `health_check` | 组件健康检查 | +| 系统 | `version` | 返回版本号 | +| 系统 | `help` | 列出所有已注册 Job | diff --git a/docs4/reme_scene.md b/docs4/reme_scene.md new file mode 100644 index 00000000..41b3acb5 --- /dev/null +++ b/docs4/reme_scene.md @@ -0,0 +1,231 @@ +# ReMe 应用场景 + +## 金融场景:产业链知识库 + +**主角**:王分析师,新能源行业研究员,每天处理 10+ 篇研报、数十条产业新闻、若干场公司调研。 + +**痛点**:信息散落在飞书文档、PDF 研报、微信群消息、调研纪要里,"上次调研宁德时代时聊到的钴价话题"再也找不回来。 + +### 一周内 ReMe 自动织出的产业链图谱 + +--- + +#### Day 1(周一)盘后:素材摄入 + 对话 + +王分析师把今天看到的 3 篇研报扔进 `resource/`,又和 Agent 口述了对刚果(金)矿权变更的看法: + +``` +对话片段: +> 今天嘉能可发了三季报,钴产量同比下滑 18%…… +> 刚果(金)那边的政策变化,对洛阳钼业 KFM 矿的影响要重点跟…… +> 下游三元正极厂商已经开始转向高镍低钴方案…… +``` + +**auto-memory** 实时把对话写入当天日记;**auto-resource** 自动解析研报写入加工笔记: + +``` +daily/ +├── 2026-05-18.md ← 当天索引页,汇总所有事件 +└── 2026-05-18/ + ├── session_001.md ← auto-memory 写入的对话日志 + │ (含嘉能可三季报、刚果金矿权、高镍化趋势等事件) + ├── resource_001.md ← auto-resource 对研报 1 的加工笔记 + ├── resource_002.md ← 研报 2 加工笔记 + └── resource_003.md ← 研报 3 加工笔记 +``` + +**Day 1 夜间 auto-dream**——CronDreamer 扫描当天 4 个文件,逐个执行 Extract → Integrate 管线: + +处理 `session_001.md`: +- **Phase 1 Extract**:从对话日志中提取 3 个抽象单元——「嘉能可钴产量下滑」(wiki)、「刚果金矿权政策风险」(wiki)、「三元正极高镍化趋势」(wiki) +- **Phase 2 Integrate**(每个 unit 独立一个 Agent): + - 搜索已有 digest,均无匹配 → 决策 **CREATE** + - 新建 `digest/wiki/嘉能可.md`、`digest/wiki/钴.md`、`digest/wiki/三元正极.md` + - 写入时自动织链接:`derived_from:: [[daily/2026-05-18/session_001]]`,以及概念互联 `relates_to:: [[三元正极]]` + +处理 `resource_001.md`(嘉能可三季报): +- **Phase 1**:提取「嘉能可钴业务财务数据」(wiki) +- **Phase 2**:搜索到刚刚新建的 `digest/wiki/嘉能可.md` → 决策 **CORROBORATE**,追加财务佐证段落 + +Day 1 结束时 `digest/wiki/` 下新增: + +``` +digest/wiki/ +├── 嘉能可.md ← CREATE + CORROBORATE(研报佐证) +├── 钴.md ← CREATE +└── 三元正极.md ← CREATE +``` + +`钴.md` 长这样: + +```markdown +--- +name: 钴 +description: 锂电正极材料关键原料,主产区刚果(金) +tags: [新能源, 原料, 钴] +--- + +所属领域:: [[新能源]] +下游产品:: [[三元正极]] + +# 钴 + +## 供给端 +主要生产商 [[嘉能可]],产能集中于刚果(金)。 +嘉能可三季度钴产量同比下滑 18%。 +derived_from:: [[daily/2026-05-18/session_001]] + +## 政策风险 +刚果(金)矿权政策变化,可能影响 KFM 矿运营。 +derived_from:: [[daily/2026-05-18/session_001]] +``` + +--- + +#### Day 2(周二):宁德时代调研 + +王分析师参加宁德时代调研会后,和 Agent 聊调研要点: + +``` +> 宁德今年全面切换 9 系高镍三元,钴用量还会继续降…… +> 产能利用率 85%,比上季度高 5 个点…… +``` + +auto-memory 写入 `daily/2026-05-19/session_001.md`。 + +**Day 2 夜间 auto-dream** 处理这份 session: +- **Phase 1**:提取「宁德时代高镍切换」(wiki)、「宁德时代产能利用率」(wiki) +- **Phase 2**: + - 「宁德时代高镍切换」→ 搜索到 `digest/wiki/三元正极.md` 已存在高镍化内容 → 决策 **REFINE**,补充"宁德 9 系切换"作为具体案例,并新建 `digest/wiki/宁德时代.md` + - 「产能利用率」→ 无匹配 → 写入 `digest/wiki/宁德时代.md`(已存在,追加章节) + +Day 2 结束时图谱新增节点和边: + +``` +digest/wiki/ +├── 嘉能可.md +├── 钴.md +├── 三元正极.md ← REFINE:新增宁德 9 系切换案例 +└── 宁德时代.md ← CREATE:含高镍切换 + 产能数据 + relates_to:: [[三元正极]] + relates_to:: [[钴]] +``` + +--- + +#### Day 3(周三):亿纬电话会 + 洛阳钼业跟踪 + +两场对话产生两份 session。夜间 auto-dream 逐个处理: + +- `session_001.md`(亿纬电话会)→ Phase 2 搜到 `宁德时代.md`、`三元正极.md` → CREATE `digest/wiki/亿纬锂能.md`,并在 `三元正极.md` 上 CORROBORATE 高镍趋势 +- `session_002.md`(洛阳钼业跟踪)→ Phase 2 搜到 `钴.md` → REFINE `钴.md`,补充洛阳钼业 KFM 矿最新动态;CREATE `digest/wiki/洛阳钼业.md` + +Day 3 结束时图谱: + +``` +digest/wiki/ +├── 嘉能可.md +├── 洛阳钼业.md ← CREATE +├── 钴.md ← REFINE:补充洛阳钼业信息 +│ relates_to:: [[嘉能可]], [[洛阳钼业]], [[三元正极]] +├── 三元正极.md ← CORROBORATE:亿纬佐证 +│ relates_to:: [[钴]], [[宁德时代]], [[亿纬锂能]] +├── 宁德时代.md +└── 亿纬锂能.md ← CREATE +``` + +每个节点都是**当天 dream 从一份 daily 文件中提取并整合的结果**,不存在跨天"聚合"——跨文件的关联通过 Phase 2 的 search 自然发现已有 digest,从而把新信息写入正确的位置。 + +--- + +#### Day 5(周五):用户主动检索 + +王分析师准备组会要讲新能源板块,主动问 Agent: + +> **"帮我分析一下锂电相关上下游"** + +Agent 调用 ReMe 的 `search` Job,走向量 + BM25 + RRF 融合检索,命中已有的 digest 节点,并通过渐进式展开获取上下游全貌: + +**第一跳——直接命中 chunk 全文 + 评分**: + +``` +digest/wiki/钴.md:5-22 [score=0.0234 vector=0.0156 keyword=0.0078] +# 钴 / ## 供给端 +主要生产商嘉能可、洛阳钼业,产能集中于刚果(金)…… + +digest/wiki/三元正极.md:10-30 [score=0.0211 vector=0.0148 keyword=0.0063] +# 三元正极 / ## 高镍低钴路线 +2026 年起主流厂商加速 9 系产品,宁德已全面切换…… + +digest/wiki/宁德时代.md:1-20 [score=0.0193 vector=0.0135 keyword=0.0058] +digest/wiki/嘉能可.md:5-30 [score=0.0167 vector=0.0117 keyword=0.0050] +digest/wiki/洛阳钼业.md:1-22 [score=0.0152 vector=0.0106 keyword=0.0046] +``` + +**第二跳——展开 wikilink 邻居目录(只有标题,不展开正文)**: + +``` +digest/wiki/钴.md 的邻居: + outlinks (3): + → digest/wiki/三元正极.md name="三元正极" description="高镍低钴技术路线" via predicate=下游产品 + → digest/wiki/嘉能可.md name="嘉能可" description="全球钴业巨头" via predicate=relates_to + → digest/wiki/洛阳钼业.md name="洛阳钼业" description="KFM 矿运营商" via predicate=relates_to + inlinks (2): + ← daily/2026-05-18/session_001.md name="盘后对话" via plain + ← daily/2026-05-19/session_001.md name="宁德调研" via plain + +digest/wiki/三元正极.md 的邻居: + outlinks (2): + → digest/wiki/宁德时代.md name="宁德时代" description="全球动力电池龙头" via predicate=relates_to + → digest/wiki/钴.md name="钴" description="锂电正极关键原料" via predicate=relates_to + inlinks (1): + ← digest/wiki/亿纬锂能.md name="亿纬锂能" description="动力电池厂商" via predicate=relates_to +``` + +**第 N 跳——Agent 按需深入**:Agent 看过目录后,决定展开 `亿纬锂能.md` 的正文获取补充信息,调用 `read` Job 拉取。 + +Agent 基于检索结果**直接回复**王分析师: + +> "锂电产业链分三段:上游钴矿(嘉能可、洛阳钼业,刚果金集中)、中游三元正极(高镍化加速)、下游电池厂(宁德/亿纬)。这周你提到的事件分别落在:嘉能可三季报 → 上游产能收缩;高镍化趋势 → 中游路线切换;宁德 9 系切换 → 下游需求验证。" + +--- + +#### Day 7:图谱已经长出层次 + +经过一周每天的 auto-dream 逐文件处理,图谱自然生长出来: + +``` + ┌─────────────┐ + ┌────────►│ 锂电 │◄────────┐ + │ └──────┬──────┘ │ + │ upstream │ │ upstream + │ │ 下游产品 │ + ┌───────┴──────┐ ▼ ┌──────┴──────┐ + │ 钴 │ ┌─────────┐ │ 锂 │ + │ (刚果金产区) │◄──┤ 原料 ├────►│ (盐湖产区) │ + └───────┬──────┘ └────┬────┘ └─────────────┘ + │ relates_to │ relates_to + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ 嘉能可 │ │ 三元正极 │◄── 高镍化趋势 + │ 洛阳钼业 │ └──────┬───────┘ + └──────────────┘ │ relates_to + ▼ + ┌──────────────┐ + │ 宁德时代 │ ← Day 2 调研 + │ 亿纬锂能 │ ← Day 3 电话会 + └──────────────┘ +``` + +**没有一个节点是凭空编造的**——每条边对应笔记里的一句 `relates_to:: [[X]]` 或 `derived_from:: [[daily/...]]`,每个节点点开就是 Markdown,每段内容都能追溯到原始 daily 事件。图谱不是一次性生成的,而是每天 dream 一点、链接一点,渐进生长出来的。 + +--- + +### ReMe 在这个场景下的核心价值 + +分析师只负责"看 + 说",知识图谱自己长出来: + +- **auto-memory** 把每次对话实时写入当天日记 +- **auto-dream** 每天逐文件执行 Extract → Integrate 管线,先搜已有 digest 再决策(CREATE / CORROBORATE / REFINE / CORRECT),知识卡片渐进生长 +- **auto-link** 是 dream 写入的副产品——`derived_from::` 溯源 + `relates_to::` 概念互联,图谱随每次 dream 自动变密 +- **混合检索 + 渐进展开** 让 Agent 先看骨架再决定深入哪个节点,不把 Top-K 全文塞进上下文 diff --git a/reme4/application.py b/reme4/application.py index 8a19eb49..7d737ec5 100644 --- a/reme4/application.py +++ b/reme4/application.py @@ -48,7 +48,7 @@ class Application(BaseComponent): cfg = self.config vault_path = Path(cfg.vault_dir).absolute() vault_path.mkdir(parents=True, exist_ok=True) - for subdir in [cfg.metadata_dir, cfg.daily_dir, cfg.digest_dir, cfg.resource_dir]: + for subdir in [cfg.metadata_dir, cfg.resource_dir, cfg.dialog_dir, cfg.daily_dir, cfg.digest_dir]: if subdir: (vault_path / subdir).mkdir(parents=True, exist_ok=True) diff --git a/reme4/config/default.yaml b/reme4/config/default.yaml index 611dff78..837b9a53 100644 --- a/reme4/config/default.yaml +++ b/reme4/config/default.yaml @@ -475,7 +475,7 @@ components: supported_extensions: [ "md" ] chunked: backend: chunked - supported_extensions: [ "txt", "html", "json", "yaml", "py" ] + supported_extensions: [ "txt", "html", "json", "jsonl", "yaml", "py" ] default: backend: default diff --git a/reme4/schema/application_config.py b/reme4/schema/application_config.py index 35924bb4..a3b07d34 100644 --- a/reme4/schema/application_config.py +++ b/reme4/schema/application_config.py @@ -28,28 +28,20 @@ class ApplicationConfig(BaseModel): """Root config for the ReMe application.""" app_name: str = Field(default=os.getenv("APP_NAME", "ReMe"), description="Application display name") - vault_dir: str = Field(default=".reme", description="Vault root directory (knowledge base) for runtime files") + vault_dir: str = Field(default=".reme", description="Vault root directory for runtime files") metadata_dir: str = Field(default="reme_metadata", description="Subdirectory for ReMe persistent state") + resource_dir: str = Field(default="resource", description="Subdirectory for external assets") + dialog_dir: str = Field(default="dialog", description="Subdirectory for dialog memory") daily_dir: str = Field(default="daily", description="Subdirectory for daily memory") - digest_dir: str = Field(default="digest", description="Subdirectory for digest") - resource_dir: str = Field( - default="resource", - description="Subdirectory for passively-received external assets (upload bucket)", - ) + digest_dir: str = Field(default="digest", description="Subdirectory for digest memory") enable_logo: bool = Field(default=True, description="Show ASCII logo on startup") language: str = Field(default="", description="Default language for LLM interactions") log_to_console: bool = Field(default=True, description="Log to console") log_to_file: bool = Field(default=True, description="Log to file") mcp_servers: dict[str, dict] = Field(default_factory=dict, description="MCP server configs by name") service: ComponentConfig = Field(default_factory=ComponentConfig, description="Service endpoint config") - jobs: dict[str, JobConfig] = Field( - default_factory=dict, - description="Job definitions keyed by job name", - ) - thread_pool_max_workers: int = Field( - default=0, - description="Max worker threads in the shared thread pool; 0 to disable", - ) + jobs: dict[str, JobConfig] = Field(default_factory=dict, description="Job definitions keyed by job name") + thread_pool_max_workers: int = Field(default=0, description="Max worker threads; 0 to disable") components: dict[ComponentEnum, dict[str, ComponentConfig]] = Field( default_factory=dict, description="Component registry keyed by type then name", diff --git a/tests4/integration/test_llm.py b/tests4/integration/test_llm.py index ef23c838..f5e30351 100644 --- a/tests4/integration/test_llm.py +++ b/tests4/integration/test_llm.py @@ -43,51 +43,6 @@ async def _make_app() -> Application: return app -def test_llm_demo_step_basic_chat(): - """LLMDemoStep drives Agent through self.llm.""" - - async def run(): - with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp): - app = await _make_app() - try: - step = LLMDemoStep(app_context=app.context) - response = await step( - query="What is 1 + 1? Reply with just the number.", - ) - text = (response.answer or "").strip() - print(f"\n[basic_chat] response: {text!r}") - assert text, "Empty assistant response" - assert "2" in text, f"Expected '2' in response, got: {text!r}" - print("✓ test_llm_demo_step_basic_chat passed") - finally: - await app.close() - - asyncio.run(run()) - - -def test_llm_demo_step_with_tool(): - """LLMDemoStep registers the add tool and the agent invokes it.""" - - async def run(): - with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp): - app = await _make_app() - try: - step = LLMDemoStep(app_context=app.context) - response = await step( - query="Use the add tool to compute 21 + 21 and report the result.", - sys_prompt="Use the `add` tool whenever the user asks to add numbers.", - use_add_tool=True, - ) - text = (response.answer or "").strip() - print(f"\n[with_tool] response: {text!r}") - assert "42" in text, f"Expected '42' in response, got: {text!r}" - print("✓ test_llm_demo_step_with_tool passed") - finally: - await app.close() - - asyncio.run(run()) - - class MathResult(BaseModel): """Structured output for a math computation.""" @@ -110,66 +65,78 @@ class SentimentAnalysis(BaseModel): ) -def test_llm_demo_step_structured_output(): - """LLMDemoStep generates structured output via generate_structured_output.""" - - async def run(): - with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp): - app = await _make_app() - try: - step = LLMDemoStep(app_context=app.context) - response = await step( - query="What is 15 multiplied by 7? Show your work.", - sys_prompt="You are a math tutor. Solve the problem step by step.", - structured_model=MathResult, - ) - structured = response.metadata.get("structured_output") - print(f"\n[structured_output] result: {structured}") - assert structured is not None, "structured_output should not be None" - assert "result" in structured, "structured_output should have 'result' field" - assert structured["result"] == 105, f"Expected result=105, got: {structured['result']}" - assert "expression" in structured, "structured_output should have 'expression' field" - assert "explanation" in structured, "structured_output should have 'explanation' field" - print("✓ test_llm_demo_step_structured_output passed") - finally: - await app.close() - - asyncio.run(run()) +async def _run_basic_chat(app: Application) -> None: + step = LLMDemoStep(app_context=app.context) + response = await step( + query="What is 1 + 1? Reply with just the number.", + ) + text = (response.answer or "").strip() + print(f"\n[basic_chat] response: {text!r}") + assert text, "Empty assistant response" + assert "2" in text, f"Expected '2' in response, got: {text!r}" + print("✓ test_llm_demo_step_basic_chat passed") -def test_llm_demo_step_structured_output_enum(): - """LLMDemoStep structured output with Literal/enum fields.""" +async def _run_with_tool(app: Application) -> None: + step = LLMDemoStep(app_context=app.context) + response = await step( + query="Use the add tool to compute 21 + 21 and report the result.", + sys_prompt="Use the `add` tool whenever the user asks to add numbers.", + use_add_tool=True, + ) + text = (response.answer or "").strip() + print(f"\n[with_tool] response: {text!r}") + assert "42" in text, f"Expected '42' in response, got: {text!r}" + print("✓ test_llm_demo_step_with_tool passed") - async def run(): - with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp): - app = await _make_app() - try: - step = LLMDemoStep(app_context=app.context) - response = await step( - query="Analyze the sentiment: 'I absolutely love this product! It exceeded all my expectations.'", - sys_prompt="You are a sentiment analysis expert. Analyze the given text.", - structured_model=SentimentAnalysis, - ) - structured = response.metadata.get("structured_output") - print(f"\n[structured_enum] result: {structured}") - assert structured is not None, "structured_output should not be None" - assert ( - structured["sentiment"] == "positive" - ), f"Expected sentiment='positive', got: {structured['sentiment']}" - assert 0 <= structured["confidence"] <= 1, f"Confidence should be 0-1, got: {structured['confidence']}" - assert isinstance(structured["key_phrases"], list), "key_phrases should be a list" - assert len(structured["key_phrases"]) > 0, "key_phrases should not be empty" - print("✓ test_llm_demo_step_structured_output_enum passed") - finally: - await app.close() - asyncio.run(run()) +async def _run_structured_output(app: Application) -> None: + step = LLMDemoStep(app_context=app.context) + response = await step( + query="What is 15 multiplied by 7? Show your work.", + sys_prompt="You are a math tutor. Solve the problem step by step.", + structured_model=MathResult, + ) + structured = response.metadata.get("structured_output") + print(f"\n[structured_output] result: {structured}") + assert structured is not None, "structured_output should not be None" + assert "result" in structured, "structured_output should have 'result' field" + assert structured["result"] == 105, f"Expected result=105, got: {structured['result']}" + assert "expression" in structured, "structured_output should have 'expression' field" + assert "explanation" in structured, "structured_output should have 'explanation' field" + print("✓ test_llm_demo_step_structured_output passed") + + +async def _run_structured_output_enum(app: Application) -> None: + step = LLMDemoStep(app_context=app.context) + response = await step( + query="Analyze the sentiment: 'I absolutely love this product! It exceeded all my expectations.'", + sys_prompt="You are a sentiment analysis expert. Analyze the given text.", + structured_model=SentimentAnalysis, + ) + structured = response.metadata.get("structured_output") + print(f"\n[structured_enum] result: {structured}") + assert structured is not None, "structured_output should not be None" + assert structured["sentiment"] == "positive", f"Expected sentiment='positive', got: {structured['sentiment']}" + assert 0 <= structured["confidence"] <= 1, f"Confidence should be 0-1, got: {structured['confidence']}" + assert isinstance(structured["key_phrases"], list), "key_phrases should be a list" + assert len(structured["key_phrases"]) > 0, "key_phrases should not be empty" + print("✓ test_llm_demo_step_structured_output_enum passed") + + +async def _run_all() -> None: + with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp): + app = await _make_app() + try: + await _run_basic_chat(app) + await _run_with_tool(app) + await _run_structured_output(app) + await _run_structured_output_enum(app) + finally: + await app.close() if __name__ == "__main__": print("=== LLMDemoStep + Agent integration tests ===") - test_llm_demo_step_basic_chat() - test_llm_demo_step_with_tool() - test_llm_demo_step_structured_output() - test_llm_demo_step_structured_output_enum() + asyncio.run(_run_all()) print("\nAll integration tests passed!")