Gaps: full lifecycle management (get/update/delete), pure PostgreSQL (no web search)

This commit is contained in:
n1kt4r committed 2026-10-15 17:47:40 +05:00
1 parent adfa630578
commit a4c4a766c1
5 files changed
+266 -81

No files matched your search

+112 -9
View File
@@ -35,6 +35,13 @@ def _conn():
def dict_row(cursor):
"""Row factory: dict по именам колонок.
cursor.description может быть None (UPDATE/DELETE без RETURNING) —
в этом случае возвращаем строку как есть, иначе падаем на итерации None.
"""
if cursor.description is None:
return lambda row: row
cols = [d.name for d in cursor.description]
return lambda row: dict(zip(cols, row))
@@ -114,16 +121,67 @@ def _tool_add_gap(args):
question = str(args.get("question") or "").strip()
if not question:
return json.dumps({"error": "пустой question"}, ensure_ascii=False), True
research.add_gap(_conn(), question,
reason=str(args.get("reason") or "user_query"),
priority=float(args.get("priority") or 0.5))
return json.dumps({"added": question}, ensure_ascii=False), False
try:
gap_id = research.add_gap(_conn(), question,
reason=str(args.get("reason") or "user_query"),
priority=float(args.get("priority") or 0.5))
except ValueError as e:
return json.dumps({"error": str(e)}, ensure_ascii=False), True
return json.dumps({"gap_id": gap_id, "question": question, "status": "open"},
ensure_ascii=False), False
def _tool_list_gaps(args):
from pegas_memory import research
gaps = research.detect_gaps(_conn())
return json.dumps({"gaps": gaps}, ensure_ascii=False, indent=2), False
status = args.get("status") or None
limit = int(args.get("limit") or 50)
try:
gaps = research.list_gaps(_conn(), status=status, limit=limit)
except ValueError as e:
return json.dumps({"error": str(e)}, ensure_ascii=False), True
return json.dumps({"status": status or "all", "count": len(gaps), "gaps": gaps},
ensure_ascii=False, indent=2, default=str), False
def _tool_get_gap(args):
from pegas_memory import research
gap_id = args.get("gap_id")
if gap_id is None:
return json.dumps({"error": "нужен gap_id"}, ensure_ascii=False), True
gap = research.get_gap(_conn(), int(gap_id))
if gap is None:
return json.dumps({"error": f"пробел {gap_id} не найден"}, ensure_ascii=False), True
return json.dumps(gap, ensure_ascii=False, indent=2, default=str), False
def _tool_update_gap(args):
from pegas_memory import research
gap_id = args.get("gap_id")
if gap_id is None:
return json.dumps({"error": "нужен gap_id"}, ensure_ascii=False), True
fields = {k: args[k] for k in ("question", "reason", "priority", "status")
if args.get(k) is not None}
if not fields:
return json.dumps({"error": "нечего обновлять (укажи question/reason/priority/status)"},
ensure_ascii=False), True
try:
ok = research.update_gap(_conn(), int(gap_id), **fields)
except ValueError as e:
return json.dumps({"error": str(e)}, ensure_ascii=False), True
if not ok:
return json.dumps({"error": f"пробел {gap_id} не найден"}, ensure_ascii=False), True
return json.dumps({"updated": int(gap_id), "gap": research.get_gap(_conn(), int(gap_id))},
ensure_ascii=False, indent=2, default=str), False
def _tool_delete_gap(args):
from pegas_memory import research
gap_id = args.get("gap_id")
if gap_id is None:
return json.dumps({"error": "нужен gap_id"}, ensure_ascii=False), True
if not research.delete_gap(_conn(), int(gap_id)):
return json.dumps({"error": f"пробел {gap_id} не найден"}, ensure_ascii=False), True
return json.dumps({"deleted": int(gap_id)}, ensure_ascii=False), False
def _tool_stats(args):
@@ -296,9 +354,51 @@ TOOLS = [
},
{
"name": "memory_list_gaps",
"description": "Список открытых пробелов (вопросов, на которые пока нет ответа) в базе, "
"отсортированный по приоритету. Вызывай, чтобы узнать, что ещё нужно исследовать.",
"inputSchema": {"type": "object", "properties": {}, "required": []},
"description": "Список пробелов (вопросов без ответа). По умолчанию — открытые, по приоритету. "
"Вызывай, чтобы узнать, что ещё нужно исследовать.",
"inputSchema": {
"type": "object",
"properties": {
"status": {"type": "string", "description": "Фильтр: open | researching | resolved | dismissed (опционально)."},
"limit": {"type": "integer", "default": 50, "description": "Сколько вернуть (1-200)."},
},
"required": [],
},
},
{
"name": "memory_get_gap",
"description": "Получить один пробел по id (все поля).",
"inputSchema": {
"type": "object",
"properties": {"gap_id": {"type": "integer", "description": "ID пробела."}},
"required": ["gap_id"],
},
},
{
"name": "memory_update_gap",
"description": "Отредактировать пробел: текст вопроса, причину, приоритет и/или статус. "
"Статусы: open (открыт) | researching (взят в работу) | resolved (закрыт) | dismissed (не актуален).",
"inputSchema": {
"type": "object",
"properties": {
"gap_id": {"type": "integer", "description": "ID пробела."},
"question": {"type": "string", "description": "Новая формулировка вопроса."},
"reason": {"type": "string", "description": "low_coverage|low_confidence|stale|contested|user_query."},
"priority": {"type": "number", "description": "Новый приоритет 0-1."},
"status": {"type": "string", "description": "open | researching | resolved | dismissed."},
},
"required": ["gap_id"],
},
},
{
"name": "memory_delete_gap",
"description": "Удалить пробел по id (если он больше не нужен). "
"Обычно вместо удаления лучше перевести в status=dismissed или resolved.",
"inputSchema": {
"type": "object",
"properties": {"gap_id": {"type": "integer", "description": "ID пробела."}},
"required": ["gap_id"],
},
},
{
"name": "memory_stats",
@@ -441,6 +541,9 @@ def handle(msg):
"memory_ingest": _tool_ingest,
"memory_add_gap": _tool_add_gap,
"memory_list_gaps": _tool_list_gaps,
"memory_get_gap": _tool_get_gap,
"memory_update_gap": _tool_update_gap,
"memory_delete_gap": _tool_delete_gap,
"memory_stats": _tool_stats,
"memory_store_code": _tool_store_code,
"memory_search_code": _tool_search_code,