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

+104
View File
@@ -0,0 +1,104 @@
"""Логическая инференция: forward-chaining правил в Memgraph.
Правило — атом типа logical_rule, content = JSON:
{"if": "imports", "then": "depends_on"} # одношаговое
{"if": ["depends_on", "depends_on"], "then": "depends_on"} # транзитивное (композиция)
Инференция итерирует до фиксированной точки: пока правила порождают новые рёбра.
Выведенные рёбра помечаются inferred=true, source='inference'.
"""
from __future__ import annotations
import json
from . import memgraph_store
def _is_kind(token) -> bool:
"""kind ребра — одиночный токен без пробелов (imports, depends_on, calls...).
Отсекает человекочитаемые правила от LLM-дистилляции вида
if='Memgraph хранит граф в RAM' — это не kind ребра.
"""
return isinstance(token, str) and " " not in token and len(token) > 0 and len(token) < 64
def load_rules(conn) -> list[dict]:
"""Загрузить машинные правила из pm_atom (type='logical_rule').
Правило валидно для инференции, только если if/then — kind-рёбра
(одиночные токены без пробелов). Человекочитаемые правила от
дистилляции отфильтровываются.
"""
with conn.cursor() as cur:
cur.execute(
"SELECT id, claim, content FROM pm_atom WHERE type = 'logical_rule' ORDER BY id"
)
rows = cur.fetchall()
rules = []
for r in rows:
try:
spec = json.loads(r["content"]) if isinstance(r["content"], str) else r["content"]
except (json.JSONDecodeError, TypeError):
continue
if "if" not in spec or "then" not in spec:
continue
if_ = spec["if"]
then = spec["then"]
# валидация: then — всегда kind; if — kind или список kind
if not _is_kind(then):
continue
if isinstance(if_, list):
if len(if_) != 2 or not all(_is_kind(k) for k in if_):
continue
elif not _is_kind(if_):
continue
rules.append({"atom_id": r["id"], "claim": r["claim"], "spec": spec})
return rules
def _apply_rule(spec: dict) -> int:
"""Применить одно правило. Возвращает число созданных рёбер."""
if_ = spec["if"]
then = spec["then"]
if isinstance(if_, list):
# композиция: [k1, k2] -> (a)-[k1]->(b)-[k2]->(c) ⇒ (a)-[then]->(c)
if len(if_) == 2:
return memgraph_store.apply_rule_chain(if_[0], if_[1], then)
return 0
# одношаговое: (a)-[if]->(b) ⇒ (a)-[then]->(b)
return memgraph_store.apply_rule_one_step(if_, then)
def infer(conn, max_rounds: int = 10) -> dict:
"""Запустить инференцию до фиксированной точки.
Возвращает {rules, rounds, edges_created}.
"""
rules = load_rules(conn)
total = 0
rounds = 0
for _ in range(max_rounds):
created = 0
for rule in rules:
created += _apply_rule(rule["spec"])
total += created
rounds += 1
if created == 0:
break
return {"rules": len(rules), "rounds": rounds, "edges_created": total}
def store_rule(conn, *, claim: str, if_: str | list, then: str) -> int:
"""Сохранить логическое правило (атом logical_rule) в PG.
Возвращает id атома. Если правило с тем же content уже есть — вернёт существующий.
"""
from . import db
content = json.dumps({"if": if_, "then": then}, ensure_ascii=False)
atom_id, created = db.upsert_atom(
conn, type="logical_rule", claim=claim, content=content,
confidence=0.9, status="verified",
)
if created:
db.enqueue_outbox(conn, entity_type="atom", entity_id=atom_id)
return atom_id