Initial commit: Pegas Memory (PG + Qdrant + Memgraph, MCP server, 14 tools)

This commit is contained in:
n1kt4r committed 2026-10-15 13:40:06 +05:00
commit 83e77ea8c9
24 files changed
+2201

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
"""Верификация атомов: self-critique + кросс-подтверждение.
Двухступенчатая верификация знания:
1. self-critique (LLM) — вердикт по новизне/противоречию против существующих.
2. кросс-подтверждение (эмбеддинги) — тот же факт из N независимых источников.
Статусы:
candidate — новый атом, не проверен
verified — подтверждён (self-critique прошёл И/ИЛИ 2+ источника)
contested — противоречит существующему знанию
stale — устарел (не актуально)
Верификация не удаляет атомы, только меняет статус/confidence.
"""
from __future__ import annotations
import json
import httpx
from . import config, db
_client = httpx.Client(timeout=600)
VERDICTS = ("new", "duplicate", "corroborates", "contradicts")
def self_critique(atom: dict, context_atoms: list[dict], model: str | None = None) -> dict:
"""LLM-вердикт по атому против существующих знаний.
Возвращает {"verdict", "confidence", "reason"}.
"""
m = model or config.LLM_MODEL
ctx = "\n".join(f"- {a.get('claim','')[:120]}" for a in context_atoms[:8])
prompt = (
f"Существующие знания:\n{ctx}\n\n"
f"Новый атом ({atom.get('type')}):\n{atom.get('claim')}\n{atom.get('content','')[:400]}\n\n"
f"Оцени атом относительно существующих знаний. Верни ТОЛЬКО JSON:\n"
f'{{"verdict": "new|duplicate|corroborates|contradicts", "confidence": 0.0-1.0, "reason": "кратко"}}\n'
f"- new: уникальное знание, которого нет в контексте\n"
f"- duplicate: то же самое, что уже есть\n"
f"- corroborates: подтверждает/уточняет существующее\n"
f"- contradicts: противоречит существующему"
)
r = _client.post(f"{config.OLLAMA_URL}/api/chat", json={
"model": m,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"options": {"temperature": 0.0},
})
r.raise_for_status()
out = r.json()["message"]["content"].strip()
start, end = out.find("{"), out.rfind("}")
if start == -1 or end == -1:
return {"verdict": "new", "confidence": 0.5, "reason": "parse error"}
try:
v = json.loads(out[start:end + 1])
except json.JSONDecodeError:
return {"verdict": "new", "confidence": 0.5, "reason": "json error"}
if v.get("verdict") not in VERDICTS:
v["verdict"] = "new"
return v
def corroborate_atom(conn, atom_id: int, threshold: float = 0.90) -> int:
"""Найти близкие атомы из ДРУГИХ источников (кросс-подтверждение).
Возвращает число найденных подтверждений (не считая сам атом).
"""
from . import qdrant_store
with conn.cursor() as cur:
cur.execute("SELECT id, doc_id, claim, content FROM pm_atom WHERE id = %s", (atom_id,))
row = cur.fetchone()
if not row:
return 0
text = (row["claim"] or "") + " " + (row["content"] or "")[:800]
hits = qdrant_store.search(text, top_k=5)
count = 0
for h in hits:
if int(h["id"]) == atom_id:
continue
if h["score"] >= threshold and h.get("doc_id") != row["doc_id"]:
count += 1
return count
def verify_atom(conn, atom_id: int, *, self_critique_enabled: bool = True,
model: str | None = None) -> dict:
"""Полная верификация одного атома. Возвращает результат.
- self-critique: вердикт LLM (contradicts → contested).
- кросс-подтверждение: N близких из других источников (N>=1 → verified).
"""
with conn.cursor() as cur:
cur.execute("SELECT id, type, claim, content, confidence, status, doc_id FROM pm_atom WHERE id = %s", (atom_id,))
atom = cur.fetchone()
if not atom:
return {"atom_id": atom_id, "error": "not found"}
result = {"atom_id": atom_id, "verdict": None, "corroborations": 0,
"new_status": atom["status"], "new_confidence": atom["confidence"]}
# 1) кросс-подтверждение по эмбеддингам (быстро, без LLM)
corr = corroborate_atom(conn, atom_id)
result["corroborations"] = corr
# 2) self-critique (LLM): контекст — ближайшие по смыслу (эмбеддинги), а не по id
if self_critique_enabled:
from . import qdrant_store
qtext = (atom["claim"] or "") + " " + (atom["content"] or "")[:400]
hits = qdrant_store.search(qtext, top_k=8)
context = [{"claim": h.get("claim", "")} for h in hits if int(h["id"]) != atom_id]
v = self_critique(atom, context, model=model)
result["verdict"] = v["verdict"]
result["reason"] = v.get("reason")
if v["verdict"] == "contradicts":
result["new_status"] = "contested"
elif v["verdict"] == "duplicate":
result["new_status"] = "stale" # дубликат — пометить неактуальным
elif v["verdict"] in ("corroborates", "new") or corr >= 1:
result["new_status"] = "verified"
elif corr >= 1:
result["new_status"] = "verified"
# confidence: кросс-подтверждение повышает уверенность
new_conf = min(1.0, float(atom["confidence"] or 0.5) + 0.1 * corr)
result["new_confidence"] = round(new_conf, 2)
with conn.cursor() as cur:
cur.execute(
"UPDATE pm_atom SET status = %s, confidence = %s, updated_at = now() WHERE id = %s",
(result["new_status"], result["new_confidence"], atom_id),
)
conn.commit()
return result
def verify_all(conn, *, limit: int = 50, self_critique_enabled: bool = True,
model: str | None = None) -> dict:
"""Верифицировать все candidate-атомы (порциями). Возвращает сводку."""
with conn.cursor() as cur:
cur.execute("SELECT id FROM pm_atom WHERE status = 'candidate' ORDER BY id LIMIT %s", (limit,))
ids = [r["id"] for r in cur.fetchall()]
summary = {"total": len(ids), "verified": 0, "contested": 0, "stale": 0, "unchanged": 0}
for aid in ids:
try:
r = verify_atom(conn, aid, self_critique_enabled=self_critique_enabled, model=model)
except Exception as e:
print(f"[verify] atom {aid} failed: {e}")
continue
ns = r.get("new_status")
if ns == "verified":
summary["verified"] += 1
elif ns == "contested":
summary["contested"] += 1
elif ns == "stale":
summary["stale"] += 1
else:
summary["unchanged"] += 1
return summary