diff --git a/plugins/hermes-zhiyi/__init__.py b/plugins/hermes-zhiyi/__init__.py index e8512f1..01dc089 100644 --- a/plugins/hermes-zhiyi/__init__.py +++ b/plugins/hermes-zhiyi/__init__.py @@ -228,6 +228,33 @@ class ZhiYiClient: logger.warning("ZhiYi search_notes error: %s", e) return [] + def graph_navigate(self, entity: str, max_hops: int = 2, + namespace: Optional[str] = None) -> Dict[str, Any]: + """图谱导航:查询实体的 N 跳关系网络。返回 {entity, count, paths}。""" + try: + payload = {"entity": entity, "max_hops": max_hops} + if namespace: + payload["namespace"] = namespace + r = self._session.post(self._url("/api/v1/graph/navigate"), json=payload, timeout=self.timeout) + if r.status_code == 200: + return r.json() + logger.warning("ZhiYi graph_navigate failed: %s %s", r.status_code, r.text[:200]) + return {} + except Exception as e: + logger.warning("ZhiYi graph_navigate error: %s", e) + return {} + + def graph_stats(self) -> Dict[str, Any]: + """获取图谱统计:节点数、边数、密度。""" + try: + r = self._session.get(self._url("/api/v1/graph/stats"), timeout=self.timeout) + if r.status_code == 200: + return r.json() + return {} + except Exception as e: + logger.warning("ZhiYi graph_stats error: %s", e) + return {} + # ── Memory Provider ────────────────────────────────────────────────────────── @@ -536,6 +563,33 @@ class HermesZhiYiMemoryProvider(MemoryProvider): "properties": {}, }, }, + { + "name": "memory_graph_navigate", + "description": "Navigate the ZhiYi knowledge graph to understand relationships around an entity. Shows N-hop relationship network — what concepts, people, or projects are connected, and how. Use when: analyzing project structure, understanding who/what is related to a topic, exploring context dependencies.", + "parameters": { + "type": "object", + "properties": { + "entity": { + "type": "string", + "description": "The central entity to navigate from (e.g. '牧尘', '织忆', '小唯', 'openclaw'). Chinese and English both work.", + }, + "max_hops": { + "type": "integer", + "description": "Maximum path depth (default: 2). 1-hop = direct neighbors only; 2-hop = friends-of-friends. Larger values return more paths but are slower.", + "default": 2, + }, + }, + "required": ["entity"], + }, + }, + { + "name": "memory_graph_stats", + "description": "Get ZhiYi knowledge graph statistics: total nodes, edges, and graph density. Use to check graph size and health.", + "parameters": { + "type": "object", + "properties": {}, + }, + }, ] def handle_tool_call(self, tool_name: str, args: Dict[str, Any], **kwargs) -> str: @@ -550,6 +604,11 @@ class HermesZhiYiMemoryProvider(MemoryProvider): return self._tool_memory_metrics() elif tool_name == "memory_stats": return self._tool_memory_stats() + elif tool_name == "memory_graph_navigate": + return self._tool_memory_graph_navigate( + args.get("entity", ""), args.get("max_hops", 2)) + elif tool_name == "memory_graph_stats": + return self._tool_memory_graph_stats() return tool_error(tool_name, "Unknown tool") # ── Tool handlers ───────────────────────────────────────────────────────── @@ -617,6 +676,55 @@ class HermesZhiYiMemoryProvider(MemoryProvider): return json.dumps({"success": False, "error": "Could not reach ZhiYi /metrics/self"}) return json.dumps({"success": True, "metrics": metrics}) + def _tool_memory_graph_navigate(self, entity: str, max_hops: int) -> str: + """图谱导航:查询实体的 N 跳关系网络。""" + if not self._client: + return json.dumps({"success": False, "error": "ZhiYi client not initialized"}) + if not entity or len(entity.strip()) < 1: + return json.dumps({"success": False, "error": "entity is required"}) + if max_hops < 1: + max_hops = 1 + if max_hops > 5: + max_hops = 5 + + result = self._client.graph_navigate(entity.strip(), max_hops=max_hops) + if not result: + return json.dumps({"success": False, "error": "Graph navigate failed — check ZhiYi server logs"}) + + # 格式化路径输出,保留关键信息 + paths = result.get("paths", []) + formatted_paths = [] + for p in paths[:50]: # 最多返回50条路径,避免太长 + formatted_paths.append({ + "from": p.get("from", ""), + "relation": p.get("relation", ""), + "to": p.get("to", ""), + "hop": p.get("hop", 1), + "weight": round(p.get("weight", 0), 3), + }) + + return json.dumps({ + "success": True, + "entity": result.get("entity", entity), + "bidirectional": result.get("bidirectional", False), + "count": result.get("count", len(formatted_paths)), + "paths": formatted_paths, + "message": f"Found {result.get('count', 0)} paths within {max_hops}-hop network", + }) + + def _tool_memory_graph_stats(self) -> str: + """获取图谱统计:节点数、边数、密度。""" + if not self._client: + return json.dumps({"success": False, "error": "ZhiYi client not initialized"}) + stats = self._client.graph_stats() + if not stats: + return json.dumps({"success": False, "error": "Could not reach ZhiYi /graph/stats"}) + return json.dumps({ + "success": True, + "stats": stats, + "message": f"Graph has {stats.get('node_count', 0)} nodes and {stats.get('edge_count', 0)} edges (density: {stats.get('density', 0):.4f})", + }) + # ── Session management ──────────────────────────────────────────────────── def on_session_switch(self, new_session_id: str, *, parent_session_id: str = "", reset: bool = False, **kwargs) -> None: @@ -635,10 +743,14 @@ class HermesZhiYiMemoryProvider(MemoryProvider): return ( "\\n[ZhiYi Memory] You have access to ZhiYi MemoryWeave — a semantic memory system with knowledge graph.\\n" "Your prefetch automatically retrieves: (1) relevant semantic memories, (2) Obsidian notes related via graph navigation.\\n" - "Use memory_search to recall past facts, decisions, and context before answering.\\n" - "Use memory_write to save important information the user tells you.\\n" - "Use memory_feedback to mark memories as useful/not-useful — this drives self-optimization.\\n" - "Use memory_metrics to check memory system health (recall hit rate, gap closures, etc.).\\n" + "Tools available:\\n" + " memory_search — semantic search for relevant past information\\n" + " memory_write — save an important fact or preference\\n" + " memory_feedback — mark memories as useful/not-useful (drives self-optimization)\\n" + " memory_metrics — check memory system health (recall hit rate, gap closures, etc.)\\n" + " memory_stats — get memory statistics (total docs, vector dimensions)\\n" + " memory_graph_navigate — navigate the knowledge graph to understand entity relationships (N-hop network)\\n" + " memory_graph_stats — get knowledge graph statistics (nodes, edges, density)\\n" ) diff --git a/plugins/hermes-zhiyi/plugin.yaml b/plugins/hermes-zhiyi/plugin.yaml index 6fc032b..2b6fc18 100644 --- a/plugins/hermes-zhiyi/plugin.yaml +++ b/plugins/hermes-zhiyi/plugin.yaml @@ -1,6 +1,6 @@ name: hermes-zhiyi -version: 1.0.0 -description: Bridge to ZhiYi MemoryWeave — bge-m3 1024-dim semantic memory with FAISS + bge-reranker-v2-m3. Replaces local lanceDB with shared ZhiYi server. +version: 1.1.0 +description: "Bridge to ZhiYi MemoryWeave - bge-m3 1024-dim semantic memory with FAISS + bge-reranker. Replaces local lanceDB with shared ZhiYi server. Now includes knowledge graph tools: memory_graph_navigate and memory_graph_stats." provider: zhiyi entry: __init__.register memory_provider: true \ No newline at end of file