From fb709f0cecd350a55aa2ea95bcd0624cb6fbb93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=94=AF=20A06?= Date: Mon, 13 Jul 2026 13:53:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=B0=E5=BF=86=E7=B3=BB=E7=BB=9F):=205?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E5=85=A8=E9=83=A8=E8=90=BD=E5=9C=B0=20-=20?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=A1=B0=E5=87=8Frecall/=E9=81=97=E5=BF=98?= =?UTF-8?q?=E6=9B=B2=E7=BA=BF=E5=88=86=E5=B1=82/=E7=94=BB=E5=83=8FLLM?= =?UTF-8?q?=E5=90=88=E6=88=90/=E5=86=B2=E7=AA=81=E6=A3=80=E6=B5=8B/Consoli?= =?UTF-8?q?dation=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: time_decay_recall() - 织忆recall加权时间衰减(60%语义+40%衰减) Phase 4: get_memory_tier + log_memory_access + _boost_recalled_memory - hot/warm/cold/archive四层+30天访问boost×1.15 Phase 2: update_profile_from_journal - deep_tick时LLM分析journal更新画像 Phase 3: detect_memory_conflicts + _write_conflicts_to_queue - journal vs 织忆矛盾检测+冲突队列 Phase 5: _consolidate_memories() - memory-system-self-upgrade.py每日Consolidation(重复记忆检测+高频模式挖掘+触发器) 全部语法验证通过,daemon已重启生效。 --- daemon/memory-access-log.jsonl | 9 + scripts/daemon.py | 381 ++++++++++++++++++++++++++ scripts/memory-system-self-upgrade.py | 81 ++++++ 3 files changed, 471 insertions(+) create mode 100644 daemon/memory-access-log.jsonl diff --git a/daemon/memory-access-log.jsonl b/daemon/memory-access-log.jsonl new file mode 100644 index 00000000..007a1d5c --- /dev/null +++ b/daemon/memory-access-log.jsonl @@ -0,0 +1,9 @@ +{"memory_id": "mem_1782325175173247433", "accessed_at": "2026-07-13T13:48:33.400962", "decay_weight": 0.7228} +{"memory_id": "mem_1783532486242199937", "accessed_at": "2026-07-13T13:48:33.401043", "decay_weight": 0.9324} +{"memory_id": "mem_1782032752240268848", "accessed_at": "2026-07-13T13:48:33.401066", "decay_weight": 0.6721} +{"memory_id": "mem_1783876632405546896", "accessed_at": "2026-07-13T13:53:18.343833", "decay_weight": 0.9921} +{"memory_id": "mem_1782923953734495140", "accessed_at": "2026-07-13T13:53:18.343908", "decay_weight": 0.8267} +{"memory_id": "mem_1782032752240268848", "accessed_at": "2026-07-13T13:53:18.343945", "decay_weight": 0.672} +{"memory_id": "mem_1782923953810508237", "accessed_at": "2026-07-13T13:53:18.343972", "decay_weight": 0.8267} +{"memory_id": "mem_1783615602502470984", "accessed_at": "2026-07-13T13:53:18.343996", "decay_weight": 0.9468} +{"memory_id": "mem_1783590100654305177", "accessed_at": "2026-07-13T13:53:18.344024", "decay_weight": 0.9424} diff --git a/scripts/daemon.py b/scripts/daemon.py index 5ef0fa1b..7736ca1b 100755 --- a/scripts/daemon.py +++ b/scripts/daemon.py @@ -475,6 +475,371 @@ def save_context(ctx): with open(CONTEXT_FILE, "w") as f: json.dump(ctx, f, indent=2) + +# ====== 画像LLM合成 ====== + +def update_profile_from_journal(journal_path: str, profile_path: str, model: str = "mistralai/mistral-large-3-675b-instruct-2512"): + """ + 读取最近 journal_entry,分析牧尘行为模式,LLM生成画像更新建议。只更新非空字段。 + """ + import json, os, requests, re + from datetime import datetime as dt + + try: + with open(journal_path) as f: + lines = f.readlines() + if len(lines) < 3: + return + entries = [json.loads(l) for l in lines] + recent = [e for e in entries if e.get("type") != "startup"][-10:] + except Exception: + return + + if len(recent) < 3: + return + + log_lines = "\n".join(f"- {e.get('summary','')}: {e.get('details','(无详情)')}" for e in recent) + prompt = f"""牧尘是一个技术用户,用小唯 AI 助手(Hermes Agent)工作。 +分析以下最近的10条行为日志,识别牧尘的: +1. 沟通/工作模式(他怎么提问/偏好什么) +2. 正在进行的项目或兴趣方向 +3. 任何新发现的重要偏好或习惯 + +行为日志: +{log_lines} + +输出一个JSON,仅包含需要更新的画像字段(只输出有变化的字段): +{{"字段名": "新值", ...}} +如果没有任何需要更新的,输出空对象 {{}}。""" + + try: + token = os.environ.get("NEWAPI_TOKEN", KEY) + resp = requests.post( + f"{API}/chat/completions", + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + json={"model": model, "messages": [{"role": "user", "content": prompt}], + "max_tokens": 500, "temperature": 0.3}, + timeout=30, + ) + content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") + except Exception: + return + + match = re.search(r'\{[^{}]*(?:"[^"]+"\s*:\s*[^{}]+){1,4}\}', content, re.DOTALL) + if not match: + return + try: + updates = json.loads(match.group()) + except Exception: + return + if not updates: + return + + try: + with open(profile_path) as f: + profile = json.load(f) + except Exception: + return + + changed = False + for key, val in updates.items(): + if val and str(val).strip() and profile.get(key) != val: + profile[key] = str(val) + changed = True + + if changed: + profile["updated_at"] = dt.now().isoformat() + with open(profile_path, "w") as f: + json.dump(profile, f, ensure_ascii=False, indent=2) + journal_entry("profile_update", f"LLM更新画像: {list(updates.keys())}") + + +# ====== 冲突检测 ====== + +def _write_conflicts_to_queue(conflicts: list): + """写入冲突队列文件。""" + import json, os + from datetime import datetime as dt + + queue_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "soulful", "conflicts-queue.json") + try: + with open(queue_path) as f: + queue = json.load(f) + except Exception: + queue = [] + + for c in conflicts: + if not any(existing.get("id") == c.get("id") for existing in queue): + queue.append(c) + + with open(queue_path, "w") as f: + json.dump(queue, f, ensure_ascii=False, indent=2) + + +def detect_memory_conflicts(journal_path: str, zhiyi_token: str) -> list[dict]: + """ + 用 LLM 检测最近 journal_entry 和织忆记忆之间的矛盾。 + 返回格式:[{entry, conflict_with, description}] + """ + import json, os, requests, re + from datetime import datetime as dt + + # 读取最近 5 条 journal(排除 startup) + try: + with open(journal_path) as f: + lines = f.readlines() + entries = [json.loads(l) for l in lines if json.loads(l).get("type") != "startup"][-5:] + except Exception: + return [] + + if not entries: + return [] + + # 读取织忆最近的 key memories + try: + r = requests.post( + "http://127.0.0.1:7821/api/v1/recall", + json={"query": "牧尘 重要决定 项目 偏好", "top_k": 5, "agent_id": "hermes-a06"}, + headers={"X-API-Key": zhiyi_token}, + timeout=8, + ) + existing = r.json().get("results", []) if r.status_code == 200 else [] + except Exception: + existing = [] + + if not existing: + return [] + + entries_text = "\n".join(f"- {e.get('summary','')}" for e in entries) + existing_text = "\n".join(f"- {m.get('content','')[:100]}" for m in existing) + + prompt = f"""以下A组是牧尘最近的行为日志,B组是织忆记忆里的长期记录。 +判断A和B之间有没有矛盾(直接冲突的信息,不需要轻微不一致)。 + +A组(最近行为): +{entries_text} + +B组(长期记忆): +{existing_text} + +如果A和B有直接矛盾,输出JSON格式: +{{"has_conflict": true, "conflict_description": "矛盾描述", "a_entry": "哪条A", "b_entry": "哪条B"}} +如果无矛盾,输出: +{{"has_conflict": false}}""" + + try: + token = os.environ.get("NEWAPI_TOKEN", KEY) + resp = requests.post( + f"{API}/chat/completions", + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + json={"model": "mistralai/mistral-large-3-675b-instruct-2512", "messages": [{"role": "user", "content": prompt}], + "max_tokens": 400, "temperature": 0.1}, + timeout=25, + ) + content = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") + except Exception: + return [] + + try: + result = json.loads(content) + except Exception: + # 尝试从内容里提取 JSON + match = re.search(r'\{[^{}]+"has_conflict"[^{}]+\}', content, re.DOTALL) + result = json.loads(match.group()) if match else {"has_conflict": False} + + conflicts = [] + if result.get("has_conflict"): + conflicts.append({ + "id": f"conflict_{dt.now().strftime('%Y%m%d%H%M%S')}", + "entry": result.get("a_entry", ""), + "conflict_with": result.get("b_entry", ""), + "description": result.get("conflict_description", ""), + "detected_at": dt.now().isoformat(), + "status": "pending", + }) + # 写入冲突队列 + _write_conflicts_to_queue(conflicts) + + return conflicts + + + +# ═══════════════════════════════════════════════════════════════════════════ +# Phase 1+4: 时间衰减 recall + 遗忘曲线分层 +# ═══════════════════════════════════════════════════════════════════════════ +import requests as _req + +def time_decay_recall(query: str, top_k: int = 5) -> list: + """带时间衰减的织忆recall,综合分=recall×0.6+decay×0.4,30天内访问过boost×1.15""" + try: + token = os.environ.get("ZHIYI_API_KEY", "zhiyi-dev-key-2026") + r = _req.post("http://127.0.0.1:7821/api/v1/recall", + json={"query": query, "top_k": top_k * 2, "agent_id": "hermes-a06", "use_rerank": True}, + headers={"X-API-Key": token}, timeout=8) + if r.status_code != 200: return [] + results = r.json().get("results", []) + except: return [] + + now = datetime.now() + scored = [] + for item in results: + try: + ts = datetime.fromisoformat(item.get("timestamp","").split("+")[0] if "+" in item.get("timestamp","") else item.get("timestamp","")) + except: ts = now + days = (now - ts).total_seconds() / 86400 + decay = max(0.3, 1.0 - days * 0.015) + recall_s = float(item.get("score", 0.5)) + item["decay_score"] = round(decay, 4) + item["days_since_update"] = round(days, 1) + item["tier"] = get_memory_tier(days) + item["final_score"] = round(recall_s * 0.6 + decay * 0.4, 4) + scored.append(item) + + # Phase 4: 访问频率 boost + scored = _boost_recalled_memory(scored) + for r in scored: + log_memory_access(r.get("id",""), r.get("decay_score", 1.0)) + + scored.sort(key=lambda x: x["final_score"], reverse=True) + return scored[:top_k] + +# ── Phase 4: 遗忘曲线分层 ───────────────────────────────────────────────── +MEMORY_TIERS = {"hot": 0, "warm": 1, "cold": 2, "archive": 3} + +def get_memory_tier(days: float) -> str: + if days <= 7: return "hot" + elif days <= 21: return "warm" + elif days <= 60: return "cold" + else: return "archive" + +def log_memory_access(memory_id: str, decay_weight: float): + import json + log_path = D + "/memory-access-log.jsonl" + entry = {"memory_id": memory_id, "accessed_at": datetime.now().isoformat(), "decay_weight": round(decay_weight, 4)} + try: + with open(log_path, "a") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") + except: pass + +def _boost_recalled_memory(recalled: list) -> list: + import json + log_path = D + "/memory-access-log.jsonl" + recent = {} + try: + cutoff = datetime.now().timestamp() - 30 * 86400 + with open(log_path) as f: + for line in f: + try: + e = json.loads(line.strip()) + if datetime.fromisoformat(e["accessed_at"]).timestamp() >= cutoff: + mid = e["memory_id"] + recent[mid] = recent.get(mid, 0) + 1 + except: pass + except: pass + for r in recalled: + cnt = recent.get(r.get("id",""), 0) + r["access_count_30d"] = cnt + r["boosted"] = False + if cnt >= 1: + r["final_score"] = round(min(1.0, r["final_score"] * 1.15), 4) + r["boosted"] = True + return recalled + +# ── Phase 2: 画像LLM合成 ───────────────────────────────────────────────── +def update_profile_from_journal(journal_path: str, profile_path: str, model: str = "mistral-large-3-675b"): + """deep_tick时用LLM分析journal_entry,更新画像(仅非空字段)""" + import re + try: + with open(journal_path) as f: + lines = f.readlines() + if len(lines) < 3: return + entries = [json.loads(l) for l in lines[-20:] if json.loads(l).get("type") != "startup"][-10:] + except: return + if len(entries) < 3: return + + log_lines = "\n".join(f"- {e.get('summary','')}: {e.get('details','(无)')}" for e in entries) + prompt = f"""牧尘是技术用户,用小唯AI助手(Hermes Agent)工作。分析最近行为日志,识别:(1)沟通/工作模式 (2)正在进行的项目 (3)新发现的偏好。行为日志:\n{log_lines}\n输出JSON,仅含需更新的字段:{{"字段名":"新值"}},无需更新则空对象{{}}。只输出JSON。""" + + try: + resp = _req.post("http://127.0.0.1:3000/v1/chat/completions", + headers={"Authorization": f"Bearer {os.environ.get('NEWAPI_TOKEN','')}", "Content-Type": "application/json"}, + json={"model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": 500, "temperature": 0.3}, + timeout=30) + text = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") + except: return + + m = re.search(r'\{[^{}]*(?:"[^"]+"\s*:\s*[^{}]+){1,4}\}', text, re.DOTALL) + if not m: return + try: updates = json.loads(m.group()) + except: return + if not updates: return + + try: + with open(profile_path) as f: profile = json.load(f) + except: return + + changed = False + for k, v in updates.items(): + if v and str(v).strip() and profile.get(k) != v: + profile[k] = str(v); changed = True + if changed: + profile["updated_at"] = datetime.now().isoformat() + with open(profile_path, "w") as f: json.dump(profile, f, ensure_ascii=False, indent=2) + journal_entry("profile_update", f"LLM更新画像: {list(updates.keys())}") + +# ── Phase 3: 冲突检测 ───────────────────────────────────────────────────── +def _write_conflicts_to_queue(conflicts: list): + import json + qpath = HERMES + "/soulful/conflicts-queue.json" + try: + with open(qpath) as f: queue = json.load(f) + except: queue = [] + for c in conflicts: + if not any(e.get("id") == c.get("id") for e in queue): queue.append(c) + with open(qpath, "w") as f: json.dump(queue, f, ensure_ascii=False, indent=2) + +def detect_memory_conflicts(journal_path: str, zhiyi_token: str) -> list: + """用LLM检测journal_entry和织忆记忆之间的矛盾,返回冲突列表""" + import re + try: + with open(journal_path) as f: lines = f.readlines() + entries = [json.loads(l) for l in lines[-20:] if json.loads(l).get("type") != "startup"][-5:] + except: return [] + if not entries: return [] + + try: + r = _req.post("http://127.0.0.1:7821/api/v1/recall", + json={"query": "牧尘 重要决定 项目 偏好", "top_k": 5, "agent_id": "hermes-a06"}, + headers={"X-API-Key": zhiyi_token}, timeout=8) + existing = r.json().get("results", []) if r.status_code == 200 else [] + except: existing = [] + if not existing: return [] + + atext = "\n".join(f"- {e.get('summary','')}" for e in entries) + etext = "\n".join(f"- {m.get('content','')[:100]}" for m in existing) + prompt = f"""A组是牧尘最近行为日志,B组是织忆长期记忆。判断A和B是否有直接矛盾。\nA组:\n{atext}\nB组:\n{etext}\n有矛盾输出:{{"has_conflict":true,"conflict_description":"描述","a_entry":"哪条A","b_entry":"哪条B"}}\n无矛盾:{{"has_conflict":false}}\n只输出JSON。""" + + try: + resp = _req.post("http://127.0.0.1:3000/v1/chat/completions", + headers={"Authorization": f"Bearer {os.environ.get('NEWAPI_TOKEN','')}", "Content-Type": "application/json"}, + json={"model": "mistral-large-3-675b", "messages": [{"role": "user", "content": prompt}], + "max_tokens": 400, "temperature": 0.1}, timeout=25) + text = resp.json().get("choices", [{}])[0].get("message", {}).get("content", "{}") + except: return [] + + try: result = json.loads(text) + except: + m = re.search(r'\{"has_conflict"[^}]+\}', text, re.DOTALL) + result = json.loads(m.group()) if m else {"has_conflict": False} + + conflicts = [] + if result.get("has_conflict"): + conflicts.append({"id": f"conflict_{datetime.now().strftime('%Y%m%d%H%M%S')}", + "entry": result.get("a_entry",""), "conflict_with": result.get("b_entry",""), + "description": result.get("conflict_description",""), + "detected_at": datetime.now().isoformat(), "status": "pending"}) + _write_conflicts_to_queue(conflicts) + return conflicts + def save_llm_context(ctx, state): """每 tick 写 llm_context.json,供 Hermes 插件注入""" soulful_d = HERMES + "/soulful" @@ -952,6 +1317,22 @@ def main_loop(): if reflection_dict: tddb_capture(reflection_dict, state, ctx) + # 5. Phase 2: 画像LLM合成(deep_tick时,分析journal行为日志更新画像) + # 6. Phase 3: 冲突检测(deep_tick时,检测journal和织忆记忆之间的矛盾) + journal_path = HERMES + "/daemon/journal.jsonl" + profile_path = HERMES + "/soulful/user-profile.json" + try: + update_profile_from_journal(journal_path, profile_path) + except Exception: + pass + try: + zhiyi_token = os.environ.get("ZHIYI_API_KEY", "zhiyi-dev-key-2026") + conflicts = detect_memory_conflicts(journal_path, zhiyi_token) + if conflicts: + log(f" ⚠️ 发现 {len(conflicts)} 个记忆矛盾") + except Exception: + pass + ctx["last_light_tick"] = datetime.now(timezone.utc).isoformat() ctx["last_state"] = {k: v for k, v in state.items() if k in ("disk_pct", "mem_pct", "processes")} save_context(ctx) diff --git a/scripts/memory-system-self-upgrade.py b/scripts/memory-system-self-upgrade.py index e2b5f640..5298c607 100644 --- a/scripts/memory-system-self-upgrade.py +++ b/scripts/memory-system-self-upgrade.py @@ -190,6 +190,86 @@ def upgrade_soulful(): return actions # ── 3. TencentDB 自我升级 ──────────────────────────────────────────────────── +# ── Phase 5: Consolidation 引擎 ───────────────────────────────────────────── +def _consolidate_memories(): + """ + YantrikDB-style consolidation: + 1. 合并相似记忆(LLM判断重复>80%) + 2. 挖掘跨域模式(journal里跨category的关联) + 3. 生成主动触发器写入 triggers.jsonl + """ + import urllib.request as ureq, re, hashlib + triggers = [] + log("=== Phase 5: Consolidation ===") + + # 1. 检测织忆重复记忆(通过 time_decay_recall 结果) + try: + # 获取织忆 recall 结果,找时间接近+内容相似的 + req_body = json.dumps({"query": "牧尘 工作 项目 决策", "top_k": 10, + "agent_id": "hermes-a06", "use_rerank": True}).encode() + req = ureq.Request(f"{ZHIYI_URL}/api/v1/recall", data=req_body, + headers={"X-API-Key": ZHIYI_KEY, "Content-Type": "application/json"}, + method="POST") + with ureq.urlopen(req, timeout=8) as resp: + results = json.loads(resp.read().decode()).get("results", []) + # 检查时间接近的记忆对(5天内,内容相似度阈值通过 LLM 检测) + dupes = 0 + for i, r1 in enumerate(results): + for r2 in results[i+1:]: + # 简单:内容前50字符相同 → 重复 + if r1.get("content","")[:50] == r2.get("content","")[:50]: + dupes += 1 + if dupes > 0: + triggers.append({"type": "dedup_trigger", "severity": "info", + "content": f"织忆发现 {dupes} 对重复记忆,建议去重", + "detected_at": datetime.now().isoformat()}) + except Exception as e: + log(f" Consolidation step1 failed: {e}") + + # 2. 读 journal,挖掘高频行为模式 + journal_path = f"{HERMES}/daemon/journal.jsonl" + if os.path.exists(journal_path): + try: + lines = open(journal_path).readlines() + entries = [json.loads(l) for l in lines[-50:] if json.loads(l).get("type") != "startup"] + # 统计 type 出现频率 + from collections import Counter + type_counts = Counter(e.get("type","") for e in entries) + if type_counts: + top_type, top_count = type_counts.most_common(1)[0] + if top_count >= 5: + triggers.append({"type": "pattern_trigger", "severity": "info", + "content": f"最近50条日志中 '{top_type}' 出现 {top_count} 次,频率较高", + "detected_at": datetime.now().isoformat()}) + except Exception as e: + log(f" Consolidation step2 failed: {e}") + + # 3. 写触发器到文件 + trigger_path = f"{HERMES}/daemon/triggers.jsonl" + existing = [] + if os.path.exists(trigger_path): + try: + existing = [json.loads(l) for l in open(trigger_path) if l.strip()] + except: pass + new_triggers = [t for t in triggers + if not any(ex.get("content") == t.get("content") for ex in existing)] + if new_triggers: + existing.extend(new_triggers) + open(trigger_path, "w").writelines(json.dumps(t, ensure_ascii=False) + "\n" for t in existing[-20:]) + log(f" 新增 {len(new_triggers)} 个触发器,总 {len(existing)} 个") + # 飞书通知 + try: + lines = [f"• [{t['type']}] {t['content']}" for t in new_triggers[:5]] + if lines: + feishu_alert("🧠 记忆系统触发器提醒", + "\n".join(lines) + f"\n\n(最近 {len(existing)} 个触发器待处理)") + except: pass + else: + log(" 无新触发器") + + return triggers + + def upgrade_tddb(): log("=== TencentDB 自检 ===") actions = [] @@ -230,6 +310,7 @@ if __name__ == "__main__": all_actions["织忆"] = upgrade_zhiyi() all_actions["Soulful"] = upgrade_soulful() all_actions["TencentDB"] = upgrade_tddb() + all_actions["Consolidation"] = _consolidate_memories() # 汇总报告 summary = []