diff --git a/.augment/commands/memory.md b/.augment/commands/memory.md new file mode 100644 index 0000000..680da9d --- /dev/null +++ b/.augment/commands/memory.md @@ -0,0 +1,6 @@ +--- +name: 'memory' +description: '记忆管理 - 统一管理项目级和用户级记忆' +--- + +IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/_omp/workflows/memory/workflow.md, READ its entire contents and follow its directions exactly! diff --git a/.augment/skills/memory-extraction/SKILL.md b/.augment/skills/memory-extraction/SKILL.md new file mode 100644 index 0000000..6cb9ea6 --- /dev/null +++ b/.augment/skills/memory-extraction/SKILL.md @@ -0,0 +1,477 @@ +--- +name: memory-extraction +description: Agent-only workflow for extracting key information from conversations, code changes, and deployments into structured memory files. Automatically updates `_omp/memory/` directory and notifies other agents. Triggered automatically at conversation end or when valuable information is detected. +metadata: + author: Wendy (Workflow Builder) + version: "2.1" + language: zh-CN + audience: agent-only +--- + +# Memory Extraction Skill (Agent-Only) + +## 目的 + +**自动提取对话中的关键信息**,智能路由到正确的存储系统: +- **项目级信息** → `_omp/memory/` 目录 (Git 版本控制) +- **用户级信息** → `openmemory` MCP (跨项目共享) + +> **Agent-Only 原则**: +> - **自动触发**:对话结束时或检测到有价值信息时自动运行 +> - **智能分类**:根据信息类型自动选择存储位置 +> - **零人工干预**:无需用户确认,静默执行 +> - **双系统同步**:同时管理 `_omp/memory/` 和 `openmemory` + +## 核心目录结构 + +``` +_omp/ # OpenMemory Plus 核心目录 +├── commands/ +│ ├── memory.md # 主命令入口 +│ └── memory-actions/ # 7 个子动作 +├── skills/ +│ └── memory-extraction/ # 本 Skill +│ ├── SKILL.md +│ ├── scripts/validate.sh +│ └── templates/*.tmpl +└── memory/ # 项目级记忆 + ├── project.yaml # 项目配置 (SSOT) + ├── decisions.yaml # 技术决策记录 + ├── changelog.yaml # 变更历史 + └── sessions/ # 会话记录 +``` + +## 双层记忆架构 + +| 系统 | 存储位置 | 用途 | +|------|----------|------| +| 项目级 | `_omp/memory/` | 项目配置、技术决策、变更记录 | +| 用户级 | `openmemory` MCP | 用户偏好、技能、跨项目上下文 | + +## 分类规则 + +> 📖 **详细规则**: 参见 `references/classification-rules.md` + +### 快速分类指南 + +| 信号 | Scope | Confidence | 存储位置 | +|------|-------|------------|----------| +| "我喜欢/偏好/习惯" | PERSONAL | EXPLICIT | openmemory | +| "项目使用/配置为" | PROJECT | EXPLICIT | _omp/memory/ | +| "决定/选择/采用" | PROJECT | EXPLICIT | decisions.yaml | +| 用户反复使用某模式 | PERSONAL | INFERRED | openmemory | +| "试试/也许/可能" | EPHEMERAL | UNCERTAIN | 不存储 | + +### 置信度阈值 + +- **存储阈值**: confidence >= 0.4 +- **自动存储**: confidence >= 0.7 +- **需确认**: 0.4 <= confidence < 0.7 + +### 敏感信息过滤 + +**禁止存储**(检测后阻止): + +| 类型 | 检测模式 | +|------|----------| +| API Key | `sk-`, `api_key`, `token=`, `bearer` | +| 密码 | `password`, `secret`, `credential` | +| 私钥 | `-----BEGIN`, `PRIVATE KEY` | +| 个人信息 | 身份证号, 银行卡号, 手机号 | + +### ROT 过滤规则 + +> 📖 **详细规则**: 参见 `references/rot-filter.md` + +| 类型 | 检测方式 | 处理 | +|------|----------|------| +| **Redundant** | 语义相似度 > 0.85 | 合并或跳过 | +| **Obsolete** | 超过 TTL 且无访问 | 标记或删除 | +| **Trivial** | 长度 < 10 或匹配阻止模式 | 丢弃 | + +**快速判断**: +- ❌ "好的" / "OK" / "明白了" → Trivial +- ❌ 与已有记忆相似度 > 0.85 → Redundant +- ❌ 被明确否定的旧信息 → Obsolete + +## 触发条件 + +### 自动触发(推荐) +- 对话结束时自动检查是否有新信息 +- 检测到以下关键事件时立即触发: + - 部署状态变更(成功/失败) + - 新服务配置 + - 重要技术决策 + - 项目里程碑 + +### 手动触发 +- 用户说 `/extract-memory` 或 "保存到记忆" +- 用户说 "记住这个" 或 "更新配置" + +--- + +## 🔄 自动化流程 + +``` +对话/操作 → 信息检测 → 分类 → 提取 → 存储 → 通知 + ↓ ↓ ↓ ↓ ↓ ↓ + 输入源 触发判断 类型识别 结构化 写入YAML 更新Agent +``` + +### Phase 1: 信息检测 + +**检测规则**(按优先级): + +| 优先级 | 信息类型 | 检测信号 | 示例 | +|--------|----------|----------|------| +| P0 | 部署变更 | `deploy`, `vercel`, `wrangler`, URL 变化 | 新域名上线 | +| P0 | 服务配置 | `config`, `secret`, `token`, API 密钥 | 更新 VERCEL_TOKEN | +| P1 | 技术决策 | `决定`, `选择`, `采用`, 架构变更 | 选择 YAML 格式 | +| P1 | 项目里程碑 | `完成`, `上线`, `发布`, 版本号 | v1.0 发布 | +| P2 | 路径变更 | 目录创建/移动, 文件重组 | 创建 _omp/memory/ | +| P2 | 工具配置 | CLI 安装, 依赖更新 | 安装 resumes-cli | + +### Phase 2: 信息分类与路由 + +根据检测结果,**智能路由**到正确的存储系统: + +#### 项目级 → `_omp/memory/` + +| 分类 | 目标文件 | 内容类型 | +|------|----------|----------| +| `project` | `_omp/memory/project.yaml` | 项目常量、部署信息、路径 | +| `decisions` | `_omp/memory/decisions.yaml` | 重要技术决策记录 | +| `changelog` | `_omp/memory/changelog.yaml` | 变更历史 | + +#### 用户级 → `openmemory` + +| 分类 | MCP 工具 | 内容类型 | +|------|----------|----------| +| `preference` | `add_memories_openmemory` | 用户偏好、习惯 | +| `skill` | `add_memories_openmemory` | 用户技能、经验 | +| `context` | `add_memories_openmemory` | 对话上下文、历史 | + +#### openmemory MCP 调用 + +**写入用户记忆**: +``` +Tool: add_memories_openmemory +Parameter: text = "用户偏好: {提取的偏好信息}" +``` + +**搜索用户记忆**: +``` +Tool: search_memory_openmemory +Parameter: query = "{搜索关键词}" +``` + +### Phase 3: 结构化提取 + +**存储格式** (参见 `templates/memory-entry.yaml.tmpl`): + +```yaml +# 项目记忆示例 +- key: tech-stack + value: "TypeScript + React + Vite" + metadata: + scope: PROJECT + confidence: 1.0 + source: explicit + temporality: permanent + timestamps: + created_at: "2026-02-03T10:00:00Z" + updated_at: "2026-02-03T10:00:00Z" + last_accessed: "2026-02-03T10:00:00Z" + tracking: + access_count: 1 + decay_score: 1.0 +``` + +**用户记忆** (通过 MCP): + +``` +Tool: add_memories_openmemory +Parameter: text = "[SCOPE:PERSONAL][CONF:0.9] 用户偏好: 2 空格缩进,函数式编程风格" +``` + +> 💡 在 text 中嵌入元数据标签,便于后续检索和过滤 + +**其他提取模板**: + +```yaml +# 部署变更 +deployment: + {service_name}: + url: {new_url} + status: {active|pending|failed} + updated_at: {timestamp} + updated_by: {agent|user} + +# 技术决策 +decisions: + - id: {uuid} + date: {date} + title: {decision_title} + context: {why_this_decision} + choice: {what_was_chosen} + alternatives: [{other_options}] + impact: {affected_areas} + +# 路径变更 +paths: + {path_key}: {new_path} +``` + +### Phase 4: 存储 + +**写入规则**: +1. 读取现有文件(如存在) +2. 合并新信息(不覆盖,追加/更新) +3. 保留注释和格式 +4. 添加 `last_updated` 时间戳 + +**示例写入**: +```yaml +# _omp/memory/project.yaml +deployment: + vercel: + url: https://web-zeta-six-79.vercel.app + status: active + updated_at: 2026-02-02T10:30:00Z # ← 自动更新 +``` + +### Phase 5: 通知其他 Agent + +更新完成后,在以下位置添加通知标记: + +```yaml +# _omp/memory/project.yaml (底部) +_meta: + last_updated: 2026-02-02T10:30:00Z + updated_by: memory-extraction-skill + changes: + - "deployment.vercel.status: pending → active" +``` + +--- + +## 输出格式 + +### `_omp/memory/project.yaml` (主配置) +见现有文件结构,本 Skill 负责自动更新。 + +### `_omp/memory/sessions/{date}.yaml` (会话记录) +```yaml +date: 2026-02-02 +sessions: + - id: session-001 + start: "10:00" + end: "11:30" + summary: "部署 Vercel 并配置 Cloudflare Worker" + key_actions: + - "更新 VERCEL_TOKEN GitHub Secret" + - "创建 _omp/memory/ 目录结构" + decisions: + - "采用 YAML 格式作为 memory 存储格式" +``` + +### `_omp/memory/decisions.yaml` (决策记录) +```yaml +decisions: + - id: dec-2026-02-02-001 + date: 2026-02-02 + title: "Memory 存储格式选择" + context: "需要跨 Agent 共享配置,支持多 IDE" + choice: "YAML 格式" + alternatives: ["JSON", "Markdown", "TOML"] + rationale: "人类可读 + 机器可解析 + 支持注释" +``` + +--- + +## 与其他 Agent 的集成 + +### 读取方(其他 Agent) + +其他 Agent 应在启动时读取 `_omp/memory/project.yaml`: + +```markdown + +> 📌 **配置中心**: 项目常量统一存储在 `_omp/memory/project.yaml` +``` + +### 写入方(本 Skill) + +本 Skill 是 `_omp/memory/` 的唯一写入者,确保: +- 格式一致性 +- 无冲突写入 +- 变更可追溯 + +--- + +## 错误处理 + +### 写入失败场景 + +| 场景 | 处理策略 | +|------|----------| +| **YAML 语法错误** | 验证失败,不写入,通知用户修复 | +| **文件不存在** | 从模板创建新文件 | +| **权限拒绝** | 记录错误,跳过此次更新 | +| **Schema 验证失败** | 回滚到上一版本,输出差异 | + +### 回退机制 + +1. **写入前备份**: 修改前复制到 `_omp/memory/.backup/` +2. **原子写入**: 写入临时文件,验证后重命名 +3. **错误日志**: 记录到 `_omp/memory/sessions/{date}.yaml` 的 `errors` 字段 + +### 验证脚本 + +```bash +# 验证所有 YAML 文件 +_omp/skills/memory-extraction/scripts/validate.sh +``` + +--- + +## 触发条件详解 + +### 自动触发时机 + +| 触发信号 | 说明 | 示例 | +|----------|------|------| +| 用户结束对话 | 用户说 "bye", "结束", "exit", "谢谢" | `用户: 好的,先这样` | +| 部署完成 | 检测到 deploy/vercel/wrangler 输出 | `vercel --prod` 成功 | +| 配置变更 | 修改了 env/secret/config 文件 | 更新 `.env` | +| 创建新目录 | 创建了项目级目录 | `mkdir _omp/memory/` | +| 重要决策 | 对话中明确了技术选型 | `决定使用 YAML 格式` | + +### 不触发的场景 + +- 普通代码编辑(非配置文件) +- 读取操作(无写入) +- 临时文件操作 + +--- + +## 辅助资源 + +### 模板文件 + +- `templates/memory-entry.yaml.tmpl` - 记忆条目模板 (含元数据和冲突追踪) + +> 💡 **扩展模板**: 如需 session 或 decision 专用模板,可基于 memory-entry.yaml.tmpl 创建 + +### 参考文档 + +- `references/classification-rules.md` - 多维度分类规则 +- `references/rot-filter.md` - ROT 过滤规则 +- `references/decay-model.md` - Ebbinghaus 衰减模型 +- `references/health-score.md` - 健康度计算公式 + +### Schema 验证 (可选) + +> ⚠️ **待实现**: Schema 验证文件尚未创建,可根据需要添加: +> - `_omp/memory/schema/project.schema.json` +> - `_omp/memory/schema/decisions.schema.json` + +--- + +## 执行检查清单 + +Agent 在对话结束前执行: + +- [ ] 是否有部署状态变更?→ 更新 `deployment` +- [ ] 是否有新路径/目录?→ 更新 `paths` +- [ ] 是否有重要决策?→ 记录到 `decisions.yaml` +- [ ] 是否有配置变更?→ 更新对应字段 +- [ ] 是否有用户偏好/技能?→ 存入 `openmemory` +- [ ] 更新 `_meta.last_updated`(使用精确 ISO 8601 时间戳) +- [ ] 运行 `validate.sh` 验证格式 + +--- + +## 冲突检测 + +Agent 在查询记忆时应检测两系统数据一致性: + +### 检测时机 + +- 用户询问配置/部署信息时 +- 执行 `/memory sync` 命令时 +- 发现两系统返回不同结果时 + +### 检测逻辑 + +``` +查询结果 + ↓ +提取关键实体 (URL, 配置值, 技术选型) + ↓ +比对 _omp/memory/ vs openmemory + ↓ +发现差异 → 提示用户确认 + ↓ +用户选择 → 更新/删除过时记录 +``` + +### 冲突处理 + +| 场景 | 处理方式 | +|------|----------| +| URL 不一致 | 提示用户,优先 `_omp/memory/` | +| 技术选型冲突 | 展示两边,请求决策 | +| 时间戳可判断 | 自动保留较新版本 | + +--- + +## 对话启动时自动加载 + +Agent 在对话开始时应**并行查询**两个系统: + +``` +对话开始 + ↓ +┌─────────────────┐ ┌─────────────────┐ +│ _omp/memory/ │ │ openmemory │ +│ (读取 YAML) │ │ (search_memory) │ +└────────┬────────┘ └────────┬────────┘ + │ │ + └───────────┬───────────┘ + ↓ + 上下文融合 +``` + +**加载步骤**: +1. 读取 `_omp/memory/project.yaml` 获取项目配置 +2. 调用 `search_memory_openmemory` 查询相关用户记忆 +3. 融合两边信息作为对话上下文 + +## 降级策略 + +**当 openmemory MCP 不可用时**: + +1. 检测 MCP 连接状态(调用失败或超时) +2. 降级到仅 `_omp/memory/` 存储 +3. 用户级信息临时存入 `_omp/memory/user-context.yaml` +4. 服务恢复后提示用户同步 + +```yaml +# _omp/memory/user-context.yaml (降级时使用) +_degraded: true +_reason: "openmemory MCP unavailable" +pending_memories: + - text: "用户偏好: 使用中文" + created_at: 2026-02-02T10:30:00Z +``` + +## 版本历史 + +| 版本 | 变更 | +|------|------| +| v2.1 | 目录重构:_omp/ 统一目录,移除 rules/,分类规则内嵌 | +| v2.0 | 双层记忆架构:整合 openmemory MCP,智能分类路由 | +| v1.1 | 添加错误处理、Schema 验证、模板文件、触发条件详解 | +| v1.0 | 初始版本:自动提取、YAML 存储、多 Agent 通知 | + diff --git a/.augment/skills/memory-extraction/references/classification-rules.md b/.augment/skills/memory-extraction/references/classification-rules.md new file mode 100644 index 0000000..231b39e --- /dev/null +++ b/.augment/skills/memory-extraction/references/classification-rules.md @@ -0,0 +1,53 @@ +# Memory Classification Rules + +## 多维度分类体系 + +### Dimension 1: Scope (范围) + +| Scope | 定义 | 存储位置 | 示例 | +|-------|------|----------|------| +| PERSONAL | 与用户个人相关 | openmemory | "我喜欢用 Vim" | +| PROJECT | 与当前项目相关 | _omp/memory/ | "项目使用 React" | +| UNIVERSAL | 跨项目通用知识 | openmemory | "TypeScript 比 JS 更安全" | +| EPHEMERAL | 仅当前会话有效 | 不存储 | "先试试这个方案" | + +### Dimension 2: Confidence (置信度) + +| Level | 范围 | 定义 | 处理方式 | +|------|------|------|----------| +| EXPLICIT | >= 0.9 | 用户明确陈述 | 直接存储 | +| INFERRED | 0.7 - 0.9 | 从行为推断 | 存储但标记 | +| UNCERTAIN | 0.4 - 0.7 | 需要确认 | 询问用户后存储 | +| NOISE | < 0.4 | 噪音信息 | 丢弃 | + +> ⚠️ **阈值说明**: +> - **存储阈值**: confidence >= 0.4 (UNCERTAIN 及以上) +> - **自动存储**: confidence >= 0.7 (INFERRED 及以上) +> - **需确认**: 0.4 <= confidence < 0.7 (UNCERTAIN 区间) + +### Dimension 3: Temporality (时效性) + +| Type | TTL | 示例 | +|------|-----|------| +| PERMANENT | 无限 | 用户偏好、项目架构 | +| CONTEXTUAL | 30d | 当前迭代目标 | +| EPHEMERAL | 会话 | 临时讨论 | + +## 分类决策树 + +``` +信息输入 + ↓ +是否敏感信息? → YES → 丢弃 + ↓ NO +是否 ROT? → YES → 丢弃 + ↓ NO +置信度 < 0.4? → YES → 丢弃 (NOISE) + ↓ NO +Scope = EPHEMERAL? → YES → 不存储 + ↓ NO +Scope = PERSONAL/UNIVERSAL? → YES → openmemory + ↓ NO +Scope = PROJECT → _omp/memory/ +``` + diff --git a/.augment/skills/memory-extraction/references/decay-model.md b/.augment/skills/memory-extraction/references/decay-model.md new file mode 100644 index 0000000..b09715c --- /dev/null +++ b/.augment/skills/memory-extraction/references/decay-model.md @@ -0,0 +1,63 @@ +# Memory Decay Model + +## Ebbinghaus 变体公式 + +``` +base_retention = e^(-t/S) +Retention(t) = base_retention + importance_boost × (1 - base_retention) + +其中: +- t = 距离上次访问的天数 +- S = 强度因子 (初始 30,每次访问 +10) +- importance_boost = 重要性加成 (0-0.5) +- 结果保证在 [0, 1] 范围内 +``` + +> 💡 **公式解释**: importance_boost 将 base_retention 向 1.0 方向提升, +> 但永远不会超过 1.0。例如 base=0.5, boost=0.5 → 0.5 + 0.5×0.5 = 0.75 + +## 强度因子 (S) 计算 + +```python +S = base_strength + (access_count * 10) + (explicit_mark * 50) + +# base_strength = 30 (默认) +# access_count = 访问次数 +# explicit_mark = 用户标记为重要 (0 或 1) +``` + +## 重要性加成 + +| 条件 | Boost | +|------|-------| +| 用户明确标记重要 | +0.5 | +| 高置信度 (>0.9) | +0.2 | +| 频繁访问 (>5次) | +0.1 | +| 项目核心配置 | +0.3 | + +> ⚠️ **Cap 限制**: `importance_boost = min(累加值, 0.5)`,最终值不超过 0.5 + +## 衰减状态分类 + +| Status | Retention | 建议操作 | +|--------|-----------|----------| +| 🟢 Active | >= 0.7 | 保持 | +| 🟡 Aging | 0.3-0.7 | 关注 | +| 🔴 Stale | 0.1-0.3 | 考虑清理 | +| ⚫ Cleanup | < 0.1 | 自动清理候选 | + +## 遗忘策略 + +### 自动遗忘 (无需确认) +- Retention < 0.1 且 90天未访问 +- 被明确标记为过时 + +### 确认遗忘 (需用户确认) +- Retention 0.1-0.3 且 60天未访问 +- 与新信息冲突的旧记忆 + +### 永不遗忘 +- 用户明确标记为重要 +- 安全/合规相关信息 +- 项目核心配置 + diff --git a/.augment/skills/memory-extraction/references/health-score.md b/.augment/skills/memory-extraction/references/health-score.md new file mode 100644 index 0000000..5a98782 --- /dev/null +++ b/.augment/skills/memory-extraction/references/health-score.md @@ -0,0 +1,67 @@ +# Memory Health Score + +## 健康度计算公式 + +```python +def calculate_health_score(memories): + """ + 计算记忆系统整体健康度 (0-100) + + Args: + memories: 所有记忆列表 + + Returns: + int: 健康度分数 (0-100) + """ + total = len(memories) + if total == 0: + return 100 # 无记忆时返回满分 + + # 1. 活跃率 (30% 权重) + active_count = count_by_status(memories, 'active') + active_ratio = active_count / total + + # 2. ROT 比例 (20% 权重) - 越低越好 + rot_count = count_by_status(memories, 'stale') + count_by_status(memories, 'cleanup') + rot_ratio = rot_count / total + + # 3. 平均置信度 (30% 权重) + avg_confidence = mean([m.confidence for m in memories]) + + # 4. 冲突率 (20% 权重) - 越低越好 + conflict_count = count_conflicts(memories) + conflict_ratio = conflict_count / total + + # 加权计算 + score = ( + active_ratio * 0.3 + + (1 - rot_ratio) * 0.2 + + avg_confidence * 0.3 + + (1 - conflict_ratio) * 0.2 + ) + return int(score * 100) +``` + +## 健康度等级 + +| 分数 | 等级 | Emoji | 说明 | +|------|------|-------|------| +| >= 80 | Excellent | ✅ | 系统健康 | +| 60-79 | Good | ⚠️ | 需要关注 | +| < 60 | Needs Attention | ❌ | 需要处理 | + +## 各指标健康阈值 + +| 指标 | 健康阈值 | 说明 | +|------|----------|------| +| 活跃率 | > 60% | Active 状态记忆占比 | +| ROT 比例 | < 20% | Stale + Cleanup 占比 | +| 平均置信度 | > 0.7 | 所有记忆的平均置信度 | +| 冲突率 | < 5% | 存在冲突的记忆占比 | + +## 使用方式 + +此公式被以下 workflow steps 引用: +- `workflows/memory/steps/status.md` - 状态显示 +- `workflows/memory/steps/metrics.md` - 质量指标面板 + diff --git a/.augment/skills/memory-extraction/references/rot-filter.md b/.augment/skills/memory-extraction/references/rot-filter.md new file mode 100644 index 0000000..c296a45 --- /dev/null +++ b/.augment/skills/memory-extraction/references/rot-filter.md @@ -0,0 +1,78 @@ +# ROT Filter Rules + +## Redundant (冗余) 检测 + +### 语义相似度检测 + +使用 Qdrant 向量搜索检测相似记忆: + +```yaml +redundancy: + similarity_threshold: 0.85 + actions: + - score >= 0.95: skip (完全重复) + - score >= 0.85: merge (合并到已有) + - score < 0.85: store (正常存储) +``` + +### 合并策略 + +当检测到相似记忆时: +1. 比较信息完整度 +2. 保留更完整的版本 +3. 合并补充信息 +4. 更新时间戳 + +## Obsolete (过时) 检测 + +### 时间衰减 + +```yaml +obsolescence: + default_ttl: 90d + check_triggers: + - on_access + - daily_scan + actions: + - age > 180d && access_count == 0: auto_delete + - age > 90d && access_count < 3: mark_stale + - age > 30d: calculate_decay +``` + +### 显式过时 + +检测信号: +- "不再使用 X" +- "改用 Y 替代 X" +- "X 已废弃" + +## Trivial (琐碎) 检测 + +### 信息密度阈值 + +```yaml +triviality: + min_length: 10 # 字符 + min_info_density: 0.3 + blocked_patterns: + # 中文确认词 + - "^(好的|OK|明白|收到|了解|知道了)$" + - "^(稍等|正在处理|请稍候).*" + - "^(是的|对|嗯|行|可以)$" + # 英文确认词 + - "^(ok|okay|sure|yes|yep|yeah|got it|roger|understood)$"i + - "^(thanks|thank you|thx|ty)$"i + - "^(wait|waiting|processing).*"i + # 通用短语 + - "^(hmm|hm|ah|oh|uh)$"i +``` + +> 💡 **注意**: `i` 后缀表示不区分大小写匹配 + +### 可操作性检测 + +低可操作性信息不存储: +- 纯情感表达 +- 无具体内容的确认 +- 临时状态描述 + diff --git a/.augment/skills/memory-extraction/scripts/validate.sh b/.augment/skills/memory-extraction/scripts/validate.sh new file mode 100755 index 0000000..9443c16 --- /dev/null +++ b/.augment/skills/memory-extraction/scripts/validate.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Memory YAML Validation Script +# Usage: ./validate.sh [file|all] +# Validates _omp/memory/*.yaml files against JSON Schema +# +# Dependencies (optional, for full schema validation): +# npm install -g ajv-cli +# +# Without ajv-cli, only YAML syntax is validated. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Navigate up to project root: scripts -> memory-extraction -> skills -> _omp -> project_root +PROJECT_ROOT="${SCRIPT_DIR}/../../../../" +MEMORY_DIR="${PROJECT_ROOT}/_omp/memory" +SCHEMA_DIR="${MEMORY_DIR}/schema" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +validate_yaml() { + local file="$1" + local schema="$2" + + echo -n " Validating $(basename "$file")... " + + # Check if file exists + if [ ! -f "$file" ]; then + echo -e "${RED}NOT FOUND${NC}" + return 1 + fi + + # Check YAML syntax using ruby (built-in on macOS) + if command -v ruby &> /dev/null; then + if ! ruby -e "require 'yaml'; YAML.load_file('$file')" 2>/dev/null; then + echo -e "${RED}YAML SYNTAX ERROR${NC}" + return 1 + fi + # Fallback: basic structure check + elif ! grep -q "^[a-z_]*:" "$file" 2>/dev/null; then + echo -e "${YELLOW}COULD NOT VALIDATE${NC}" + return 0 + fi + + # Validate against schema (if ajv or similar is available) + if command -v ajv &> /dev/null && [ -f "$schema" ]; then + if ajv validate -s "$schema" -d "$file" --spec=draft7 2>/dev/null; then + echo -e "${GREEN}VALID${NC}" + return 0 + else + echo -e "${YELLOW}SCHEMA MISMATCH${NC}" + return 1 + fi + else + echo -e "${GREEN}YAML OK${NC} (schema validation skipped)" + return 0 + fi +} + +echo "🔍 Memory YAML Validation" +echo "=========================" +echo "" + +ERRORS=0 + +# Validate project.yaml +echo "📁 _omp/memory/" +validate_yaml "${MEMORY_DIR}/project.yaml" "${SCHEMA_DIR}/project.schema.json" || ERRORS=$((ERRORS + 1)) +validate_yaml "${MEMORY_DIR}/decisions.yaml" "${SCHEMA_DIR}/decisions.schema.json" || ERRORS=$((ERRORS + 1)) +validate_yaml "${MEMORY_DIR}/changelog.yaml" "" || ERRORS=$((ERRORS + 1)) + +# Validate session files +echo "" +echo "📁 _omp/memory/sessions/" +for session_file in "${MEMORY_DIR}"/sessions/*.yaml; do + if [ -f "$session_file" ]; then + validate_yaml "$session_file" "${SCHEMA_DIR}/session.schema.json" || ERRORS=$((ERRORS + 1)) + fi +done + +echo "" +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✅ All validations passed!${NC}" + exit 0 +else + echo -e "${RED}❌ $ERRORS validation error(s) found${NC}" + exit 1 +fi + diff --git a/.augment/skills/memory-extraction/templates/decision.yaml.tmpl b/.augment/skills/memory-extraction/templates/decision.yaml.tmpl new file mode 100644 index 0000000..3b69877 --- /dev/null +++ b/.augment/skills/memory-extraction/templates/decision.yaml.tmpl @@ -0,0 +1,32 @@ +# Decision Template +# 追加到 memory/decisions.yaml 的 decisions 数组中 +# +# ============================================ +# 使用说明 (Agent 参考) +# ============================================ +# 变量占位符: +# {{date}} - 日期 (YYYY-MM-DD) +# {{seq}} - 当日序号 (001, 002, 003...) +# {{title}} - 决策标题 +# {{context}} - 决策背景/问题描述 +# {{choice}} - 最终选择 +# {{alt_1}} - 备选方案 (可多个: alt_2, alt_3...) +# {{rationale}} - 选择理由 +# {{impact_1}} - 影响范围 (可多个: impact_2, impact_3...) +# ============================================ + +- id: dec-{{date}}-{{seq}} + date: {{date}} + title: "{{title}}" + context: | + {{context}} + choice: "{{choice}}" + alternatives: + - "{{alt_1}}" + - "{{alt_2}}" + rationale: | + {{rationale}} + impact: + - "{{impact_1}}" + status: active + diff --git a/.augment/skills/memory-extraction/templates/memory-entry.yaml.tmpl b/.augment/skills/memory-extraction/templates/memory-entry.yaml.tmpl new file mode 100644 index 0000000..d9189ca --- /dev/null +++ b/.augment/skills/memory-extraction/templates/memory-entry.yaml.tmpl @@ -0,0 +1,40 @@ +# Memory Entry Template +# 用于项目级记忆存储 +# +# ============================================ +# 使用说明 (Agent 参考) +# ============================================ +# 变量占位符: +# {{key}} - 记忆键 (唯一标识) +# {{value}} - 记忆值 (内容) +# {{scope}} - 作用域 (PROJECT | PERSONAL | UNIVERSAL) +# {{confidence}} - 置信度 (0.0 - 1.0) +# {{source}} - 来源 (explicit | inferred | imported) +# {{temporality}} - 时间性 (permanent | contextual | ephemeral) +# {{created_at}} - 创建时间 (ISO 8601) +# {{updated_at}} - 更新时间 (ISO 8601) +# {{last_accessed}} - 最后访问时间 (ISO 8601) +# {{access_count}} - 访问计数 +# {{decay_score}} - 衰减分数 (0.0 - 1.0) +# {{conflict_status}} - 冲突状态 (none | detected | resolved) +# {{conflict_related_id}} - 关联冲突记忆 ID (可选) +# ============================================ + +- key: "{{key}}" + value: "{{value}}" + metadata: + scope: "{{scope}}" # PROJECT | PERSONAL | UNIVERSAL + confidence: {{confidence}} # 0.0 - 1.0 + source: "{{source}}" # explicit | inferred | imported + temporality: "{{temporality}}" # permanent | contextual | ephemeral + timestamps: + created_at: "{{created_at}}" + updated_at: "{{updated_at}}" + last_accessed: "{{last_accessed}}" + tracking: + access_count: {{access_count}} + decay_score: {{decay_score}} + conflict: + status: "{{conflict_status}}" # none | detected | resolved + related_id: "{{conflict_related_id}}" # ID of conflicting memory (if any) + diff --git a/.augment/skills/memory-extraction/templates/session.yaml.tmpl b/.augment/skills/memory-extraction/templates/session.yaml.tmpl new file mode 100644 index 0000000..a97b9bb --- /dev/null +++ b/.augment/skills/memory-extraction/templates/session.yaml.tmpl @@ -0,0 +1,35 @@ +# memory/sessions/{{date}}.yaml - 会话记录 +# 由 memory-extraction skill 自动维护 +# +# ============================================ +# 使用说明 (Agent 参考) +# ============================================ +# 变量占位符: +# {{date}} - 日期 (YYYY-MM-DD) +# {{timestamp}} - ISO 8601 时间戳 (2026-02-02T14:30:00+08:00) +# {{summary}} - 会话摘要 +# {{action_1}} - 关键操作 (可多个: action_2, action_3...) +# {{decision_id}} - 决策 ID (dec-YYYY-MM-DD-NNN) +# {{decision_title}} - 决策标题 +# {{outcome_1}} - 成果 (可多个: outcome_2, outcome_3...) +# ============================================ + +date: {{date}} + +sessions: + - id: session-001 + summary: "{{summary}}" + key_actions: + - "{{action_1}}" + decisions: + - id: "{{decision_id}}" + title: "{{decision_title}}" + outcomes: + - "{{outcome_1}}" + +# 元数据 +_meta: + last_updated: {{timestamp}} + updated_by: memory-extraction-skill + total_sessions: 1 + diff --git a/.env.example b/.env.example index e850b00..f31679e 100644 --- a/.env.example +++ b/.env.example @@ -34,21 +34,35 @@ EMBEDDING_MODEL=bge-m3 # EMBEDDING_MODEL=text-embedding-3-small # ============================================ -# LLM for Classification (Optional) +# LLM for Classification (记忆分类) # ============================================ +# 只需配置一个 Provider,系统会自动检测 -# DeepSeek (recommended for Chinese) +# DeepSeek (推荐,性价比高,中文能力强) DEEPSEEK_API_KEY= -DEEPSEEK_BASE_URL=https://api.deepseek.com +# DEEPSEEK_BASE_URL=https://api.deepseek.com -# OpenAI (alternative) +# MiniMax (国产大模型,响应快速) +MINIMAX_API_KEY= +# MINIMAX_BASE_URL=https://api.minimax.chat/v1 + +# 智谱 AI / ZhiPu (GLM 系列,学术背景) +ZHIPU_API_KEY= +# ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 + +# 通义千问 / Qwen (阿里云,生态完善) +DASHSCOPE_API_KEY= +# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 + +# OpenAI (原版 GPT,功能最全) OPENAI_API_KEY= -OPENAI_BASE_URL=https://api.openai.com/v1 +# OPENAI_BASE_URL=https://api.openai.com/v1 -# Anthropic Claude (alternative) -ANTHROPIC_API_KEY= +# 自定义模型 (覆盖默认模型) +# LLM_MODEL=deepseek-chat -# Local LLM via Ollama +# Local LLM via Ollama (本地运行,无需 API Key) +# 需要先安装模型: ollama pull qwen2.5:7b # LLM_PROVIDER=ollama # LLM_MODEL=qwen2.5:7b diff --git a/_omp/commands/memory.md b/_omp/commands/memory.md new file mode 100644 index 0000000..680da9d --- /dev/null +++ b/_omp/commands/memory.md @@ -0,0 +1,6 @@ +--- +name: 'memory' +description: '记忆管理 - 统一管理项目级和用户级记忆' +--- + +IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/_omp/workflows/memory/workflow.md, READ its entire contents and follow its directions exactly! diff --git a/_omp/skills/memory-extraction/SKILL.md b/_omp/skills/memory-extraction/SKILL.md new file mode 100644 index 0000000..6cb9ea6 --- /dev/null +++ b/_omp/skills/memory-extraction/SKILL.md @@ -0,0 +1,477 @@ +--- +name: memory-extraction +description: Agent-only workflow for extracting key information from conversations, code changes, and deployments into structured memory files. Automatically updates `_omp/memory/` directory and notifies other agents. Triggered automatically at conversation end or when valuable information is detected. +metadata: + author: Wendy (Workflow Builder) + version: "2.1" + language: zh-CN + audience: agent-only +--- + +# Memory Extraction Skill (Agent-Only) + +## 目的 + +**自动提取对话中的关键信息**,智能路由到正确的存储系统: +- **项目级信息** → `_omp/memory/` 目录 (Git 版本控制) +- **用户级信息** → `openmemory` MCP (跨项目共享) + +> **Agent-Only 原则**: +> - **自动触发**:对话结束时或检测到有价值信息时自动运行 +> - **智能分类**:根据信息类型自动选择存储位置 +> - **零人工干预**:无需用户确认,静默执行 +> - **双系统同步**:同时管理 `_omp/memory/` 和 `openmemory` + +## 核心目录结构 + +``` +_omp/ # OpenMemory Plus 核心目录 +├── commands/ +│ ├── memory.md # 主命令入口 +│ └── memory-actions/ # 7 个子动作 +├── skills/ +│ └── memory-extraction/ # 本 Skill +│ ├── SKILL.md +│ ├── scripts/validate.sh +│ └── templates/*.tmpl +└── memory/ # 项目级记忆 + ├── project.yaml # 项目配置 (SSOT) + ├── decisions.yaml # 技术决策记录 + ├── changelog.yaml # 变更历史 + └── sessions/ # 会话记录 +``` + +## 双层记忆架构 + +| 系统 | 存储位置 | 用途 | +|------|----------|------| +| 项目级 | `_omp/memory/` | 项目配置、技术决策、变更记录 | +| 用户级 | `openmemory` MCP | 用户偏好、技能、跨项目上下文 | + +## 分类规则 + +> 📖 **详细规则**: 参见 `references/classification-rules.md` + +### 快速分类指南 + +| 信号 | Scope | Confidence | 存储位置 | +|------|-------|------------|----------| +| "我喜欢/偏好/习惯" | PERSONAL | EXPLICIT | openmemory | +| "项目使用/配置为" | PROJECT | EXPLICIT | _omp/memory/ | +| "决定/选择/采用" | PROJECT | EXPLICIT | decisions.yaml | +| 用户反复使用某模式 | PERSONAL | INFERRED | openmemory | +| "试试/也许/可能" | EPHEMERAL | UNCERTAIN | 不存储 | + +### 置信度阈值 + +- **存储阈值**: confidence >= 0.4 +- **自动存储**: confidence >= 0.7 +- **需确认**: 0.4 <= confidence < 0.7 + +### 敏感信息过滤 + +**禁止存储**(检测后阻止): + +| 类型 | 检测模式 | +|------|----------| +| API Key | `sk-`, `api_key`, `token=`, `bearer` | +| 密码 | `password`, `secret`, `credential` | +| 私钥 | `-----BEGIN`, `PRIVATE KEY` | +| 个人信息 | 身份证号, 银行卡号, 手机号 | + +### ROT 过滤规则 + +> 📖 **详细规则**: 参见 `references/rot-filter.md` + +| 类型 | 检测方式 | 处理 | +|------|----------|------| +| **Redundant** | 语义相似度 > 0.85 | 合并或跳过 | +| **Obsolete** | 超过 TTL 且无访问 | 标记或删除 | +| **Trivial** | 长度 < 10 或匹配阻止模式 | 丢弃 | + +**快速判断**: +- ❌ "好的" / "OK" / "明白了" → Trivial +- ❌ 与已有记忆相似度 > 0.85 → Redundant +- ❌ 被明确否定的旧信息 → Obsolete + +## 触发条件 + +### 自动触发(推荐) +- 对话结束时自动检查是否有新信息 +- 检测到以下关键事件时立即触发: + - 部署状态变更(成功/失败) + - 新服务配置 + - 重要技术决策 + - 项目里程碑 + +### 手动触发 +- 用户说 `/extract-memory` 或 "保存到记忆" +- 用户说 "记住这个" 或 "更新配置" + +--- + +## 🔄 自动化流程 + +``` +对话/操作 → 信息检测 → 分类 → 提取 → 存储 → 通知 + ↓ ↓ ↓ ↓ ↓ ↓ + 输入源 触发判断 类型识别 结构化 写入YAML 更新Agent +``` + +### Phase 1: 信息检测 + +**检测规则**(按优先级): + +| 优先级 | 信息类型 | 检测信号 | 示例 | +|--------|----------|----------|------| +| P0 | 部署变更 | `deploy`, `vercel`, `wrangler`, URL 变化 | 新域名上线 | +| P0 | 服务配置 | `config`, `secret`, `token`, API 密钥 | 更新 VERCEL_TOKEN | +| P1 | 技术决策 | `决定`, `选择`, `采用`, 架构变更 | 选择 YAML 格式 | +| P1 | 项目里程碑 | `完成`, `上线`, `发布`, 版本号 | v1.0 发布 | +| P2 | 路径变更 | 目录创建/移动, 文件重组 | 创建 _omp/memory/ | +| P2 | 工具配置 | CLI 安装, 依赖更新 | 安装 resumes-cli | + +### Phase 2: 信息分类与路由 + +根据检测结果,**智能路由**到正确的存储系统: + +#### 项目级 → `_omp/memory/` + +| 分类 | 目标文件 | 内容类型 | +|------|----------|----------| +| `project` | `_omp/memory/project.yaml` | 项目常量、部署信息、路径 | +| `decisions` | `_omp/memory/decisions.yaml` | 重要技术决策记录 | +| `changelog` | `_omp/memory/changelog.yaml` | 变更历史 | + +#### 用户级 → `openmemory` + +| 分类 | MCP 工具 | 内容类型 | +|------|----------|----------| +| `preference` | `add_memories_openmemory` | 用户偏好、习惯 | +| `skill` | `add_memories_openmemory` | 用户技能、经验 | +| `context` | `add_memories_openmemory` | 对话上下文、历史 | + +#### openmemory MCP 调用 + +**写入用户记忆**: +``` +Tool: add_memories_openmemory +Parameter: text = "用户偏好: {提取的偏好信息}" +``` + +**搜索用户记忆**: +``` +Tool: search_memory_openmemory +Parameter: query = "{搜索关键词}" +``` + +### Phase 3: 结构化提取 + +**存储格式** (参见 `templates/memory-entry.yaml.tmpl`): + +```yaml +# 项目记忆示例 +- key: tech-stack + value: "TypeScript + React + Vite" + metadata: + scope: PROJECT + confidence: 1.0 + source: explicit + temporality: permanent + timestamps: + created_at: "2026-02-03T10:00:00Z" + updated_at: "2026-02-03T10:00:00Z" + last_accessed: "2026-02-03T10:00:00Z" + tracking: + access_count: 1 + decay_score: 1.0 +``` + +**用户记忆** (通过 MCP): + +``` +Tool: add_memories_openmemory +Parameter: text = "[SCOPE:PERSONAL][CONF:0.9] 用户偏好: 2 空格缩进,函数式编程风格" +``` + +> 💡 在 text 中嵌入元数据标签,便于后续检索和过滤 + +**其他提取模板**: + +```yaml +# 部署变更 +deployment: + {service_name}: + url: {new_url} + status: {active|pending|failed} + updated_at: {timestamp} + updated_by: {agent|user} + +# 技术决策 +decisions: + - id: {uuid} + date: {date} + title: {decision_title} + context: {why_this_decision} + choice: {what_was_chosen} + alternatives: [{other_options}] + impact: {affected_areas} + +# 路径变更 +paths: + {path_key}: {new_path} +``` + +### Phase 4: 存储 + +**写入规则**: +1. 读取现有文件(如存在) +2. 合并新信息(不覆盖,追加/更新) +3. 保留注释和格式 +4. 添加 `last_updated` 时间戳 + +**示例写入**: +```yaml +# _omp/memory/project.yaml +deployment: + vercel: + url: https://web-zeta-six-79.vercel.app + status: active + updated_at: 2026-02-02T10:30:00Z # ← 自动更新 +``` + +### Phase 5: 通知其他 Agent + +更新完成后,在以下位置添加通知标记: + +```yaml +# _omp/memory/project.yaml (底部) +_meta: + last_updated: 2026-02-02T10:30:00Z + updated_by: memory-extraction-skill + changes: + - "deployment.vercel.status: pending → active" +``` + +--- + +## 输出格式 + +### `_omp/memory/project.yaml` (主配置) +见现有文件结构,本 Skill 负责自动更新。 + +### `_omp/memory/sessions/{date}.yaml` (会话记录) +```yaml +date: 2026-02-02 +sessions: + - id: session-001 + start: "10:00" + end: "11:30" + summary: "部署 Vercel 并配置 Cloudflare Worker" + key_actions: + - "更新 VERCEL_TOKEN GitHub Secret" + - "创建 _omp/memory/ 目录结构" + decisions: + - "采用 YAML 格式作为 memory 存储格式" +``` + +### `_omp/memory/decisions.yaml` (决策记录) +```yaml +decisions: + - id: dec-2026-02-02-001 + date: 2026-02-02 + title: "Memory 存储格式选择" + context: "需要跨 Agent 共享配置,支持多 IDE" + choice: "YAML 格式" + alternatives: ["JSON", "Markdown", "TOML"] + rationale: "人类可读 + 机器可解析 + 支持注释" +``` + +--- + +## 与其他 Agent 的集成 + +### 读取方(其他 Agent) + +其他 Agent 应在启动时读取 `_omp/memory/project.yaml`: + +```markdown + +> 📌 **配置中心**: 项目常量统一存储在 `_omp/memory/project.yaml` +``` + +### 写入方(本 Skill) + +本 Skill 是 `_omp/memory/` 的唯一写入者,确保: +- 格式一致性 +- 无冲突写入 +- 变更可追溯 + +--- + +## 错误处理 + +### 写入失败场景 + +| 场景 | 处理策略 | +|------|----------| +| **YAML 语法错误** | 验证失败,不写入,通知用户修复 | +| **文件不存在** | 从模板创建新文件 | +| **权限拒绝** | 记录错误,跳过此次更新 | +| **Schema 验证失败** | 回滚到上一版本,输出差异 | + +### 回退机制 + +1. **写入前备份**: 修改前复制到 `_omp/memory/.backup/` +2. **原子写入**: 写入临时文件,验证后重命名 +3. **错误日志**: 记录到 `_omp/memory/sessions/{date}.yaml` 的 `errors` 字段 + +### 验证脚本 + +```bash +# 验证所有 YAML 文件 +_omp/skills/memory-extraction/scripts/validate.sh +``` + +--- + +## 触发条件详解 + +### 自动触发时机 + +| 触发信号 | 说明 | 示例 | +|----------|------|------| +| 用户结束对话 | 用户说 "bye", "结束", "exit", "谢谢" | `用户: 好的,先这样` | +| 部署完成 | 检测到 deploy/vercel/wrangler 输出 | `vercel --prod` 成功 | +| 配置变更 | 修改了 env/secret/config 文件 | 更新 `.env` | +| 创建新目录 | 创建了项目级目录 | `mkdir _omp/memory/` | +| 重要决策 | 对话中明确了技术选型 | `决定使用 YAML 格式` | + +### 不触发的场景 + +- 普通代码编辑(非配置文件) +- 读取操作(无写入) +- 临时文件操作 + +--- + +## 辅助资源 + +### 模板文件 + +- `templates/memory-entry.yaml.tmpl` - 记忆条目模板 (含元数据和冲突追踪) + +> 💡 **扩展模板**: 如需 session 或 decision 专用模板,可基于 memory-entry.yaml.tmpl 创建 + +### 参考文档 + +- `references/classification-rules.md` - 多维度分类规则 +- `references/rot-filter.md` - ROT 过滤规则 +- `references/decay-model.md` - Ebbinghaus 衰减模型 +- `references/health-score.md` - 健康度计算公式 + +### Schema 验证 (可选) + +> ⚠️ **待实现**: Schema 验证文件尚未创建,可根据需要添加: +> - `_omp/memory/schema/project.schema.json` +> - `_omp/memory/schema/decisions.schema.json` + +--- + +## 执行检查清单 + +Agent 在对话结束前执行: + +- [ ] 是否有部署状态变更?→ 更新 `deployment` +- [ ] 是否有新路径/目录?→ 更新 `paths` +- [ ] 是否有重要决策?→ 记录到 `decisions.yaml` +- [ ] 是否有配置变更?→ 更新对应字段 +- [ ] 是否有用户偏好/技能?→ 存入 `openmemory` +- [ ] 更新 `_meta.last_updated`(使用精确 ISO 8601 时间戳) +- [ ] 运行 `validate.sh` 验证格式 + +--- + +## 冲突检测 + +Agent 在查询记忆时应检测两系统数据一致性: + +### 检测时机 + +- 用户询问配置/部署信息时 +- 执行 `/memory sync` 命令时 +- 发现两系统返回不同结果时 + +### 检测逻辑 + +``` +查询结果 + ↓ +提取关键实体 (URL, 配置值, 技术选型) + ↓ +比对 _omp/memory/ vs openmemory + ↓ +发现差异 → 提示用户确认 + ↓ +用户选择 → 更新/删除过时记录 +``` + +### 冲突处理 + +| 场景 | 处理方式 | +|------|----------| +| URL 不一致 | 提示用户,优先 `_omp/memory/` | +| 技术选型冲突 | 展示两边,请求决策 | +| 时间戳可判断 | 自动保留较新版本 | + +--- + +## 对话启动时自动加载 + +Agent 在对话开始时应**并行查询**两个系统: + +``` +对话开始 + ↓ +┌─────────────────┐ ┌─────────────────┐ +│ _omp/memory/ │ │ openmemory │ +│ (读取 YAML) │ │ (search_memory) │ +└────────┬────────┘ └────────┬────────┘ + │ │ + └───────────┬───────────┘ + ↓ + 上下文融合 +``` + +**加载步骤**: +1. 读取 `_omp/memory/project.yaml` 获取项目配置 +2. 调用 `search_memory_openmemory` 查询相关用户记忆 +3. 融合两边信息作为对话上下文 + +## 降级策略 + +**当 openmemory MCP 不可用时**: + +1. 检测 MCP 连接状态(调用失败或超时) +2. 降级到仅 `_omp/memory/` 存储 +3. 用户级信息临时存入 `_omp/memory/user-context.yaml` +4. 服务恢复后提示用户同步 + +```yaml +# _omp/memory/user-context.yaml (降级时使用) +_degraded: true +_reason: "openmemory MCP unavailable" +pending_memories: + - text: "用户偏好: 使用中文" + created_at: 2026-02-02T10:30:00Z +``` + +## 版本历史 + +| 版本 | 变更 | +|------|------| +| v2.1 | 目录重构:_omp/ 统一目录,移除 rules/,分类规则内嵌 | +| v2.0 | 双层记忆架构:整合 openmemory MCP,智能分类路由 | +| v1.1 | 添加错误处理、Schema 验证、模板文件、触发条件详解 | +| v1.0 | 初始版本:自动提取、YAML 存储、多 Agent 通知 | + diff --git a/_omp/skills/memory-extraction/references/classification-rules.md b/_omp/skills/memory-extraction/references/classification-rules.md new file mode 100644 index 0000000..231b39e --- /dev/null +++ b/_omp/skills/memory-extraction/references/classification-rules.md @@ -0,0 +1,53 @@ +# Memory Classification Rules + +## 多维度分类体系 + +### Dimension 1: Scope (范围) + +| Scope | 定义 | 存储位置 | 示例 | +|-------|------|----------|------| +| PERSONAL | 与用户个人相关 | openmemory | "我喜欢用 Vim" | +| PROJECT | 与当前项目相关 | _omp/memory/ | "项目使用 React" | +| UNIVERSAL | 跨项目通用知识 | openmemory | "TypeScript 比 JS 更安全" | +| EPHEMERAL | 仅当前会话有效 | 不存储 | "先试试这个方案" | + +### Dimension 2: Confidence (置信度) + +| Level | 范围 | 定义 | 处理方式 | +|------|------|------|----------| +| EXPLICIT | >= 0.9 | 用户明确陈述 | 直接存储 | +| INFERRED | 0.7 - 0.9 | 从行为推断 | 存储但标记 | +| UNCERTAIN | 0.4 - 0.7 | 需要确认 | 询问用户后存储 | +| NOISE | < 0.4 | 噪音信息 | 丢弃 | + +> ⚠️ **阈值说明**: +> - **存储阈值**: confidence >= 0.4 (UNCERTAIN 及以上) +> - **自动存储**: confidence >= 0.7 (INFERRED 及以上) +> - **需确认**: 0.4 <= confidence < 0.7 (UNCERTAIN 区间) + +### Dimension 3: Temporality (时效性) + +| Type | TTL | 示例 | +|------|-----|------| +| PERMANENT | 无限 | 用户偏好、项目架构 | +| CONTEXTUAL | 30d | 当前迭代目标 | +| EPHEMERAL | 会话 | 临时讨论 | + +## 分类决策树 + +``` +信息输入 + ↓ +是否敏感信息? → YES → 丢弃 + ↓ NO +是否 ROT? → YES → 丢弃 + ↓ NO +置信度 < 0.4? → YES → 丢弃 (NOISE) + ↓ NO +Scope = EPHEMERAL? → YES → 不存储 + ↓ NO +Scope = PERSONAL/UNIVERSAL? → YES → openmemory + ↓ NO +Scope = PROJECT → _omp/memory/ +``` + diff --git a/_omp/skills/memory-extraction/references/decay-model.md b/_omp/skills/memory-extraction/references/decay-model.md new file mode 100644 index 0000000..b09715c --- /dev/null +++ b/_omp/skills/memory-extraction/references/decay-model.md @@ -0,0 +1,63 @@ +# Memory Decay Model + +## Ebbinghaus 变体公式 + +``` +base_retention = e^(-t/S) +Retention(t) = base_retention + importance_boost × (1 - base_retention) + +其中: +- t = 距离上次访问的天数 +- S = 强度因子 (初始 30,每次访问 +10) +- importance_boost = 重要性加成 (0-0.5) +- 结果保证在 [0, 1] 范围内 +``` + +> 💡 **公式解释**: importance_boost 将 base_retention 向 1.0 方向提升, +> 但永远不会超过 1.0。例如 base=0.5, boost=0.5 → 0.5 + 0.5×0.5 = 0.75 + +## 强度因子 (S) 计算 + +```python +S = base_strength + (access_count * 10) + (explicit_mark * 50) + +# base_strength = 30 (默认) +# access_count = 访问次数 +# explicit_mark = 用户标记为重要 (0 或 1) +``` + +## 重要性加成 + +| 条件 | Boost | +|------|-------| +| 用户明确标记重要 | +0.5 | +| 高置信度 (>0.9) | +0.2 | +| 频繁访问 (>5次) | +0.1 | +| 项目核心配置 | +0.3 | + +> ⚠️ **Cap 限制**: `importance_boost = min(累加值, 0.5)`,最终值不超过 0.5 + +## 衰减状态分类 + +| Status | Retention | 建议操作 | +|--------|-----------|----------| +| 🟢 Active | >= 0.7 | 保持 | +| 🟡 Aging | 0.3-0.7 | 关注 | +| 🔴 Stale | 0.1-0.3 | 考虑清理 | +| ⚫ Cleanup | < 0.1 | 自动清理候选 | + +## 遗忘策略 + +### 自动遗忘 (无需确认) +- Retention < 0.1 且 90天未访问 +- 被明确标记为过时 + +### 确认遗忘 (需用户确认) +- Retention 0.1-0.3 且 60天未访问 +- 与新信息冲突的旧记忆 + +### 永不遗忘 +- 用户明确标记为重要 +- 安全/合规相关信息 +- 项目核心配置 + diff --git a/_omp/skills/memory-extraction/references/health-score.md b/_omp/skills/memory-extraction/references/health-score.md new file mode 100644 index 0000000..5a98782 --- /dev/null +++ b/_omp/skills/memory-extraction/references/health-score.md @@ -0,0 +1,67 @@ +# Memory Health Score + +## 健康度计算公式 + +```python +def calculate_health_score(memories): + """ + 计算记忆系统整体健康度 (0-100) + + Args: + memories: 所有记忆列表 + + Returns: + int: 健康度分数 (0-100) + """ + total = len(memories) + if total == 0: + return 100 # 无记忆时返回满分 + + # 1. 活跃率 (30% 权重) + active_count = count_by_status(memories, 'active') + active_ratio = active_count / total + + # 2. ROT 比例 (20% 权重) - 越低越好 + rot_count = count_by_status(memories, 'stale') + count_by_status(memories, 'cleanup') + rot_ratio = rot_count / total + + # 3. 平均置信度 (30% 权重) + avg_confidence = mean([m.confidence for m in memories]) + + # 4. 冲突率 (20% 权重) - 越低越好 + conflict_count = count_conflicts(memories) + conflict_ratio = conflict_count / total + + # 加权计算 + score = ( + active_ratio * 0.3 + + (1 - rot_ratio) * 0.2 + + avg_confidence * 0.3 + + (1 - conflict_ratio) * 0.2 + ) + return int(score * 100) +``` + +## 健康度等级 + +| 分数 | 等级 | Emoji | 说明 | +|------|------|-------|------| +| >= 80 | Excellent | ✅ | 系统健康 | +| 60-79 | Good | ⚠️ | 需要关注 | +| < 60 | Needs Attention | ❌ | 需要处理 | + +## 各指标健康阈值 + +| 指标 | 健康阈值 | 说明 | +|------|----------|------| +| 活跃率 | > 60% | Active 状态记忆占比 | +| ROT 比例 | < 20% | Stale + Cleanup 占比 | +| 平均置信度 | > 0.7 | 所有记忆的平均置信度 | +| 冲突率 | < 5% | 存在冲突的记忆占比 | + +## 使用方式 + +此公式被以下 workflow steps 引用: +- `workflows/memory/steps/status.md` - 状态显示 +- `workflows/memory/steps/metrics.md` - 质量指标面板 + diff --git a/_omp/skills/memory-extraction/references/rot-filter.md b/_omp/skills/memory-extraction/references/rot-filter.md new file mode 100644 index 0000000..c296a45 --- /dev/null +++ b/_omp/skills/memory-extraction/references/rot-filter.md @@ -0,0 +1,78 @@ +# ROT Filter Rules + +## Redundant (冗余) 检测 + +### 语义相似度检测 + +使用 Qdrant 向量搜索检测相似记忆: + +```yaml +redundancy: + similarity_threshold: 0.85 + actions: + - score >= 0.95: skip (完全重复) + - score >= 0.85: merge (合并到已有) + - score < 0.85: store (正常存储) +``` + +### 合并策略 + +当检测到相似记忆时: +1. 比较信息完整度 +2. 保留更完整的版本 +3. 合并补充信息 +4. 更新时间戳 + +## Obsolete (过时) 检测 + +### 时间衰减 + +```yaml +obsolescence: + default_ttl: 90d + check_triggers: + - on_access + - daily_scan + actions: + - age > 180d && access_count == 0: auto_delete + - age > 90d && access_count < 3: mark_stale + - age > 30d: calculate_decay +``` + +### 显式过时 + +检测信号: +- "不再使用 X" +- "改用 Y 替代 X" +- "X 已废弃" + +## Trivial (琐碎) 检测 + +### 信息密度阈值 + +```yaml +triviality: + min_length: 10 # 字符 + min_info_density: 0.3 + blocked_patterns: + # 中文确认词 + - "^(好的|OK|明白|收到|了解|知道了)$" + - "^(稍等|正在处理|请稍候).*" + - "^(是的|对|嗯|行|可以)$" + # 英文确认词 + - "^(ok|okay|sure|yes|yep|yeah|got it|roger|understood)$"i + - "^(thanks|thank you|thx|ty)$"i + - "^(wait|waiting|processing).*"i + # 通用短语 + - "^(hmm|hm|ah|oh|uh)$"i +``` + +> 💡 **注意**: `i` 后缀表示不区分大小写匹配 + +### 可操作性检测 + +低可操作性信息不存储: +- 纯情感表达 +- 无具体内容的确认 +- 临时状态描述 + diff --git a/_omp/skills/memory-extraction/scripts/validate.sh b/_omp/skills/memory-extraction/scripts/validate.sh new file mode 100755 index 0000000..9443c16 --- /dev/null +++ b/_omp/skills/memory-extraction/scripts/validate.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# Memory YAML Validation Script +# Usage: ./validate.sh [file|all] +# Validates _omp/memory/*.yaml files against JSON Schema +# +# Dependencies (optional, for full schema validation): +# npm install -g ajv-cli +# +# Without ajv-cli, only YAML syntax is validated. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Navigate up to project root: scripts -> memory-extraction -> skills -> _omp -> project_root +PROJECT_ROOT="${SCRIPT_DIR}/../../../../" +MEMORY_DIR="${PROJECT_ROOT}/_omp/memory" +SCHEMA_DIR="${MEMORY_DIR}/schema" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +validate_yaml() { + local file="$1" + local schema="$2" + + echo -n " Validating $(basename "$file")... " + + # Check if file exists + if [ ! -f "$file" ]; then + echo -e "${RED}NOT FOUND${NC}" + return 1 + fi + + # Check YAML syntax using ruby (built-in on macOS) + if command -v ruby &> /dev/null; then + if ! ruby -e "require 'yaml'; YAML.load_file('$file')" 2>/dev/null; then + echo -e "${RED}YAML SYNTAX ERROR${NC}" + return 1 + fi + # Fallback: basic structure check + elif ! grep -q "^[a-z_]*:" "$file" 2>/dev/null; then + echo -e "${YELLOW}COULD NOT VALIDATE${NC}" + return 0 + fi + + # Validate against schema (if ajv or similar is available) + if command -v ajv &> /dev/null && [ -f "$schema" ]; then + if ajv validate -s "$schema" -d "$file" --spec=draft7 2>/dev/null; then + echo -e "${GREEN}VALID${NC}" + return 0 + else + echo -e "${YELLOW}SCHEMA MISMATCH${NC}" + return 1 + fi + else + echo -e "${GREEN}YAML OK${NC} (schema validation skipped)" + return 0 + fi +} + +echo "🔍 Memory YAML Validation" +echo "=========================" +echo "" + +ERRORS=0 + +# Validate project.yaml +echo "📁 _omp/memory/" +validate_yaml "${MEMORY_DIR}/project.yaml" "${SCHEMA_DIR}/project.schema.json" || ERRORS=$((ERRORS + 1)) +validate_yaml "${MEMORY_DIR}/decisions.yaml" "${SCHEMA_DIR}/decisions.schema.json" || ERRORS=$((ERRORS + 1)) +validate_yaml "${MEMORY_DIR}/changelog.yaml" "" || ERRORS=$((ERRORS + 1)) + +# Validate session files +echo "" +echo "📁 _omp/memory/sessions/" +for session_file in "${MEMORY_DIR}"/sessions/*.yaml; do + if [ -f "$session_file" ]; then + validate_yaml "$session_file" "${SCHEMA_DIR}/session.schema.json" || ERRORS=$((ERRORS + 1)) + fi +done + +echo "" +if [ $ERRORS -eq 0 ]; then + echo -e "${GREEN}✅ All validations passed!${NC}" + exit 0 +else + echo -e "${RED}❌ $ERRORS validation error(s) found${NC}" + exit 1 +fi + diff --git a/_omp/skills/memory-extraction/templates/decision.yaml.tmpl b/_omp/skills/memory-extraction/templates/decision.yaml.tmpl new file mode 100644 index 0000000..3b69877 --- /dev/null +++ b/_omp/skills/memory-extraction/templates/decision.yaml.tmpl @@ -0,0 +1,32 @@ +# Decision Template +# 追加到 memory/decisions.yaml 的 decisions 数组中 +# +# ============================================ +# 使用说明 (Agent 参考) +# ============================================ +# 变量占位符: +# {{date}} - 日期 (YYYY-MM-DD) +# {{seq}} - 当日序号 (001, 002, 003...) +# {{title}} - 决策标题 +# {{context}} - 决策背景/问题描述 +# {{choice}} - 最终选择 +# {{alt_1}} - 备选方案 (可多个: alt_2, alt_3...) +# {{rationale}} - 选择理由 +# {{impact_1}} - 影响范围 (可多个: impact_2, impact_3...) +# ============================================ + +- id: dec-{{date}}-{{seq}} + date: {{date}} + title: "{{title}}" + context: | + {{context}} + choice: "{{choice}}" + alternatives: + - "{{alt_1}}" + - "{{alt_2}}" + rationale: | + {{rationale}} + impact: + - "{{impact_1}}" + status: active + diff --git a/_omp/skills/memory-extraction/templates/memory-entry.yaml.tmpl b/_omp/skills/memory-extraction/templates/memory-entry.yaml.tmpl new file mode 100644 index 0000000..d9189ca --- /dev/null +++ b/_omp/skills/memory-extraction/templates/memory-entry.yaml.tmpl @@ -0,0 +1,40 @@ +# Memory Entry Template +# 用于项目级记忆存储 +# +# ============================================ +# 使用说明 (Agent 参考) +# ============================================ +# 变量占位符: +# {{key}} - 记忆键 (唯一标识) +# {{value}} - 记忆值 (内容) +# {{scope}} - 作用域 (PROJECT | PERSONAL | UNIVERSAL) +# {{confidence}} - 置信度 (0.0 - 1.0) +# {{source}} - 来源 (explicit | inferred | imported) +# {{temporality}} - 时间性 (permanent | contextual | ephemeral) +# {{created_at}} - 创建时间 (ISO 8601) +# {{updated_at}} - 更新时间 (ISO 8601) +# {{last_accessed}} - 最后访问时间 (ISO 8601) +# {{access_count}} - 访问计数 +# {{decay_score}} - 衰减分数 (0.0 - 1.0) +# {{conflict_status}} - 冲突状态 (none | detected | resolved) +# {{conflict_related_id}} - 关联冲突记忆 ID (可选) +# ============================================ + +- key: "{{key}}" + value: "{{value}}" + metadata: + scope: "{{scope}}" # PROJECT | PERSONAL | UNIVERSAL + confidence: {{confidence}} # 0.0 - 1.0 + source: "{{source}}" # explicit | inferred | imported + temporality: "{{temporality}}" # permanent | contextual | ephemeral + timestamps: + created_at: "{{created_at}}" + updated_at: "{{updated_at}}" + last_accessed: "{{last_accessed}}" + tracking: + access_count: {{access_count}} + decay_score: {{decay_score}} + conflict: + status: "{{conflict_status}}" # none | detected | resolved + related_id: "{{conflict_related_id}}" # ID of conflicting memory (if any) + diff --git a/_omp/skills/memory-extraction/templates/session.yaml.tmpl b/_omp/skills/memory-extraction/templates/session.yaml.tmpl new file mode 100644 index 0000000..a97b9bb --- /dev/null +++ b/_omp/skills/memory-extraction/templates/session.yaml.tmpl @@ -0,0 +1,35 @@ +# memory/sessions/{{date}}.yaml - 会话记录 +# 由 memory-extraction skill 自动维护 +# +# ============================================ +# 使用说明 (Agent 参考) +# ============================================ +# 变量占位符: +# {{date}} - 日期 (YYYY-MM-DD) +# {{timestamp}} - ISO 8601 时间戳 (2026-02-02T14:30:00+08:00) +# {{summary}} - 会话摘要 +# {{action_1}} - 关键操作 (可多个: action_2, action_3...) +# {{decision_id}} - 决策 ID (dec-YYYY-MM-DD-NNN) +# {{decision_title}} - 决策标题 +# {{outcome_1}} - 成果 (可多个: outcome_2, outcome_3...) +# ============================================ + +date: {{date}} + +sessions: + - id: session-001 + summary: "{{summary}}" + key_actions: + - "{{action_1}}" + decisions: + - id: "{{decision_id}}" + title: "{{decision_title}}" + outcomes: + - "{{outcome_1}}" + +# 元数据 +_meta: + last_updated: {{timestamp}} + updated_by: memory-extraction-skill + total_sessions: 1 + diff --git a/_omp/workflows/memory/steps/clean.md b/_omp/workflows/memory/steps/clean.md new file mode 100644 index 0000000..d747aa4 --- /dev/null +++ b/_omp/workflows/memory/steps/clean.md @@ -0,0 +1,92 @@ +--- +name: clean +description: 清理过期、冗余或琐碎的记忆 (ROT) +--- + +# Step: 清理记忆 + +## EXECUTION RULES + +- ✅ Analyze all memories for ROT +- ✅ Show candidates before deletion +- ✅ Require user confirmation + +--- + +## EXECUTION + +### 1. Get All Memories + +Call `list_memories_openmemory` to get all user memories. + +### 2. Analyze ROT + +Identify cleanup candidates: + +| Type | Definition | Detection | +|------|------------|-----------| +| **Redundant** (冗余) | 重复或相似 | 语义相似度 > 0.9 | +| **Outdated** (过时) | 超过 90 天 | 创建时间 > 90 days | +| **Trivial** (琐碎) | 无实际价值 | 内容 < 10 chars or too generic | + +### 3. Display Candidates + +``` +🧹 清理候选记忆 + +以下记忆可能需要清理: + +⚠️ Outdated (过时): +{foreach outdated} +{n}. "{content}" + 创建: {days_ago} 天前 +{/foreach} + +⚠️ Redundant (冗余): +{foreach redundant} +{n}. "{content}" (与 "{similar_to}" 相似) +{/foreach} + +⚠️ Trivial (琐碎): +{foreach trivial} +{n}. "{content}" +{/foreach} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +操作选项: +- "全部删除" - 删除所有候选 +- "删除 1,2,3" - 删除指定项 +- "取消" - 不删除 +``` + +### 4. Execute Deletion + +Based on user selection: +- Get memory IDs to delete +- Call `delete_memories_openmemory` with IDs +- Display deletion result + +### 5. Display Result + +``` +🗑️ 清理完成 + +已删除 {n} 条记忆 +释放空间: {estimated_size} +``` + +--- + +## SAFETY + +- ⚠️ NEVER delete without user confirmation +- ⚠️ Show content preview before deletion +- ⚠️ Support selective deletion + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" diff --git a/_omp/workflows/memory/steps/consolidate.md b/_omp/workflows/memory/steps/consolidate.md new file mode 100644 index 0000000..b1d02a1 --- /dev/null +++ b/_omp/workflows/memory/steps/consolidate.md @@ -0,0 +1,95 @@ +--- +name: consolidate +description: 整合碎片化记忆,合并相似条目 +--- + +# Step: 记忆整合 + +## EXECUTION RULES + +- ✅ Find similar memories using semantic search +- ✅ Group by topic/entity +- ✅ Merge with user confirmation + +--- + +## EXECUTION + +### 1. Scan All Memories + +1. Call `list_memories_openmemory` for user memories +2. Read `_omp/memory/*.yaml` for project memories +3. Build memory index + +### 2. Semantic Clustering + +Group memories by semantic similarity: + +``` +For each memory M: + 1. Search for similar memories (threshold: 0.7) + 2. If found, add to cluster + 3. Else, create new cluster +``` + +### 3. Display Clusters + +``` +🔗 记忆整合分析 + +发现 {n} 个可整合的记忆组: + +📦 组 1: {topic} +├── "{memory_1}" [项目级] +├── "{memory_2}" [用户级] +└── "{memory_3}" [用户级] +建议: 合并为 "{suggested_merge}" + +📦 组 2: {topic} +├── "{memory_1}" +└── "{memory_2}" +建议: 保留 "{preferred}" (更完整) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +操作: +- "合并 1" - 执行组 1 的合并 +- "全部合并" - 执行所有建议 +- "跳过" - 不执行 +``` + +### 4. Execute Merge + +For each approved merge: + +``` +1. Backup: Save original memories to temp storage +2. Create: Create consolidated memory with merged content +3. Verify: Confirm new memory was created successfully +4. Delete: Delete original fragments one by one +5. Update: Update any references to old IDs + +Error Handling: +- IF step 2 fails → Abort, no changes made +- IF step 4 fails → Rollback: delete new memory, restore from backup +- IF step 5 fails → Log warning, continue (non-critical) +``` + +### 5. Display Result + +``` +✅ 整合完成 + +合并了 {n} 组记忆 +├── 删除碎片: {deleted} 条 +├── 创建整合: {created} 条 +└── 节省空间: ~{percent}% +``` + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" + diff --git a/_omp/workflows/memory/steps/decay.md b/_omp/workflows/memory/steps/decay.md new file mode 100644 index 0000000..36fa419 --- /dev/null +++ b/_omp/workflows/memory/steps/decay.md @@ -0,0 +1,135 @@ +--- +name: decay +description: 使用 Ebbinghaus 衰减模型分析记忆的时间衰减状态 +--- + +# Step: 衰减分析 (Ebbinghaus 模型) + +## 参考文档 + +详见: `_omp/skills/memory-extraction/references/decay-model.md` + +## EXECUTION RULES + +- ✅ Calculate retention scores using Ebbinghaus formula +- ✅ Compute strength factor (S) based on access history +- ✅ Apply importance boosts +- ✅ Classify memories by decay status +- ✅ Provide actionable recommendations + +--- + +## EXECUTION + +### 1. Get All Memories + +Call `list_memories_openmemory` to get all user memories with: +- Creation timestamp +- Last access timestamp +- Access count +- Importance flags +- Confidence scores + +### 2. Calculate Strength Factor (S) + +For each memory, calculate: +``` +S = 30 + (access_count × 10) + (explicit_mark × 50) + +Where: +- 30 = base_strength (default) +- access_count = number of times accessed +- explicit_mark = 1 if user marked important, 0 otherwise +``` + +### 3. Calculate Importance Boost + +Determine boost based on: +``` +importance_boost = 0 +if user_marked_important: importance_boost += 0.5 +if confidence > 0.9: importance_boost += 0.2 +if access_count > 5: importance_boost += 0.1 +if is_core_config: importance_boost += 0.3 + +# Apply cap to ensure boost never exceeds 0.5 +importance_boost = min(importance_boost, 0.5) +``` + +### 4. Calculate Retention Score + +For each memory: +``` +days_since_access = today - last_access_date +base_retention = e^(-days_since_access/S) +Retention(t) = base_retention + importance_boost × (1 - base_retention) + +# Result is guaranteed to be in [0, 1] by formula design +``` + +### 5. Classify by Status + +| Status | Retention | Meaning | Action | +|--------|-----------|---------|--------| +| 🟢 Active | >= 0.7 | 活跃,保持 | 保持 | +| 🟡 Aging | 0.3-0.7 | 老化,需关注 | 关注 | +| 🔴 Stale | 0.1-0.3 | 陈旧,考虑清理 | 清理 | +| ⚫ Cleanup | < 0.1 | 待清理 | 自动清理 | + +### 6. Display Analysis + +``` +⏰ 记忆衰减分析 (Ebbinghaus 模型) + +📊 总览: {total} 条记忆 + +🟢 Active ({n}条) {progress_bar} {percent}% +{foreach top_5} +├── {content} +│ └─ Retention: {retention:.2%} | S={strength} | 访问: {access_count}次 +{/foreach} +└── ... (输入 "展开 active" 查看全部) + +🟡 Aging ({n}条) {progress_bar} {percent}% +{foreach all} +├── {content} +│ └─ Retention: {retention:.2%} | S={strength} | 最后访问: {days_ago}天前 +{/foreach} + +🔴 Stale ({n}条) {progress_bar} {percent}% +{foreach all} +├── {content} +│ └─ Retention: {retention:.2%} | S={strength} | 最后访问: {days_ago}天前 +{/foreach} + +⚫ Cleanup ({n}条) {progress_bar} {percent}% +{foreach all} +└── {content} + └─ Retention: {retention:.2%} | 建议自动清理 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +💡 建议: +{recommendations} + +📋 遗忘策略: +- 自动遗忘: Retention < 0.1 且 90天未访问 +- 确认遗忘: Retention 0.1-0.3 且 60天未访问 +- 永不遗忘: 用户标记重要 / 安全合规 / 核心配置 +``` + +### 7. Follow-up Actions + +- `"清理 stale"` → Delete all Stale memories (with confirmation) +- `"自动清理"` → Auto-delete Cleanup status memories +- `"刷新 N"` → Update memory (re-add to refresh S factor) +- `"展开 {status}"` → Show all in category +- `"标记重要 N"` → Mark memory as important (increase boost) +- `"详情 N"` → Show detailed decay calculation + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" diff --git a/_omp/workflows/memory/steps/graph.md b/_omp/workflows/memory/steps/graph.md new file mode 100644 index 0000000..1aca266 --- /dev/null +++ b/_omp/workflows/memory/steps/graph.md @@ -0,0 +1,88 @@ +--- +name: graph +description: 查看记忆中的实体关系图谱 +--- + +# Step: 知识图谱 + +## EXECUTION RULES + +- ✅ Extract entities from memories +- ✅ Identify relationships +- ✅ Visualize in ASCII or Mermaid + +--- + +## EXECUTION + +### 1. Read Graph Data + +Check for existing `_omp/memory/graph.yaml`: +- If exists, load entities and relations +- If not, initialize empty graph + +### 2. Extract Entities + +From project and user memories, extract: + +| Entity Type | Examples | +|-------------|----------| +| project | Project name | +| technology | TypeScript, React, Python | +| service | API, CLI, Web | +| database | Qdrant, PostgreSQL | +| config | tsconfig.json, package.json | +| preference | User preferences | + +### 3. Identify Relations + +| Relation | Meaning | +|----------|---------| +| uses | A uses B | +| depends_on | A depends on B | +| configured_by | A configured by B | +| prefers | User prefers A | + +### 4. Display Graph + +``` +🔗 知识图谱 + +项目: {project_name} + +实体关系: +┌─────────────────────────────────────────┐ +│ │ +│ [{entity}] ──{relation}──> [{entity}] │ +│ │ │ +│ └──{relation}──> [{entity}] │ +│ │ +│ [User] ──prefers──> [{preference}] │ +│ │ +└─────────────────────────────────────────┘ + +统计: {n} 个实体, {n} 个关系 +``` + +### 5. Follow-up Actions + +- `"添加关系"` → Add new entity relation +- `"查看 {entity}"` → Show all relations for entity +- `"导出 mermaid"` → Export as Mermaid diagram +- `"导出 dot"` → Export as DOT format + +### 6. Mermaid Export Format + +```mermaid +graph LR + A[CLI] -->|uses| B[TypeScript] + A -->|depends_on| C[Commander.js] + D[User] -->|prefers| E[中文] +``` + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" diff --git a/_omp/workflows/memory/steps/metrics.md b/_omp/workflows/memory/steps/metrics.md new file mode 100644 index 0000000..fa23c8e --- /dev/null +++ b/_omp/workflows/memory/steps/metrics.md @@ -0,0 +1,112 @@ +--- +name: metrics +description: 显示记忆系统的质量指标和健康度 +--- + +# Step: 质量指标 + +## EXECUTION RULES + +- ✅ Calculate memory quality metrics +- ✅ Track trends over time +- ✅ Provide improvement suggestions + +--- + +## METRICS DEFINITION + +### Core Metrics + +| 指标 | 计算方式 | 健康阈值 | +|------|----------|----------| +| **总量** | count(all) | - | +| **活跃率** | count(active) / total | > 60% | +| **ROT 比例** | count(rot) / total | < 20% | +| **平均置信度** | avg(confidence) | > 0.7 | +| **冲突率** | conflicts / total | < 5% | +| **访问频率** | accesses / day | > 0.1 | + +### Quality Score + +> 📖 **公式详情**: 参见 `_omp/skills/memory-extraction/references/health-score.md` + +健康度计算使用标准公式,综合以下四个指标: +- **活跃率** (30% 权重): Active 状态记忆占比 +- **ROT 比例** (20% 权重): Stale + Cleanup 占比,越低越好 +- **平均置信度** (30% 权重): 所有记忆的平均置信度 +- **冲突率** (20% 权重): 存在冲突的记忆占比,越低越好 + +最终分数 = 加权求和 × 100,范围 0-100 + +--- + +## EXECUTION + +### 1. Collect Memory Data + +Call `list_memories_openmemory` to get: +- Total memory count +- Decay status distribution (Active/Aging/Stale/Cleanup) +- Confidence scores +- Access frequency data +- Conflict information + +### 2. Calculate Metrics + +- **活跃率**: count(Active) / total +- **ROT 比例**: (count(Stale) + count(Cleanup)) / total +- **平均置信度**: mean of all confidence scores +- **冲突率**: count(conflicts) / total +- **访问频率**: total accesses / days since creation + +### 3. Calculate Quality Score + +Apply the formula above to get overall health score (0-100) + +### 4. Display Dashboard + +``` +📊 记忆质量指标 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 整体健康度: {score}/100 + {health_bar} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📈 核心指标 + +| 指标 | 当前 | 阈值 | 状态 | +|------|------|------|------| +| 记忆总量 | {total} | - | ℹ️ | +| 活跃率 | {active_pct}% | >60% | {status} | +| ROT 比例 | {rot_pct}% | <20% | {status} | +| 平均置信度 | {avg_conf} | >0.7 | {status} | +| 冲突率 | {conflict_pct}% | <5% | {status} | + +📊 衰减状态分布 + +├── 🟢 Active: {n} 条 ({pct}%) +├── 🟡 Aging: {n} 条 ({pct}%) +├── 🔴 Stale: {n} 条 ({pct}%) +└── ⚫ Cleanup: {n} 条 ({pct}%) + +💡 改进建议 + +{suggestions} +``` + +### 5. Improvement Suggestions Logic + +- If active_ratio < 60%: "建议清理陈旧记忆以提高活跃率" +- If rot_ratio > 20%: "ROT 比例过高,建议执行清理操作" +- If avg_confidence < 0.7: "置信度较低,建议验证和更新记忆" +- If conflict_ratio > 5%: "存在较多冲突,建议执行同步检查" +- If all metrics healthy: "✅ 系统状态良好,继续保持" + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" + diff --git a/_omp/workflows/memory/steps/search.md b/_omp/workflows/memory/steps/search.md new file mode 100644 index 0000000..81669c5 --- /dev/null +++ b/_omp/workflows/memory/steps/search.md @@ -0,0 +1,68 @@ +--- +name: search +description: 在项目级和用户级记忆中进行语义搜索 +--- + +# Step: 搜索记忆 + +## EXECUTION RULES + +- ✅ Get search query from user if not provided +- ✅ Search both project and user memory +- ✅ Display results with relevance scores + +--- + +## EXECUTION + +### 1. Get Search Query + +If user provided query in their command, use it directly. +Otherwise ask: +> "请输入搜索关键词:" + +### 2. Search User Memory + +Call `search_memory_openmemory` with the query: +- Get matching memories with scores +- Sort by relevance + +### 3. Search Project Memory + +Search `_omp/memory/*.md` and `_omp/memory/*.yaml` files: +- Grep for query keywords +- Match file content + +### 4. Display Results + +``` +🔍 搜索结果: "{query}" + +📁 项目级结果: +{foreach result} +{n}. [{filename}] {matched_content} + 匹配位置: 行 {line_number} +{/foreach} + +👤 用户级结果: +{foreach result} +{n}. {memory_content} + 相关度: {score} | 创建: {time_ago} | 状态: {decay_status} +{/foreach} + +共找到 {total} 条相关记忆 +``` + +### 5. Follow-up Actions + +用户可继续操作: +- `"删除 N"` → 删除指定记忆 +- `"详情 N"` → 展示完整内容 +- `"更新 N"` → 修改记忆内容 + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" diff --git a/_omp/workflows/memory/steps/status.md b/_omp/workflows/memory/steps/status.md new file mode 100644 index 0000000..d07a3bf --- /dev/null +++ b/_omp/workflows/memory/steps/status.md @@ -0,0 +1,141 @@ +--- +name: status +description: 查看项目级和用户级记忆的详细状态 +--- + +# Step: 查看记忆状态 + +## EXECUTION RULES + +- ✅ Execute all steps in order +- ✅ Display results clearly +- ✅ End with menu prompt + +--- + +## EXECUTION + +### 1. Read Project-Level Memory + +Read `_omp/memory/` directory: +- List all files with last modified time +- Count total files + +### 2. Get User-Level Memory + +Call `list_memories_openmemory`: +- Get all user memories +- Count by decay status (Active/Aging/Stale/Cleanup) + +### 3. Calculate Health Score + +> 📖 **公式详情**: 参见 `_omp/skills/memory-extraction/references/health-score.md` + +Calculate the overall health score using the standard formula: +- **活跃率** (30%): Active 状态记忆占比 +- **ROT 比例** (20%): Stale + Cleanup 占比 (越低越好) +- **平均置信度** (30%): 所有记忆的平均置信度 +- **冲突率** (20%): 存在冲突的记忆占比 (越低越好) + +Health emoji mapping: +- >= 80: ✅ (Excellent) +- >= 60: ⚠️ (Good) +- < 60: ❌ (Needs attention) + +### 4. Display Status + +``` +📋 记忆状态 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🏥 健康度: {score}/100 {health_emoji} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📊 记忆系统详细状态 + +📁 项目级 (_omp/memory/) +├── project.yaml ({last_modified}) +├── decisions.yaml ({last_modified}) +└── ... 共 {n} 个文件 + +👤 用户级 (openmemory) +├── 总记忆数: {total} 条 +├── 最近添加: "{latest_memory}" ({time_ago}) +└── 衰减状态: + ├── 🟢 Active: {n} 条 + ├── 🟡 Aging: {n} 条 + ├── 🔴 Stale: {n} 条 + └── ⚫ Cleanup: {n} 条 + +{status_message} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +💡 快速操作: +{if has_issues} +- 输入 "清理" 处理 {rot_count} 条 ROT 记忆 +- 输入 "同步" 解决 {conflict_count} 个冲突 +{else} +✅ 记忆系统运行正常 +{/if} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 5. Status Message Logic + +- If Cleanup > 0: `"⚠️ 有 {n} 条待清理记忆,建议执行清理"` +- If Stale > 3: `"💡 有较多陈旧记忆,建议检查是否仍需要"` +- Else: `"✅ 系统状态正常"` + +### 6. Quick Actions Logic + +Determine if there are issues to address: + +```python +has_issues = (cleanup_count > 0) or (conflict_count > 0) +rot_count = stale_count + cleanup_count +``` + +Display quick action suggestions: +- If `has_issues` is true: + - Show "清理" action if `rot_count > 0` + - Show "同步" action if `conflict_count > 0` +- If `has_issues` is false: + - Show "✅ 记忆系统运行正常" + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" + +--- + +## IMPLEMENTATION NOTES + +### Health Score Calculation Details + +The health score combines four key metrics: + +1. **Active Ratio (30% weight)**: Percentage of memories in "active" status + - Higher active ratio = healthier memory system + +2. **ROT Ratio (20% weight)**: Percentage of memories that are "stale" or "cleanup" + - Lower ROT ratio = healthier memory system + - ROT = Redundant, Obsolete, Trivial + +3. **Average Confidence (30% weight)**: Mean confidence score across all memories + - Higher confidence = more reliable memories + +4. **Conflict Ratio (20% weight)**: Percentage of memories with conflicts + - Lower conflict ratio = healthier memory system + +### Display Format + +The health snapshot appears at the top of the status output with: +- Clear visual separator (━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━) +- Health score out of 100 +- Emoji indicator for quick visual assessment +- Quick action suggestions based on detected issues + +This provides users with an at-a-glance view of memory system health before diving into detailed metrics. diff --git a/_omp/workflows/memory/steps/store.md b/_omp/workflows/memory/steps/store.md new file mode 100644 index 0000000..f30a359 --- /dev/null +++ b/_omp/workflows/memory/steps/store.md @@ -0,0 +1,81 @@ +--- +name: store +description: 手动添加新的记忆到系统中 +--- + +# Step: 存储记忆 + +## EXECUTION RULES + +- ✅ Get content from user if not provided +- ✅ Classify and route to correct storage +- ✅ Confirm before storing + +--- + +## EXECUTION + +### 1. Get Content + +If user provided content in their command, use it directly. +Otherwise ask: +> "请输入要存储的信息:" + +### 2. Classify Information + +Analyze content and determine storage location: + +| Keywords | Storage | Action | +|----------|---------|--------| +| 项目路径, URL, 配置, deploy, domain | `_omp/memory/` | Write to YAML | +| 选择, 决定, 使用, 采用 (技术决策) | `_omp/memory/decisions.yaml` | Append decision | +| 喜欢, 偏好, 习惯 (用户偏好) | openmemory | `add_memories_openmemory` | +| 熟悉, 擅长, 会用 (用户技能) | openmemory | `add_memories_openmemory` | +| Other | Ask user | - | + +### 3. Confirm Storage + +``` +📝 即将存储: + +内容: "{content}" +类型: {type} +位置: {location} + +确认存储?[Y/n] +``` + +### 4. Execute Storage + +**For project-level:** +- Read existing YAML file +- Append new entry with timestamp +- Write back + +**For user-level:** +- Call `add_memories_openmemory` with content + +### 5. Display Result + +``` +💾 记忆已存储 + +类型: {type} +内容: "{content}" +位置: {location} +时间: {timestamp} +``` + +### 6. Batch Storage + +If content contains multiple items (comma/semicolon separated): +- Split into individual items +- Classify each separately +- Store all with confirmation + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要存储其他信息吗?输入 **M** 返回菜单,或直接输入下一个操作" diff --git a/_omp/workflows/memory/steps/sync.md b/_omp/workflows/memory/steps/sync.md new file mode 100644 index 0000000..a5224dd --- /dev/null +++ b/_omp/workflows/memory/steps/sync.md @@ -0,0 +1,246 @@ +--- +name: sync +description: 检测并解决项目级和用户级记忆之间的冲突 +--- + +# Step: 同步检查 + +## EXECUTION RULES + +- ✅ Compare project and user memories semantically +- ✅ Detect conflicts using similarity threshold +- ✅ Auto-resolve when confidence is high +- ✅ Guide user through ambiguous cases + +--- + +## CONFLICT DETECTION + +### Semantic Matching + +使用向量搜索检测冲突: + +```yaml +conflict_detection: + similarity_threshold: 0.7 + confidence_threshold: 0.8 + conflict_types: + - value_mismatch # 同一实体不同值 + - duplicate # 两边存在相同信息 + - stale # 一边过时 + - partial # 部分重叠 +``` + +### Auto-Resolution Rules + +| 场景 | 条件 | 自动处理 | +|------|------|----------| +| 时间戳可判断 | 时间差 > 24h | 保留较新版本 | +| 置信度差异 | 置信度差 > 0.3 | 保留高置信度版本 | +| 完整性差异 | 一方更完整 | 保留完整版本 | +| 项目 vs 用户 | 项目配置相关 | 优先项目级 | +| 完全重复 | 语义相似度 > 0.95 | 删除重复,保留一份 | + +### Manual Resolution Required + +- 两边时间戳接近 (< 24h) +- 置信度相近 (差 < 0.2) +- 语义相似但不完全相同 (0.7-0.95) +- 安全/合规相关信息 +- 用户明确标记为重要的信息 + +--- + +## EXECUTION + +### 1. Read Project Memory + +Read all files in `_omp/memory/`: +- Extract key-value pairs from YAML files +- Extract topics from markdown files +- Record timestamps and confidence scores + +### 2. Build Comparison Index + +For each project memory item: +1. Generate semantic embedding +2. Store with metadata (timestamp, confidence, source) +3. Prepare for similarity matching + +### 3. Search User Memory + +Call `search_memory_openmemory` with project topics: +- Find related user memories with similarity scores +- Retrieve timestamps and confidence data +- Build comparison pairs + +### 4. Analyze Conflicts + +For each comparison pair: + +``` +IF similarity_score >= 0.95: + → Type: DUPLICATE + → Confidence: HIGH + → Action: Auto-resolve (delete duplicate) + +ELSE IF similarity_score >= 0.7: + → Type: POTENTIAL_CONFLICT + → Analyze timestamps: + IF time_diff > 24h: + → Confidence: HIGH + → Action: Auto-resolve (keep newer) + ELSE IF time_diff <= 24h: + → Confidence: MEDIUM + → Action: Manual review needed + + → Analyze confidence scores: + IF confidence_diff > 0.3: + → Confidence: HIGH + → Action: Auto-resolve (keep higher confidence) + ELSE: + → Confidence: MEDIUM + → Action: Manual review needed + + → Analyze completeness: + IF one_is_significantly_more_complete: + → Confidence: HIGH + → Action: Auto-resolve (keep complete version) + +ELSE IF similarity_score >= 0.5: + → Type: PARTIAL_OVERLAP + → Action: Manual review (may need merge) + +ELSE: + → No conflict detected +``` + +### 5. Display Results + +``` +🔄 同步检查结果 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +✅ 自动解决 ({auto_count} 个): + +{foreach auto_resolved} +✓ {description} + ├── 类型: {conflict_type} + ├── 置信度: {confidence}% + └── 操作: {action} +{/foreach} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +⚠️ 需要手动处理 ({manual_count} 个): + +{foreach manual_review} +{n}. {description} + ├── 📁 项目级: {project_value} (时间: {project_time}) + ├── 👤 用户级: {user_value} (时间: {user_time}) + ├── 相似度: {similarity}% + ├── 置信度: {confidence}% + └── 建议: {recommendation} +{/foreach} + +{if no_conflicts} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +✅ 无冲突,项目级和用户级记忆同步正常 +{/if} +``` + +### 6. Resolution Options + +Display menu: + +``` +操作选项: +[A] 执行所有自动解决 +[M] 逐个处理手动项 +[R] 查看详细报告 +[C] 完成同步 +``` + +#### Menu Handling Logic: + +- IF A: Execute all auto-resolved conflicts, then display results +- IF M: Enter manual resolution mode (see step 7) +- IF R: Display detailed conflict analysis report +- IF C: Save changes and return to menu +- IF Any other: Help user, then redisplay menu + +#### EXECUTION RULES: + +- ALWAYS halt and wait for user input after presenting menu +- ONLY proceed when user selects 'C' +- After other menu items execution, return to this menu + +### 7. Manual Resolution Mode + +For each manual conflict: + +``` +⚠️ 冲突 {n}/{total}: {description} + +📁 项目级: {project_value} + 时间: {project_time} + 置信度: {project_confidence}% + +👤 用户级: {user_value} + 时间: {user_time} + 置信度: {user_confidence}% + +相似度: {similarity}% +建议: {recommendation} + +操作选项: +[P] 保留项目级版本 +[U] 保留用户级版本 +[M] 手动合并 +[S] 跳过此冲突 +[Q] 返回菜单 +``` + +Handle each selection: +- P: Keep project version, delete user version +- U: Keep user version, update project version +- M: Prompt for merged content +- S: Skip this conflict +- Q: Return to main menu + +### 8. Execute Resolution + +Based on user selections: +- Update or delete memories as needed +- For project-level: update YAML files +- For user-level: call `update_memories_openmemory` or `delete_memories_openmemory` +- Display summary of changes + +### 9. Display Summary + +``` +✅ 同步完成 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📊 处理统计: +├── 自动解决: {auto_count} 个 +├── 手动处理: {manual_count} 个 +├── 跳过: {skipped_count} 个 +└── 总计: {total_count} 个 + +📝 变更详情: +├── 更新: {updated_count} 条记忆 +├── 删除: {deleted_count} 条记忆 +└── 合并: {merged_count} 条记忆 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## RETURN TO MENU + +完成后提示: +> "还需要其他操作吗?输入 **M** 返回菜单,或直接输入下一个操作" diff --git a/_omp/workflows/memory/workflow.md b/_omp/workflows/memory/workflow.md new file mode 100644 index 0000000..f292ee7 --- /dev/null +++ b/_omp/workflows/memory/workflow.md @@ -0,0 +1,151 @@ +--- +name: memory +description: 记忆管理工作流 - 统一管理项目级和用户级记忆 +version: "2.0" +--- + +# Memory Management Workflow + +**Goal:** 提供统一的记忆管理入口,支持查看、搜索、存储、清理、同步、衰减分析和知识图谱功能。 + +**Your Role:** 你是记忆管理专家,帮助用户高效管理项目级(`memory/`)和用户级(openmemory)记忆。使用中文交流,技术术语保留英文。 + +--- + +## WORKFLOW ARCHITECTURE + +This uses **micro-file architecture** with **menu-driven routing**: + +- Main workflow displays status and menu +- Each action is a self-contained step file +- User selects action via number or natural language +- After action completion, return to menu + +--- + +## INITIALIZATION + +### Configuration + +- `installed_path` = `{project-root}/_omp/workflows/memory` +- `memory_folder` = `{project-root}/_omp/memory` +- `steps_path` = `{installed_path}/steps` + +### MCP Tools Available + +| Tool | Purpose | +|------|---------| +| `add_memories_openmemory` | 添加用户级记忆 | +| `search_memory_openmemory` | 语义搜索记忆 | +| `list_memories_openmemory` | 列出所有记忆 | +| `delete_memories_openmemory` | 删除指定记忆 | + +--- + +## EXECUTION + +### Step 1: Quick Status Check + +1. Read `_omp/memory/` directory, count files +2. Call `list_memories_openmemory` to get user memory count +3. Display status summary + +### Step 2: Display Menu + +``` +🧠 OpenMemory Plus - 记忆管理 + +📊 当前状态: +├── 项目级 (_omp/memory/): {n} 个文件 +└── 用户级 (openmemory): {n} 条记忆 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +选择操作: + +[1] 📊 查看状态 详细记忆状态和统计 +[2] 🔍 搜索记忆 语义搜索项目和用户记忆 +[3] 💾 存储记忆 手动添加新记忆 +[4] 🧹 清理记忆 清理 ROT (冗余/过时/琐碎) +[5] 🔄 同步检查 检测冲突并解决 +[6] ⏰ 衰减分析 查看记忆衰减状态 +[7] 🔗 知识图谱 查看实体关系 +[8] 📦 记忆整合 合并碎片化记忆 +[9] 📊 质量指标 查看记忆健康度和指标 + +[M] 返回菜单 [X] 退出 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +输入数字选择,或直接描述你的需求: +``` + +### Step 3: Wait for User Input + +**STOP and WAIT** for user input. Do NOT proceed automatically. + +### Step 4: Route to Step File + +Based on user input, load the corresponding step file: + +| Input | Keywords | Load Step | +|-------|----------|-----------| +| `1` | 状态, 概览, status, overview | `./steps/status.md` | +| `2` | 搜索, 找, 查, search, find | `./steps/search.md` | +| `3` | 存储, 记住, 保存, store, save, remember | `./steps/store.md` | +| `4` | 清理, 删除, clean, delete, remove | `./steps/clean.md` | +| `5` | 同步, 冲突, sync, conflict | `./steps/sync.md` | +| `6` | 衰减, 老化, decay, aging | `./steps/decay.md` | +| `7` | 图谱, 关系, graph, relation | `./steps/graph.md` | +| `8` | 整合, 合并, consolidate, merge | `./steps/consolidate.md` | +| `9` | 指标, 质量, metrics, health | `./steps/metrics.md` | +| `M` | 菜单, menu | Re-display menu | +| `X` | 退出, exit, quit | Exit workflow | + +### Natural Language Routing + +Support natural language commands: + +- "搜索部署配置" → Load `search.md` +- "这个项目用 React" → Load `store.md` +- "清理过期的记忆" → Load `clean.md` +- "查看记忆衰减" → Load `decay.md` + +--- + +## STEP FILE PROTOCOL + +Each step file MUST: + +1. Execute its specific action +2. Display results clearly +3. Offer follow-up actions +4. End with: `"还需要其他操作吗?输入 M 返回菜单,或直接输入下一个操作"` + +--- + +## FALLBACK (MCP Unavailable) + +If `openmemory` MCP tools are not available: + +1. Display warning: `"⚠️ openmemory MCP 不可用,用户级记忆功能受限"` +2. Offer to store user-level info temporarily in `_omp/memory/user-context.yaml` +3. Continue with project-level memory operations + +--- + +## SUCCESS METRICS + +✅ Quick status displayed on entry +✅ Menu clearly presented +✅ User input correctly routed +✅ Each action completes with clear output +✅ Return to menu flow works smoothly + +## FAILURE MODES + +❌ Proceeding without user input +❌ Misrouting user commands +❌ Not handling MCP unavailability +❌ Breaking out of menu loop unexpectedly + diff --git a/cli/src/commands/install.ts b/cli/src/commands/install.ts index 6255738..fb268d2 100644 --- a/cli/src/commands/install.ts +++ b/cli/src/commands/install.ts @@ -32,6 +32,12 @@ import { IDE_MCP_CONFIGS, type McpConfigResult, } from '../lib/mcp-config.js'; +import { + LLM_PROVIDERS, + PROVIDER_CHOICES, + getMcpEnvForProvider, + validateApiKey, +} from '../lib/providers.js'; // ============================================================================ // Types @@ -47,6 +53,13 @@ interface InstallOptions { configureMcp?: boolean; // Auto-configure MCP for IDEs verify?: boolean; // Verify MCP setup after install skipVerify?: boolean; // Skip verification + llm?: string; // LLM Provider for categorization +} + +/** Selected provider state for the install session */ +interface ProviderState { + name: string; + apiKey?: string; } interface IdeConfig { @@ -383,6 +396,147 @@ function processTemplate(content: string, projectName: string): string { .replace(/\{\{CREATED_AT\}\}/g, now); } +// ============================================================================ +// LLM Provider Selection +// ============================================================================ + +/** + * Select LLM Provider for memory categorization + */ +async function selectLlmProvider(options: InstallOptions): Promise { + // If provider specified via CLI option + if (options.llm) { + const providerName = options.llm.toLowerCase(); + if (LLM_PROVIDERS[providerName]) { + console.log(chalk.green(` \u2713 \u4f7f\u7528 LLM Provider: ${LLM_PROVIDERS[providerName].displayName}`)); + return { name: providerName }; + } else { + console.log(chalk.yellow(` \u26a0 \u672a\u77e5\u7684 Provider: ${options.llm}`)); + console.log(chalk.gray(` \u6709\u6548\u9009\u9879: ${Object.keys(LLM_PROVIDERS).join(', ')}`)); + if (!isTTY() || isCI() || options.yes) { + console.log(chalk.red(' \u2717 \u975e\u4ea4\u4e92\u6a21\u5f0f\u4e0b\u5fc5\u987b\u6307\u5b9a\u6709\u6548\u7684 Provider')); + return { name: 'none' }; + } + console.log(chalk.gray(' \u5c06\u8fdb\u5165\u4ea4\u4e92\u5f0f\u9009\u62e9...\n')); + } + } + + // Non-interactive mode: skip provider selection + if (!isTTY() || isCI() || options.yes) { + console.log(chalk.gray(' \u8df3\u8fc7 LLM Provider \u914d\u7f6e (\u53ef\u7a0d\u540e\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf)')); + return { name: 'none' }; + } + + console.log(chalk.bold.cyan('\n\u2501\u2501\u2501 LLM Provider \u914d\u7f6e \u2501\u2501\u2501\n')); + console.log(chalk.gray('\u8bb0\u5fc6\u5206\u7c7b\u529f\u80fd\u9700\u8981 LLM \u670d\u52a1\uff0c\u9009\u62e9\u4e00\u4e2a Provider:\n')); + + const { provider } = await inquirer.prompt([ + { + type: 'list', + name: 'provider', + message: '\u9009\u62e9 LLM Provider:', + choices: [ + ...PROVIDER_CHOICES, + new inquirer.Separator(), + { name: '\u23ed\ufe0f \u8df3\u8fc7 (\u7a0d\u540e\u914d\u7f6e)', value: 'none' }, + ], + default: 'deepseek', + }, + ]); + + if (provider === 'none') { + console.log(chalk.yellow('\n\u5df2\u8df3\u8fc7 LLM \u914d\u7f6e\uff0c\u8bb0\u5fc6\u5206\u7c7b\u529f\u80fd\u6682\u65f6\u4e0d\u53ef\u7528')); + console.log(chalk.gray('\u540e\u7eed\u53ef\u901a\u8fc7\u8bbe\u7f6e\u73af\u5883\u53d8\u91cf\u542f\u7528\n')); + return { name: 'none' }; + } + + const providerConfig = LLM_PROVIDERS[provider]; + + // Ollama doesn't need API key + if (provider === 'ollama') { + console.log(chalk.green(`\n \u2713 \u4f7f\u7528\u672c\u5730 Ollama\uff0c\u65e0\u9700 API Key`)); + console.log(chalk.gray(` \u8bf7\u786e\u4fdd\u5df2\u5b89\u88c5\u6a21\u578b: ollama pull ${providerConfig.defaultModel}\n`)); + return { name: provider }; + } + + // Other providers need API key + const { apiKey } = await inquirer.prompt([ + { + type: 'password', + name: 'apiKey', + message: `\u8bf7\u8f93\u5165 ${providerConfig.displayName} API Key:`, + mask: '*', + validate: (input: string) => input.length > 0 || '\u8bf7\u8f93\u5165\u6709\u6548\u7684 API Key', + }, + ]); + + // L2 Fix: Validate API Key + const { shouldValidate } = await inquirer.prompt([ + { + type: 'confirm', + name: 'shouldValidate', + message: '\u662f\u5426\u9a8c\u8bc1 API Key \u6709\u6548\u6027?', + default: true, + }, + ]); + + if (shouldValidate) { + const spinner = ora('\u9a8c\u8bc1 API Key...').start(); + const result = await validateApiKey(provider, apiKey); + + if (result.valid) { + spinner.succeed(chalk.green('API Key \u9a8c\u8bc1\u6210\u529f')); + } else { + spinner.fail(chalk.red(`API Key \u9a8c\u8bc1\u5931\u8d25: ${result.error}`)); + + const { continueAnyway } = await inquirer.prompt([ + { + type: 'confirm', + name: 'continueAnyway', + message: '\u662f\u5426\u4ecd\u7136\u7ee7\u7eed\u4f7f\u7528\u6b64 API Key?', + default: false, + }, + ]); + + if (!continueAnyway) { + console.log(chalk.yellow('\n\u5df2\u53d6\u6d88\uff0c\u8bf7\u91cd\u65b0\u8fd0\u884c\u5b89\u88c5\u547d\u4ee4')); + return { name: 'none' }; + } + } + } + + console.log(chalk.green(`\n \u2713 ${providerConfig.displayName} \u914d\u7f6e\u5b8c\u6210`)); + + return { name: provider, apiKey }; +} + +/** + * Generate .env file content for the selected provider + */ +function generateEnvFile(providerState: ProviderState): string { + const lines = [ + '# OpenMemory Plus - Environment Configuration', + `# Generated: ${new Date().toISOString()}`, + '', + '# LLM Provider Configuration', + ]; + + if (providerState.name !== 'none') { + const provider = LLM_PROVIDERS[providerState.name]; + if (provider && provider.envKey && providerState.apiKey) { + lines.push(`${provider.envKey}=${providerState.apiKey}`); + if (provider.baseUrl && providerState.name !== 'openai') { + const baseUrlKey = provider.envKey.replace('_API_KEY', '_BASE_URL'); + lines.push(`${baseUrlKey}=${provider.baseUrl}`); + } + lines.push(`LLM_MODEL=${provider.defaultModel}`); + } + } + + lines.push(''); + return lines.join('\n'); +} + // ============================================================================ // Phase 1: System Dependencies (with Docker Compose support) // ============================================================================ @@ -734,6 +888,19 @@ async function phase2_initProject(options: InstallOptions): Promise { } console.log(chalk.green(' ✓ 创建 _omp/ (核心目录)')); + // Copy patches directory for LLM provider support + const patchesSource = join(templatesDir, 'patches'); + const patchesTarget = join(ompDir, 'patches'); + if (existsSync(patchesSource)) { + mkdirSync(patchesTarget, { recursive: true }); + const patchErrors = copyDir(patchesSource, patchesTarget); + if (patchErrors.length > 0) { + console.log(chalk.yellow(' ⚠ 部分补丁文件复制失败:')); + patchErrors.forEach((err) => console.log(chalk.gray(` - ${err}`))); + } + console.log(chalk.green(' ✓ 创建 _omp/patches/ (LLM 适配)')); + } + // Process memory template files with project name const ompMemoryDir = join(ompDir, 'memory'); // Ensure memory directory exists (may not be in template) @@ -867,11 +1034,19 @@ async function phase3_configureMcp(ide: string, options: InstallOptions): Promis return true; } -function phase3_showCompletion(ide: string, mcpConfigured: boolean): void { +function phase3_showCompletion(ide: string, mcpConfigured: boolean, providerState?: ProviderState): void { console.log(chalk.bold.cyan('\n━━━ 安装完成 ━━━\n')); console.log(chalk.green.bold('🎉 OpenMemory Plus 已成功安装!\n')); + // Show provider info if configured + if (providerState && providerState.name !== 'none') { + const provider = LLM_PROVIDERS[providerState.name]; + console.log(chalk.bold('🤖 LLM Provider: ') + chalk.cyan(provider.displayName)); + console.log(chalk.gray(` 模型: ${provider.defaultModel}`)); + console.log(''); + } + if (mcpConfigured) { console.log(chalk.green('✓ MCP 已自动配置到 ' + (IDE_MCP_CONFIGS[ide]?.name || ide))); console.log(''); @@ -906,12 +1081,44 @@ export async function installCommand(options: InstallOptions): Promise { // Phase 1: Check and install dependencies await phase1_checkAndInstallDeps(options); + // Phase 1.5: Select LLM Provider + const providerState = await selectLlmProvider(options); + // Phase 2: Initialize project const ide = await phase2_initProject(options); + // Generate .env file if provider is configured + if (providerState && providerState.name !== 'none' && providerState.apiKey) { + const cwd = process.cwd(); + const envPath = join(cwd, '.env'); + if (!existsSync(envPath) || options.force) { + const envContent = generateEnvFile(providerState); + writeFileSync(envPath, envContent); + console.log(chalk.green(` \u2713 \u521b\u5efa .env (${LLM_PROVIDERS[providerState.name].displayName} \u914d\u7f6e)`)); + + // H3 Fix: \u786e\u4fdd .gitignore \u5305\u542b .env + const gitignorePath = join(cwd, '.gitignore'); + const envEntries = ['.env', '.env.local', '.env*.local']; + if (existsSync(gitignorePath)) { + const content = readFileSync(gitignorePath, 'utf-8'); + const missingEntries = envEntries.filter(e => !content.includes(e)); + if (missingEntries.length > 0) { + writeFileSync(gitignorePath, content + '\n# Environment files\n' + missingEntries.join('\n') + '\n'); + console.log(chalk.green(' \u2713 \u66f4\u65b0 .gitignore (\u6dfb\u52a0 .env)')); + } + } else { + writeFileSync(gitignorePath, '# Environment files\n' + envEntries.join('\n') + '\n'); + console.log(chalk.green(' \u2713 \u521b\u5efa .gitignore')); + } + + // H3 Fix: \u663e\u793a\u5b89\u5168\u8b66\u544a + console.log(chalk.yellow('\n \u26a0\ufe0f \u5b89\u5168\u63d0\u793a: .env \u6587\u4ef6\u5305\u542b API Key\uff0c\u8bf7\u52ff\u63d0\u4ea4\u5230\u7248\u672c\u63a7\u5236')); + } + } + // Phase 3: Configure MCP, verify, and test const mcpSuccess = await phase3_configureMcp(ide, options); // Show completion - phase3_showCompletion(ide, mcpSuccess); + phase3_showCompletion(ide, mcpSuccess, providerState); } diff --git a/cli/src/index.ts b/cli/src/index.ts index fba8545..1b46623 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -47,6 +47,7 @@ program .description('一键安装和配置 OpenMemory Plus (推荐)') .option('-y, --yes', '跳过确认提示') .option('-i, --ide ', 'IDE 类型: augment, claude, cursor, gemini, common') + .option('--llm ', 'LLM Provider: deepseek, minimax, zhipu, qwen, openai, ollama') .option('--skip-deps', '跳过依赖安装,仅配置项目') .option('--show-mcp', '显示 MCP 配置 JSON') .option('-f, --force', '强制覆盖已存在的配置文件') diff --git a/cli/src/lib/providers.ts b/cli/src/lib/providers.ts new file mode 100644 index 0000000..ad9e0f3 --- /dev/null +++ b/cli/src/lib/providers.ts @@ -0,0 +1,214 @@ +/** + * LLM Provider Configuration + * 支持多种 LLM Provider 用于记忆分类功能 + */ + +export interface LlmProvider { + name: string; + displayName: string; + baseUrl: string; + defaultModel: string; + envKey: string; + supportsStructuredOutput: boolean; + description: string; +} + +export const LLM_PROVIDERS: Record = { + deepseek: { + name: 'deepseek', + displayName: 'DeepSeek', + baseUrl: 'https://api.deepseek.com', + defaultModel: 'deepseek-chat', + envKey: 'DEEPSEEK_API_KEY', + supportsStructuredOutput: false, + description: '性价比高,中文能力强', + }, + minimax: { + name: 'minimax', + displayName: 'MiniMax', + baseUrl: 'https://api.minimax.chat/v1', + defaultModel: 'abab6.5s-chat', + envKey: 'MINIMAX_API_KEY', + supportsStructuredOutput: false, + description: '国产大模型,响应快速', + }, + zhipu: { + name: 'zhipu', + displayName: '智谱 AI (ZhiPu)', + baseUrl: 'https://open.bigmodel.cn/api/paas/v4', + defaultModel: 'glm-4-flash', + envKey: 'ZHIPU_API_KEY', + supportsStructuredOutput: false, + description: 'GLM 系列,学术背景', + }, + qwen: { + name: 'qwen', + displayName: '通义千问 (Qwen)', + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + defaultModel: 'qwen-turbo', + envKey: 'DASHSCOPE_API_KEY', + supportsStructuredOutput: false, + description: '阿里云,生态完善', + }, + openai: { + name: 'openai', + displayName: 'OpenAI', + baseUrl: 'https://api.openai.com/v1', + defaultModel: 'gpt-4o-mini', + envKey: 'OPENAI_API_KEY', + supportsStructuredOutput: true, + description: '原版 GPT,功能最全', + }, + ollama: { + name: 'ollama', + displayName: 'Ollama (本地)', + baseUrl: 'http://localhost:11434/v1', + defaultModel: 'qwen2.5:7b', + envKey: '', + supportsStructuredOutput: false, + description: '本地运行,无需 API Key', + }, +}; + +// Provider icons for display +const PROVIDER_ICONS: Record = { + deepseek: '🚀', + minimax: '🤖', + zhipu: '🧠', + qwen: '☁️', + openai: '🌐', + ollama: '💻', +}; + +// L1 Fix: Auto-generate PROVIDER_CHOICES from LLM_PROVIDERS +export const PROVIDER_CHOICES = Object.entries(LLM_PROVIDERS).map(([key, provider]) => { + const icon = PROVIDER_ICONS[key] || '📦'; + const recommended = key === 'deepseek' ? ' (推荐)' : ''; + return { + name: `${icon} ${provider.displayName}${recommended} - ${provider.description}`, + value: key, + }; +}); + +/** + * 生成 Provider 的环境变量配置 + */ +export function generateProviderEnv( + providerName: string, + apiKey?: string +): Record { + const provider = LLM_PROVIDERS[providerName]; + if (!provider) return {}; + + const env: Record = {}; + + // 添加 API Key (如果有) + if (provider.envKey && apiKey) { + env[provider.envKey] = apiKey; + } + + // 添加 base URL (如果不是默认的 OpenAI,且有 envKey) + // H4 Fix: 跳过 Ollama (envKey 为空) + if (provider.baseUrl && providerName !== 'openai' && provider.envKey) { + const baseUrlKey = provider.envKey.replace('_API_KEY', '_BASE_URL'); + env[baseUrlKey] = provider.baseUrl; + } + + // 添加模型配置 + env['LLM_MODEL'] = provider.defaultModel; + env['LLM_PROVIDER'] = providerName; + + return env; +} + +/** + * 获取 MCP 配置中需要的环境变量 + */ +export function getMcpEnvForProvider( + providerName: string, + apiKey?: string +): Record { + const provider = LLM_PROVIDERS[providerName]; + if (!provider) return {}; + + const env: Record = { + MEM0_EMBEDDING_MODEL: 'bge-m3', + MEM0_EMBEDDING_PROVIDER: 'ollama', + OLLAMA_HOST: 'http://localhost:11434', + QDRANT_HOST: 'localhost', + QDRANT_PORT: '6333', + }; + + if (provider.envKey && apiKey) { + env[provider.envKey] = apiKey; + } + + if (provider.baseUrl && providerName !== 'openai') { + // 使用 OPENAI_BASE_URL 作为通用的 base URL 配置 + env['OPENAI_BASE_URL'] = provider.baseUrl; + // 同时设置 provider 特定的 key + if (apiKey) { + env['OPENAI_API_KEY'] = apiKey; + } + } + + return env; +} + +/** + * L2 Fix: Validate API Key by making a test request + * Returns { valid: true } or { valid: false, error: string } + */ +export async function validateApiKey( + providerName: string, + apiKey: string +): Promise<{ valid: boolean; error?: string }> { + const provider = LLM_PROVIDERS[providerName]; + if (!provider) { + return { valid: false, error: '未知的 Provider' }; + } + + // Ollama doesn't need API key validation + if (providerName === 'ollama') { + return { valid: true }; + } + + try { + const baseUrl = provider.baseUrl || 'https://api.openai.com/v1'; + const response = await fetch(`${baseUrl}/models`, { + method: 'GET', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + signal: AbortSignal.timeout(10000), // 10 second timeout + }); + + if (response.ok) { + return { valid: true }; + } + + // Handle common error codes + if (response.status === 401) { + return { valid: false, error: 'API Key 无效或已过期' }; + } + if (response.status === 403) { + return { valid: false, error: 'API Key 权限不足' }; + } + if (response.status === 429) { + // Rate limited but key is valid + return { valid: true }; + } + + return { valid: false, error: `HTTP ${response.status}: ${response.statusText}` }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes('timeout') || message.includes('TimeoutError')) { + return { valid: false, error: '连接超时,请检查网络' }; + } + if (message.includes('fetch') || message.includes('network')) { + return { valid: false, error: '网络错误,无法连接到 API' }; + } + return { valid: false, error: message }; + } +} diff --git a/cli/templates/docker-compose.full.yml b/cli/templates/docker-compose.full.yml new file mode 100644 index 0000000..571321e --- /dev/null +++ b/cli/templates/docker-compose.full.yml @@ -0,0 +1,129 @@ +# OpenMemory Plus - Full Stack Docker Compose +# 完整版:Qdrant + Ollama + BGE-M3 + OpenMemory MCP +# +# 使用方法: +# 启动: docker compose -f docker-compose.full.yml up -d +# 停止: docker compose -f docker-compose.full.yml down +# 日志: docker compose -f docker-compose.full.yml logs -f +# +# 配置 LLM Provider: +# 在 .env 文件中设置对应的 API Key: +# - DEEPSEEK_API_KEY (推荐) +# - MINIMAX_API_KEY +# - ZHIPU_API_KEY +# - DASHSCOPE_API_KEY (通义千问) +# - OPENAI_API_KEY + +version: '3.8' + +services: + # Qdrant 向量数据库 + qdrant: + image: qdrant/qdrant:latest + container_name: openmemory-qdrant + ports: + - "6333:6333" + - "6334:6334" + volumes: + - qdrant_data:/qdrant/storage + environment: + - QDRANT__SERVICE__GRPC_PORT=6334 + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:6333/readyz"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # Ollama - 本地 LLM 运行时 (用于 BGE-M3 嵌入) + ollama: + image: ollama/ollama:latest + container_name: openmemory-ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # BGE-M3 模型初始化器 + bge-m3-init: + image: ollama/ollama:latest + container_name: openmemory-bge-init + depends_on: + ollama: + condition: service_healthy + volumes: + - ollama_data:/root/.ollama + environment: + - OLLAMA_HOST=http://ollama:11434 + entrypoint: ["/bin/sh", "-c"] + command: + - | + echo "🔄 检查 BGE-M3 模型..." + if ollama list | grep -q "bge-m3"; then + echo "✅ BGE-M3 模型已存在" + else + echo "📥 下载 BGE-M3 模型 (约 1.2GB)..." + ollama pull bge-m3 + echo "✅ BGE-M3 模型下载完成" + fi + restart: "no" + + # OpenMemory MCP 服务 + openmemory-mcp: + image: mem0ai/openmemory-mcp:latest + container_name: openmemory-mcp + ports: + - "8765:8765" + volumes: + # 挂载修改后的 categorization.py 以支持多 LLM Provider + - ./patches/categorization.py:/app/app/utils/categorization.py:ro + environment: + # Qdrant 配置 + - QDRANT_HOST=qdrant + - QDRANT_PORT=6333 + # Ollama 配置 (用于 embedding) + - OLLAMA_HOST=http://ollama:11434 + - MEM0_EMBEDDING_MODEL=bge-m3 + - MEM0_EMBEDDING_PROVIDER=ollama + # LLM Provider 配置 (用于记忆分类) + # 只需设置一个,系统会自动检测 + - DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY:-} + - MINIMAX_API_KEY=${MINIMAX_API_KEY:-} + - ZHIPU_API_KEY=${ZHIPU_API_KEY:-} + - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY:-} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + # 可选: 自定义模型 + - LLM_MODEL=${LLM_MODEL:-} + depends_on: + qdrant: + condition: service_healthy + ollama: + condition: service_healthy + bge-m3-init: + condition: service_completed_successfully + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8765/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + +volumes: + qdrant_data: + name: openmemory-qdrant-data + ollama_data: + name: openmemory-ollama-data + +networks: + default: + name: openmemory-network + diff --git a/cli/templates/patches/categorization.py b/cli/templates/patches/categorization.py new file mode 100644 index 0000000..0b1710a --- /dev/null +++ b/cli/templates/patches/categorization.py @@ -0,0 +1,142 @@ +""" +OpenMemory Plus - Multi-Provider Categorization Module +支持多种 LLM Provider 的记忆分类功能 + +Supported Providers: +- DeepSeek (推荐) +- MiniMax +- ZhiPu (智谱 AI) +- Qwen (通义千问) +- OpenAI +- Ollama (本地) +""" + +import json +import logging +import os +from typing import List, Optional, Tuple + +from app.utils.prompts import MEMORY_CATEGORIZATION_PROMPT +from dotenv import load_dotenv +from openai import OpenAI +from pydantic import BaseModel +from tenacity import retry, stop_after_attempt, wait_exponential + +load_dotenv() + + +class MemoryCategories(BaseModel): + categories: List[str] + + +# Provider configuration: (env_key, base_url, default_model, supports_structured_output) +# M2 Fix: Added Ollama support for local LLM +PROVIDERS = [ + ("DEEPSEEK_API_KEY", "https://api.deepseek.com", "deepseek-chat", False), + ("MINIMAX_API_KEY", "https://api.minimax.chat/v1", "abab6.5s-chat", False), + ("ZHIPU_API_KEY", "https://open.bigmodel.cn/api/paas/v4", "glm-4-flash", False), + ("DASHSCOPE_API_KEY", "https://dashscope.aliyuncs.com/compatible-mode/v1", "qwen-turbo", False), + ("OPENAI_API_KEY", None, "gpt-4o-mini", True), # None = use default OpenAI URL +] + +# Ollama configuration (local LLM, no API key needed) +OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://localhost:11434") +OLLAMA_MODEL = os.environ.get("LLM_MODEL", "qwen2.5:7b") + + +def get_llm_config() -> Tuple[Optional[str], Optional[str], Optional[str], bool]: + """Detect configured LLM provider from environment variables.""" + # First check for API-based providers + for env_key, base_url, default_model, supports_structured in PROVIDERS: + api_key = os.environ.get(env_key) + if api_key: + # Allow custom base_url override + base_url_key = env_key.replace("_API_KEY", "_BASE_URL") + actual_base_url = os.environ.get(base_url_key) or base_url + # Allow custom model override + actual_model = os.environ.get("LLM_MODEL") or default_model + provider_name = env_key.replace("_API_KEY", "").lower() + logging.info(f"[LLM] Using provider: {provider_name}, model: {actual_model}") + return api_key, actual_base_url, actual_model, supports_structured + + # M2 Fix: Check for Ollama (local LLM, no API key needed) + llm_provider = os.environ.get("LLM_PROVIDER", "").lower() + if llm_provider == "ollama" or os.environ.get("OLLAMA_HOST"): + ollama_base_url = f"{OLLAMA_HOST}/v1" + logging.info(f"[LLM] Using provider: ollama, model: {OLLAMA_MODEL}") + # Ollama uses "ollama" as a dummy API key for OpenAI-compatible endpoint + return "ollama", ollama_base_url, OLLAMA_MODEL, False + + return None, None, None, False + + +# Initialize LLM client +api_key, base_url, model_name, supports_structured = get_llm_config() + +openai_client: Optional[OpenAI] = None +if api_key: + if base_url: + openai_client = OpenAI(api_key=api_key, base_url=base_url) + else: + openai_client = OpenAI(api_key=api_key) + + +def extract_json_from_text(text: str) -> Optional[dict]: + """Extract JSON object from text response.""" + text = text.strip() + # Try to find JSON object in the response + start_idx = text.find("{") + end_idx = text.rfind("}") + 1 + if start_idx != -1 and end_idx > start_idx: + try: + return json.loads(text[start_idx:end_idx]) + except json.JSONDecodeError: + pass + return None + + +@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15)) +def get_categories_for_memory(memory: str) -> List[str]: + """Get categories for a memory using configured LLM provider.""" + if openai_client is None: + logging.warning("[WARN] No LLM API key configured, skipping categorization") + return [] + + try: + messages = [ + {"role": "system", "content": MEMORY_CATEGORIZATION_PROMPT}, + {"role": "user", "content": memory}, + ] + + if supports_structured: + # OpenAI with structured output (beta API) + completion = openai_client.beta.chat.completions.parse( + model=model_name, + messages=messages, + response_format=MemoryCategories, + temperature=0, + ) + parsed: MemoryCategories = completion.choices[0].message.parsed + return [cat.strip().lower() for cat in parsed.categories] + else: + # Other providers: use JSON mode with explicit instruction + json_instruction = ( + 'Return ONLY a JSON object with format: {"categories": ["cat1", "cat2"]}. ' + "No other text or explanation." + ) + completion = openai_client.chat.completions.create( + model=model_name, + messages=messages + [{"role": "user", "content": json_instruction}], + temperature=0, + ) + content = completion.choices[0].message.content + if content: + parsed_json = extract_json_from_text(content) + if parsed_json and "categories" in parsed_json: + return [cat.strip().lower() for cat in parsed_json["categories"]] + return [] + + except Exception as e: + logging.error(f"[ERROR] Failed to get categories: {e}") + return [] + diff --git a/cli/tests/install.test.ts b/cli/tests/install.test.ts index f464e3a..ad3ab32 100644 --- a/cli/tests/install.test.ts +++ b/cli/tests/install.test.ts @@ -138,5 +138,48 @@ describe('install command', () => { // But still install valid ones expect(existsSync(join(TEST_DIR, '.augment', 'commands', 'memory.md'))).toBe(true); }); + + // H2 Fix: Add tests for --llm option + describe('--llm option', () => { + it('should accept valid provider via --llm option', () => { + const output = execSync(`node ${CLI_PATH} install -i augment -y --skip-deps --llm deepseek`, { + cwd: TEST_DIR, + encoding: 'utf-8', + }); + + expect(output).toContain('DeepSeek'); + expect(existsSync(join(TEST_DIR, '_omp', 'commands', 'memory.md'))).toBe(true); + }); + + it('should warn about invalid provider in non-interactive mode', () => { + const output = execSync(`node ${CLI_PATH} install -i augment -y --skip-deps --llm invalid_provider`, { + cwd: TEST_DIR, + encoding: 'utf-8', + }); + + expect(output).toContain('未知的 Provider'); + expect(output).toContain('invalid_provider'); + // Should still complete installation + expect(existsSync(join(TEST_DIR, '_omp', 'commands', 'memory.md'))).toBe(true); + }); + + it('should show --llm option in help', () => { + const output = execSync(`node ${CLI_PATH} install --help`, { + encoding: 'utf-8', + }); + + expect(output).toContain('--llm'); + expect(output).toContain('deepseek'); + }); + + it('should copy patches directory for LLM provider support', () => { + execSync(`node ${CLI_PATH} install -i augment -y --skip-deps --llm ollama`, { + cwd: TEST_DIR, + stdio: 'pipe', + }); + + expect(existsSync(join(TEST_DIR, '_omp', 'patches', 'categorization.py'))).toBe(true); + }); + }); }); diff --git a/cli/tests/providers.test.ts b/cli/tests/providers.test.ts new file mode 100644 index 0000000..5865b70 --- /dev/null +++ b/cli/tests/providers.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + LLM_PROVIDERS, + PROVIDER_CHOICES, + generateProviderEnv, + getMcpEnvForProvider, + validateApiKey, + type LlmProvider, +} from '../src/lib/providers.js'; + +describe('LLM Providers Configuration', () => { + describe('LLM_PROVIDERS', () => { + it('should have all expected providers', () => { + const expectedProviders = ['deepseek', 'minimax', 'zhipu', 'qwen', 'openai', 'ollama']; + expect(Object.keys(LLM_PROVIDERS)).toEqual(expectedProviders); + }); + + it('should have valid structure for each provider', () => { + for (const [key, provider] of Object.entries(LLM_PROVIDERS)) { + expect(provider.name).toBe(key); + expect(provider.displayName).toBeTruthy(); + expect(provider.baseUrl).toBeTruthy(); + expect(provider.defaultModel).toBeTruthy(); + expect(typeof provider.supportsStructuredOutput).toBe('boolean'); + expect(provider.description).toBeTruthy(); + } + }); + + it('should have envKey for all providers except ollama', () => { + for (const [key, provider] of Object.entries(LLM_PROVIDERS)) { + if (key === 'ollama') { + expect(provider.envKey).toBe(''); + } else { + expect(provider.envKey).toMatch(/_API_KEY$/); + } + } + }); + + it('should only have OpenAI supporting structured output', () => { + expect(LLM_PROVIDERS.openai.supportsStructuredOutput).toBe(true); + for (const [key, provider] of Object.entries(LLM_PROVIDERS)) { + if (key !== 'openai') { + expect(provider.supportsStructuredOutput).toBe(false); + } + } + }); + }); + + describe('PROVIDER_CHOICES', () => { + it('should have same number of choices as providers', () => { + expect(PROVIDER_CHOICES.length).toBe(Object.keys(LLM_PROVIDERS).length); + }); + + it('should have valid choice structure', () => { + for (const choice of PROVIDER_CHOICES) { + expect(choice.name).toBeTruthy(); + expect(choice.value).toBeTruthy(); + expect(LLM_PROVIDERS[choice.value]).toBeDefined(); + } + }); + }); + + describe('generateProviderEnv', () => { + it('should return empty object for unknown provider', () => { + const result = generateProviderEnv('unknown'); + expect(result).toEqual({}); + }); + + it('should generate env for deepseek with API key', () => { + const result = generateProviderEnv('deepseek', 'test-key'); + expect(result).toEqual({ + DEEPSEEK_API_KEY: 'test-key', + DEEPSEEK_BASE_URL: 'https://api.deepseek.com', + LLM_MODEL: 'deepseek-chat', + LLM_PROVIDER: 'deepseek', + }); + }); + + it('should not include base URL for openai', () => { + const result = generateProviderEnv('openai', 'test-key'); + expect(result.OPENAI_API_KEY).toBe('test-key'); + expect(result.OPENAI_BASE_URL).toBeUndefined(); + expect(result.LLM_MODEL).toBe('gpt-4o-mini'); + }); + + it('should handle ollama without API key', () => { + const result = generateProviderEnv('ollama'); + expect(result.LLM_MODEL).toBe('qwen2.5:7b'); + expect(result.LLM_PROVIDER).toBe('ollama'); + // Should not have invalid _BASE_URL key + expect(result['_BASE_URL']).toBeUndefined(); + }); + + it('should not include API key if not provided', () => { + const result = generateProviderEnv('deepseek'); + expect(result.DEEPSEEK_API_KEY).toBeUndefined(); + }); + }); + + describe('getMcpEnvForProvider', () => { + it('should return empty object for unknown provider', () => { + const result = getMcpEnvForProvider('unknown'); + expect(result).toEqual({}); + }); + + it('should include base MCP config', () => { + const result = getMcpEnvForProvider('deepseek', 'test-key'); + expect(result.MEM0_EMBEDDING_MODEL).toBe('bge-m3'); + expect(result.MEM0_EMBEDDING_PROVIDER).toBe('ollama'); + expect(result.OLLAMA_HOST).toBe('http://localhost:11434'); + expect(result.QDRANT_HOST).toBe('localhost'); + expect(result.QDRANT_PORT).toBe('6333'); + }); + + it('should set OPENAI_BASE_URL for non-openai providers', () => { + const result = getMcpEnvForProvider('deepseek', 'test-key'); + expect(result.OPENAI_BASE_URL).toBe('https://api.deepseek.com'); + expect(result.OPENAI_API_KEY).toBe('test-key'); + expect(result.DEEPSEEK_API_KEY).toBe('test-key'); + }); + + it('should not set OPENAI_BASE_URL for openai provider', () => { + const result = getMcpEnvForProvider('openai', 'test-key'); + expect(result.OPENAI_BASE_URL).toBeUndefined(); + expect(result.OPENAI_API_KEY).toBe('test-key'); + }); + + it('should handle ollama without API key', () => { + const result = getMcpEnvForProvider('ollama'); + expect(result.MEM0_EMBEDDING_MODEL).toBe('bge-m3'); + // Should not have invalid keys + expect(result['']).toBeUndefined(); + }); + }); + + describe('validateApiKey', () => { + it('should return valid for unknown provider', async () => { + const result = await validateApiKey('unknown', 'test-key'); + expect(result.valid).toBe(false); + expect(result.error).toBe('未知的 Provider'); + }); + + it('should return valid for ollama without validation', async () => { + const result = await validateApiKey('ollama', ''); + expect(result.valid).toBe(true); + }); + + it('should handle network errors gracefully', async () => { + // Mock fetch to simulate network error + const originalFetch = global.fetch; + global.fetch = vi.fn().mockRejectedValue(new Error('network error')); + + const result = await validateApiKey('deepseek', 'invalid-key'); + expect(result.valid).toBe(false); + expect(result.error).toContain('网络错误'); + + global.fetch = originalFetch; + }); + + it('should handle 401 unauthorized', async () => { + const originalFetch = global.fetch; + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + statusText: 'Unauthorized', + }); + + const result = await validateApiKey('openai', 'invalid-key'); + expect(result.valid).toBe(false); + expect(result.error).toContain('无效或已过期'); + + global.fetch = originalFetch; + }); + + it('should return valid for successful response', async () => { + const originalFetch = global.fetch; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + }); + + const result = await validateApiKey('openai', 'valid-key'); + expect(result.valid).toBe(true); + + global.fetch = originalFetch; + }); + + it('should treat rate limit (429) as valid key', async () => { + const originalFetch = global.fetch; + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 429, + statusText: 'Too Many Requests', + }); + + const result = await validateApiKey('deepseek', 'valid-key'); + expect(result.valid).toBe(true); + + global.fetch = originalFetch; + }); + }); +});