diff --git a/.claude/hooks/session_breath.py b/.claude/hooks/session_breath.py index e412453..9074c3c 100644 --- a/.claude/hooks/session_breath.py +++ b/.claude/hooks/session_breath.py @@ -15,7 +15,6 @@ # OMBRE_HOOK_SKIP — set to "1" to disable the hook temporarily # ============================================================ -import json import os import sys import urllib.request @@ -28,35 +27,18 @@ def main(): base_url = os.environ.get("OMBRE_HOOK_URL", "http://localhost:8000").rstrip("/") - # Build MCP call via HTTP POST to the streamable-http endpoint - # The breath tool with no query triggers surfacing mode. - payload = json.dumps({ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/call", - "params": { - "name": "breath", - "arguments": {"query": "", "max_results": 2} - } - }).encode("utf-8") - req = urllib.request.Request( - f"{base_url}/mcp", - data=payload, - headers={"Content-Type": "application/json"}, - method="POST", + f"{base_url}/breath-hook", + headers={"Accept": "text/plain"}, + method="GET", ) try: with urllib.request.urlopen(req, timeout=8) as response: raw = response.read().decode("utf-8") - data = json.loads(raw) - # Extract text from MCP tool result - result_content = data.get("result", {}).get("content", []) - text_parts = [c.get("text", "") for c in result_content if c.get("type") == "text"] - output = "\n".join(text_parts).strip() - if output and output != "权重池平静,没有需要处理的记忆。": - print(f"[Ombre Brain - 记忆浮现]\n{output}") + output = raw.strip() + if output: + print(output) except (urllib.error.URLError, OSError): # Server not available (local stdio mode or not running) — silent fail pass diff --git a/.claude/settings.json b/.claude/settings.json index 3c34cf8..24a3553 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,7 +7,7 @@ { "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/session_breath.py\"", - "shell": "powershell", + "shell": "/bin/bash", "timeout": 12, "statusMessage": "Ombre Brain 正在浮现记忆..." } @@ -19,7 +19,7 @@ { "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/session_breath.py\"", - "shell": "powershell", + "shell": "/bin/bash", "timeout": 12, "statusMessage": "Ombre Brain 正在浮现记忆..." } diff --git a/README.md b/README.md index 5718ca1..5217017 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,8 @@ Claude ←→ MCP Protocol ←→ server.py ### 步骤 / Steps ```bash -git clone https://github.com/P0lar1zzZ/Ombre-Brain.git -cd Ombre-Brain +git clone https://git.p0lar1s.uk/P0lar1s/Ombre_Brain.git +cd Ombre_Brain python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate diff --git a/bucket_manager.py b/bucket_manager.py index d056f4e..4664080 100644 --- a/bucket_manager.py +++ b/bucket_manager.py @@ -87,8 +87,9 @@ class BucketManager: scoring = config.get("scoring_weights", {}) self.w_topic = scoring.get("topic_relevance", 4.0) self.w_emotion = scoring.get("emotion_resonance", 2.0) - self.w_time = scoring.get("time_proximity", 1.5) + self.w_time = scoring.get("time_proximity", 2.5) self.w_importance = scoring.get("importance", 1.0) + self.content_weight = scoring.get("content_weight", 3.0) # Added to allow better content-based matching during merge # --------------------------------------------------------- # Create a new bucket @@ -573,9 +574,9 @@ class BucketManager: ) * 2 ) - content_score = fuzz.partial_ratio(query, bucket.get("content", "")[:500]) * 1 + content_score = fuzz.partial_ratio(query, bucket.get("content", "")[:1000]) * self.content_weight - return (name_score + domain_score + tag_score + content_score) / (100 * 8.5) + return (name_score + domain_score + tag_score + content_score) / (100 * 10.5) # --------------------------------------------------------- # Emotion resonance sub-score: @@ -619,7 +620,7 @@ class BucketManager: days = max(0.0, (datetime.now() - last_active).total_seconds() / 86400) except (ValueError, TypeError): days = 30 - return math.exp(-0.02 * days) + return math.exp(-0.1 * days) # --------------------------------------------------------- # List all buckets diff --git a/check_buckets.py b/check_buckets.py new file mode 100644 index 0000000..5e649c1 --- /dev/null +++ b/check_buckets.py @@ -0,0 +1,30 @@ +import asyncio +from bucket_manager import BucketManager +from utils import load_config + +async def main(): + config = load_config() + bm = BucketManager(config) + buckets = await bm.list_all(include_archive=True) + + print(f"Total buckets: {len(buckets)}") + + domains = {} + for b in buckets: + for d in b.get("metadata", {}).get("domain", []): + domains[d] = domains.get(d, 0) + 1 + + print(f"Domains: {domains}") + + # Check for formatting issues (e.g., missing critical fields) + issues = 0 + for b in buckets: + meta = b.get("metadata", {}) + if not meta.get("name") or not meta.get("domain") or not b.get("content"): + print(f"Format issue in {b['id']}") + issues += 1 + + print(f"Found {issues} formatting issues.") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/dehydrator.py b/dehydrator.py index ccb8944..2db6132 100644 --- a/dehydrator.py +++ b/dehydrator.py @@ -193,21 +193,8 @@ class Dehydrator: if count_tokens_approx(content) < 100: return self._format_output(content, metadata) - # --- Try API compression first (best quality) --- - # --- 优先尝试 API 压缩 --- - if self.api_available: - try: - result = await self._api_dehydrate(content) - if result: - return self._format_output(result, metadata) - except Exception as e: - logger.warning( - f"API dehydration failed, degrading to local / " - f"API 脱水失败,降级到本地压缩: {e}" - ) - - # --- Local compression fallback (works without API) --- - # --- 本地压缩兜底 --- + # --- Local compression (Always used as requested) --- + # --- 本地压缩 --- result = self._local_dehydrate(content) return self._format_output(result, metadata) @@ -322,9 +309,10 @@ class Dehydrator: # --- Top-8 sentences + keyword list / 取高分句 + 关键词列表 --- selected = [s for _, s in scored[:8]] summary = "。".join(selected) - keyword_str = ", ".join(keywords[:10]) - - return f"[摘要] {summary}\n[关键词] {keyword_str}" + + if len(summary) > 1000: + summary = summary[:1000] + "…" + return summary # --------------------------------------------------------- # Local merge (simple concatenation + truncation) @@ -393,7 +381,6 @@ class Dehydrator: header = "" if metadata and isinstance(metadata, dict): name = metadata.get("name", "未命名") - tags = ", ".join(metadata.get("tags", [])) domains = ", ".join(metadata.get("domain", [])) try: valence = float(metadata.get("valence", 0.5)) @@ -403,10 +390,10 @@ class Dehydrator: header = f"📌 记忆桶: {name}" if domains: header += f" [主题:{domains}]" - if tags: - header += f" [标签:{tags}]" header += f" [情感:V{valence:.1f}/A{arousal:.1f}]" header += "\n" + + content = re.sub(r'\[\[([^\]]+)\]\]', r'\1', content) return f"{header}{content}" # --------------------------------------------------------- diff --git a/server.py b/server.py index b3b69b4..124d114 100644 --- a/server.py +++ b/server.py @@ -89,6 +89,43 @@ async def health_check(request): return JSONResponse({"status": "error", "detail": str(e)}, status_code=500) +# ============================================================= +# /breath-hook endpoint: Dedicated hook for SessionStart +# 会话启动专用挂载点 +# ============================================================= +@mcp.custom_route("/breath-hook", methods=["GET"]) +async def breath_hook(request): + from starlette.responses import PlainTextResponse + try: + all_buckets = await bucket_mgr.list_all(include_archive=False) + # pinned + pinned = [b for b in all_buckets if b["metadata"].get("pinned") or b["metadata"].get("protected")] + # top 2 unresolved by score + unresolved = [b for b in all_buckets + if not b["metadata"].get("resolved", False) + and b["metadata"].get("type") != "permanent" + and not b["metadata"].get("pinned") + and not b["metadata"].get("protected")] + scored = sorted(unresolved, key=lambda b: decay_engine.calculate_score(b["metadata"]), reverse=True) + top = scored[:2] + + parts = [] + for b in pinned: + summary = await dehydrator.dehydrate(b["content"], {k: v for k, v in b["metadata"].items() if k != "tags"}) + parts.append(f"📌 [核心准则] {summary}") + for b in top: + summary = await dehydrator.dehydrate(b["content"], {k: v for k, v in b["metadata"].items() if k != "tags"}) + await bucket_mgr.touch(b["id"]) + parts.append(summary) + + if not parts: + return PlainTextResponse("") + return PlainTextResponse("[Ombre Brain - 记忆浮现]\n" + "\n---\n".join(parts)) + except Exception as e: + logger.warning(f"Breath hook failed: {e}") + return PlainTextResponse("") + + # ============================================================= # Internal helper: merge-or-create # 内部辅助:检查是否可合并,可以则合并,否则新建 @@ -111,7 +148,7 @@ async def _merge_or_create( 返回 (桶ID或名称, 是否合并)。 """ try: - existing = await bucket_mgr.search(content, limit=1) + existing = await bucket_mgr.search(content, limit=1, domain_filter=domain or None) except Exception as e: logger.warning(f"Search for merge failed, creating new / 合并搜索失败,新建: {e}") existing = [] @@ -186,7 +223,8 @@ async def breath( pinned_results = [] for b in pinned_buckets: try: - summary = await dehydrator.dehydrate(b["content"], b["metadata"]) + clean_meta = {k: v for k, v in b["metadata"].items() if k != "tags"} + summary = await dehydrator.dehydrate(b["content"], clean_meta) pinned_results.append(f"📌 [核心准则] {summary}") except Exception as e: logger.warning(f"Failed to dehydrate pinned bucket / 钉选桶脱水失败: {e}") @@ -211,7 +249,8 @@ async def breath( dynamic_results = [] for b in top: try: - summary = await dehydrator.dehydrate(b["content"], b["metadata"]) + clean_meta = {k: v for k, v in b["metadata"].items() if k != "tags"} + summary = await dehydrator.dehydrate(b["content"], clean_meta) await bucket_mgr.touch(b["id"]) score = decay_engine.calculate_score(b["metadata"]) dynamic_results.append(f"[权重:{score:.2f}] {summary}") @@ -249,7 +288,8 @@ async def breath( results = [] for bucket in matches: try: - summary = await dehydrator.dehydrate(bucket["content"], bucket["metadata"]) + clean_meta = {k: v for k, v in bucket["metadata"].items() if k != "tags"} + summary = await dehydrator.dehydrate(bucket["content"], clean_meta) await bucket_mgr.touch(bucket["id"]) results.append(summary) except Exception as e: @@ -271,7 +311,8 @@ async def breath( drifted = random.sample(low_weight, min(random.randint(1, 3), len(low_weight))) drift_results = [] for b in drifted: - summary = await dehydrator.dehydrate(b["content"], b["metadata"]) + clean_meta = {k: v for k, v in b["metadata"].items() if k != "tags"} + summary = await dehydrator.dehydrate(b["content"], clean_meta) drift_results.append(f"[surface_type: random]\n{summary}") results.append("--- 忽然想起来 ---\n" + "\n---\n".join(drift_results)) except Exception as e: