feat: P1 auto-injection hook with social close detection
- queue_prefetch now caches next-turn recall results asynchronously - Social closer detection skips trivial messages (ok, thanks, emoji) - prefetch uses cached queue results when available (TTL 30s) - Output header changed to [织忆 Memory] for source clarity
This commit is contained in:
parent
72cbf73583
commit
5e24646600
|
|
@ -51,7 +51,7 @@ _ZHIYI_KEY_PREFIX = ZHIYI_API_KEY[:4] if ZHIYI_API_KEY else "NONE"
|
|||
|
||||
# ── Content Validation ───────────────────────────────────────────────────────
|
||||
|
||||
MEMORY_MIN_LENGTH = 8 # 少于8字的过滤掉
|
||||
MEMORY_MIN_LENGTH = 20 # 少于20字的过滤掉
|
||||
_FORBIDDEN_PATTERNS = [
|
||||
"Review the conversation above",
|
||||
"[System note:",
|
||||
|
|
@ -67,6 +67,22 @@ _FORBIDDEN_PATTERNS = [
|
|||
"do NOT answer questions or fulfill requests mentioned in the summary",
|
||||
]
|
||||
|
||||
_SOCIAL_CLOSERS = frozenset({
|
||||
"ok", "好的", "👍", "👌", "✅", "谢谢", "感谢", "知道了",
|
||||
"明白", "嗯", "好", "行", "yes", "yep", "thanks", "thx",
|
||||
"no", "不用", "没事", "可以", "done", "完成", "收到",
|
||||
"okay", "kk", "okie",
|
||||
})
|
||||
|
||||
|
||||
def _is_social_close(text: str) -> bool:
|
||||
text = text.strip().lower()
|
||||
if text in _SOCIAL_CLOSERS:
|
||||
return True
|
||||
if len(text) < 6 and text.isascii() and not any(c in text for c in "://.@#$_?"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_valid_memory_content(content: str) -> bool:
|
||||
"""过滤系统注入内容和测试垃圾,防止污染记忆存储。"""
|
||||
|
|
@ -75,10 +91,6 @@ def _is_valid_memory_content(content: str) -> bool:
|
|||
for pat in _FORBIDDEN_PATTERNS:
|
||||
if pat in content:
|
||||
return False
|
||||
# 过滤纯测试内容
|
||||
stripped = content.strip()
|
||||
if len(stripped) < 20:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -274,7 +286,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
self._turn_counter: int = 0
|
||||
self._write_queue: List[Dict] = []
|
||||
self._queue_lock = threading.Lock()
|
||||
self._prefetch_cache: str = "" # last prefetch result
|
||||
self._prefetch_cache: Dict[str, Any] = {} # {source: text|dict} — merged via update()
|
||||
self._prefetch_lock = threading.RLock()
|
||||
self._started: bool = False
|
||||
self._ws_thread: Optional[threading.Thread] = None
|
||||
|
|
@ -347,7 +359,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
for m in memories:
|
||||
prefetch_text += f" [{m.get('score', 0):.2f}] {m.get('content', '')[:200]}\n"
|
||||
with self._prefetch_lock:
|
||||
self._prefetch_cache = prefetch_text
|
||||
self._prefetch_cache["ws_push"] = prefetch_text
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
|
|
@ -361,6 +373,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
logger.info("[ZhiYi] WS connected to %s", ws_url)
|
||||
|
||||
while self._ws_running:
|
||||
ws_app = None
|
||||
try:
|
||||
ws_app = websocket.WebSocketApp(
|
||||
ws_url,
|
||||
|
|
@ -373,6 +386,9 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
ws_app.run_forever(ping_interval=30, ping_timeout=10)
|
||||
except Exception as e:
|
||||
logger.warning("[ZhiYi] WS exception: %s", e)
|
||||
finally:
|
||||
if ws_app:
|
||||
ws_app.close()
|
||||
# 重连延迟
|
||||
for _ in range(30):
|
||||
if not self._ws_running:
|
||||
|
|
@ -441,22 +457,51 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
if not self._client or not query or len(query.strip()) < 2:
|
||||
return ""
|
||||
|
||||
blocks = ["[ZhiYi Memory — relevant past context]"]
|
||||
# 社交关闭消息不触发预取
|
||||
if _is_social_close(query):
|
||||
return ""
|
||||
|
||||
# 语义搜索
|
||||
results = self._client.recall(query.strip(), top_k=3,
|
||||
agent_id=os.environ.get("ZHIYI_AGENT_ID", "hermes-a06"))
|
||||
# 优先从 queue_prefetch 缓存取(TTL < 30s)
|
||||
with self._prefetch_lock:
|
||||
queued = self._prefetch_cache.pop("queue", None)
|
||||
if queued and isinstance(queued, dict):
|
||||
cached_ts = queued.get("timestamp", 0)
|
||||
if time.time() - cached_ts < 30:
|
||||
cached_results = queued.get("results", [])
|
||||
cached_notes = queued.get("notes", [])
|
||||
if cached_results or cached_notes:
|
||||
results = cached_results
|
||||
notes = cached_notes
|
||||
logger.debug("[ZhiYi] using queue_prefetch cache (%d results, %d notes, age=%.1fs)",
|
||||
len(cached_results), len(cached_notes), time.time() - cached_ts)
|
||||
else:
|
||||
results = None
|
||||
notes = None
|
||||
else:
|
||||
results = None
|
||||
notes = None
|
||||
else:
|
||||
results = None
|
||||
notes = None
|
||||
|
||||
blocks = ["[织忆 Memory — relevant past context]"]
|
||||
|
||||
# 语义搜索(如果缓存没有命中)
|
||||
if results is None:
|
||||
results = self._client.recall(query.strip(), top_k=3,
|
||||
agent_id=os.environ.get("ZHIYI_AGENT_ID", "hermes-a06"))
|
||||
for r in results:
|
||||
score = r.get("score", 0)
|
||||
content = r.get("content", "") or r.get("text", "")
|
||||
content = r.get("content", "") or r.get("text", "") or r.get("snippet", "")
|
||||
cat = r.get("category", "")
|
||||
if content:
|
||||
blocks.append(f" [{score:.2f}][{cat}] {content[:500]}")
|
||||
|
||||
# 图谱导航 + Obsidian 笔记(从 query 提取关键词作为实体)
|
||||
notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3)
|
||||
# 图谱导航 + Obsidian 笔记(如果缓存没有命中)
|
||||
if notes is None:
|
||||
notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3)
|
||||
if notes:
|
||||
blocks.append("\n[ZhiYi Graph — related Obsidian notes]")
|
||||
blocks.append("\n[织忆 Graph — related Obsidian notes]")
|
||||
for n in notes:
|
||||
title = n.get("title", "无标题")
|
||||
path = n.get("path", "")
|
||||
|
|
@ -472,16 +517,36 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
|
||||
# 合并 WebSocket prefetch.push 推送事件
|
||||
with self._prefetch_lock:
|
||||
if self._prefetch_cache and "[ZhiYi Prefetch" in self._prefetch_cache:
|
||||
text += "\n" + self._prefetch_cache
|
||||
ws_push = self._prefetch_cache.pop("ws_push", None)
|
||||
if ws_push:
|
||||
text += "\n" + ws_push
|
||||
|
||||
with self._prefetch_lock:
|
||||
self._prefetch_cache = text
|
||||
self._prefetch_cache.update({"sync": text})
|
||||
return text
|
||||
|
||||
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
|
||||
"""空实现 — prefetch 已是同步的,不需要额外的异步队列。"""
|
||||
pass
|
||||
"""异步预取:本轮对话结束后立即查询织忆,下一轮 prefetch 直接返回缓存。"""
|
||||
if not self._client or not query or len(query.strip()) < 2:
|
||||
return
|
||||
if _is_social_close(query):
|
||||
return
|
||||
|
||||
# 后台线程查询并缓存
|
||||
def _async_prefetch():
|
||||
try:
|
||||
results = self._client.recall(query.strip(), top_k=3)
|
||||
notes = self._client.search_notes(query.strip(), max_hops=2, max_notes=3)
|
||||
with self._prefetch_lock:
|
||||
self._prefetch_cache["queue"] = {
|
||||
"results": results,
|
||||
"notes": notes,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug("[ZhiYi] queue_prefetch error: %s", e)
|
||||
|
||||
threading.Thread(target=_async_prefetch, daemon=True).start()
|
||||
|
||||
# ── Tool interface ────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -623,7 +688,7 @@ class HermesZhiYiMemoryProvider(MemoryProvider):
|
|||
return json.dumps({"success": True, "results": [], "message": "No relevant memories found"})
|
||||
formatted = []
|
||||
for r in results:
|
||||
content = r.get("content", "") or r.get("text", "")
|
||||
content = r.get("content", "") or r.get("text", "") or r.get("snippet", "")
|
||||
formatted.append({
|
||||
"content": content[:1000],
|
||||
"score": round(r.get("score", 0), 4),
|
||||
|
|
|
|||
Loading…
Reference in New Issue