feat(pegas-memory): Tier-1 self-improvement tools (commit + staleness)

- memory_commit: closes the 'commit hole' — writes verified answers from
  closed gaps back into atoms (mandatory dedup); leaves gap open if no
  verified answer exists. Reads verified status from PostgreSQL (source of
  truth), not the Qdrant projection.
- memory_stale: staleness guard with two signals — content-staleness
  (re-fetch + LLM verdict, needs proxy for foreign sites) and age-staleness
  (proxy-free fallback by pm_document.fetched_at, max_age_days).
- db: populate pm_document.fetched_at on upsert (freshness signal).
- docs: README/AGENTS/ARCHITECTURE/EXAMPLES updated (18 tools).
- self_improve.py: new module (227 lines).
This commit is contained in:
n1kt4r committed 2026-10-15 22:06:30 +05:00
1 parent a4c4a766c1
commit eca8a11e23
8 files changed
+439 -6

No files matched your search

+80
View File
@@ -289,6 +289,55 @@ def _tool_verify(args):
return json.dumps(result, ensure_ascii=False, indent=2), False
def _tool_commit(args):
"""Tier 1 (commit): закрыть дыру — answer закрытого gap пишется в атомы."""
from pegas_memory import self_improve
gap_ids = args.get("gap_ids")
try:
gap_ids = [int(x) for x in (gap_ids or [])]
except (TypeError, ValueError):
return json.dumps({"error": "gap_ids должен быть списком целых id"}, ensure_ascii=False), True
if not gap_ids:
return json.dumps({"error": "нужен gap_ids (список id закрытых пробелов)"}, ensure_ascii=False), True
dedup = bool(args.get("dedup", True))
try:
result = self_improve.commit_gaps(_conn(), gap_ids=gap_ids, dedup=dedup)
except ValueError as e:
return json.dumps({"error": str(e)}, ensure_ascii=False), True
return json.dumps(result, ensure_ascii=False, indent=2), False
def _tool_stale(args):
"""Tier 1 (maintain): гвард сталкнутости — атомы по свежему источнику → stale."""
from pegas_memory import self_improve
atom_id = args.get("atom_id")
all_ = bool(args.get("all", False))
if atom_id is not None:
try:
result = self_improve.stale_guard(
_conn(), atom_id=int(atom_id),
age_only=bool(args.get("age_only", False)),
max_age_days=int(args.get("max_age_days") or 90),
)
except ValueError as e:
return json.dumps({"error": str(e)}, ensure_ascii=False), True
elif all_:
try:
result = self_improve.stale_guard(
_conn(), all=True,
limit=min(int(args.get("limit") or 20), 200),
sample=bool(args.get("sample", True)),
min_confidence=float(args.get("min_confidence") or 0.0),
age_only=bool(args.get("age_only", False)),
max_age_days=int(args.get("max_age_days") or 90),
)
except ValueError as e:
return json.dumps({"error": str(e)}, ensure_ascii=False), True
else:
return json.dumps({"error": "укажи atom_id или all=true"}, ensure_ascii=False), True
return json.dumps(result, ensure_ascii=False, indent=2), False
TOOLS = [
{
"name": "memory_search",
@@ -492,6 +541,35 @@ TOOLS = [
{
"name": "memory_commit",
"description": "Tier 1 (commit): закрыть «дыру commit» — верифицированный ответ закрытого пробела пишется обратно в атомы (не даёт знанию слетывать). Берет id пробелов, ищет верифицированные атомы-ответы, дистиллирует канонический атом с обязательным dedup и закрывает пробел. Atom-efficiency: dedup по умолчанию.",
"inputSchema": {
"type": "object",
"properties": {
"gap_ids": {"type": "array", "items": {"type": "integer"}, "description": "Список id закрытых пробелов, answer которых надо записать в атомы."},
"dedup": {"type": "boolean", "description": "Семантическая дедупляция при записи (default true)."},
},
"required": ["gap_ids"],
},
},
{
"name": "memory_stale",
"description": "Tier 1 (maintain): гвард сталкнутости. Пере-проверяет атом(ы) по свежу источнику: противоречит/устарел → status='stale'. Лимит LLM-вызовов через limit/sample. atom_id — один атом; all — пачка (с limit/sample).",
"inputSchema": {
"type": "object",
"properties": {
"atom_id": {"type": "integer", "description": "ID атома для проверки на актуальность."},
"all": {"type": "boolean", "description": "Прогнать пачку атомов (с limit/sample)."},
"limit": {"type": "integer", "description": "Лимит атомов в пачке (1-200)."},
"sample": {"type": "boolean", "description": "Сэмпл вместо проверки всех (по умолчанию true при all)."},
"min_confidence": {"type": "number", "description": "Мин. уверенность атомов в пачке (default 0.0)."},
"age_only": {"type": "boolean", "description": "Без рефечя: только age-staleness по fetched_at (proxy-free)."},
"max_age_days": {"type": "integer", "description": "Возраст контента в днях, старше которого → stale (default 90)."},
},
"required": [],
},
},
{
"name": "memory_get_context",
"description": "Собрать релевантный контекст из твоей базы для ответа (поиск по атомам + форматирование). "
@@ -551,6 +629,8 @@ def handle(msg):
"memory_infer": _tool_infer,
"memory_query_graph": _tool_query_graph,
"memory_verify": _tool_verify,
"memory_commit": _tool_commit,
"memory_stale": _tool_stale,
"memory_get_context": _tool_get_context,
}
fn = handlers.get(tool)