115 lines
4.6 KiB
Python
115 lines
4.6 KiB
Python
"""Управление пробелами в знаниях (research gaps).
|
|
|
|
Работает **только с PostgreSQL** — никакого веб-поиска. Поиск выполняет сама
|
|
модель встроенным инструментом \`web_search\` (SearXNG и автоматический
|
|
research-цикл убраны).
|
|
|
|
Жизненный цикл пробела:
|
|
open → researching → resolved | dismissed
|
|
|
|
Поля:
|
|
question — что нужно исследовать (формулировка)
|
|
reason — почему пробел обнаружен
|
|
priority — 0.0 .. 1.0 (больше = важнее)
|
|
status — open | researching | resolved | dismissed
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
VALID_REASONS = ("low_coverage", "low_confidence", "stale", "contested", "user_query")
|
|
VALID_STATUSES = ("open", "researching", "resolved", "dismissed")
|
|
|
|
_COLS = "id, question, reason, priority, status, created_at"
|
|
|
|
|
|
def list_gaps(conn, *, status: str | None = None, limit: int = 50) -> list[dict]:
|
|
"""Список пробелов. Без фильтра — сначала открытые, затем по приоритету."""
|
|
limit = max(1, min(int(limit), 200))
|
|
with conn.cursor() as cur:
|
|
if status:
|
|
if status not in VALID_STATUSES:
|
|
raise ValueError(f"status must be one of {VALID_STATUSES}")
|
|
cur.execute(
|
|
f"SELECT {_COLS} FROM pm_research_gap WHERE status = %s "
|
|
"ORDER BY priority DESC, id LIMIT %s",
|
|
(status, limit),
|
|
)
|
|
else:
|
|
cur.execute(
|
|
f"SELECT {_COLS} FROM pm_research_gap "
|
|
"ORDER BY (status = 'open') DESC, priority DESC, id LIMIT %s",
|
|
(limit,),
|
|
)
|
|
return cur.fetchall()
|
|
|
|
|
|
def get_gap(conn, gap_id: int) -> dict | None:
|
|
"""Один пробел по id (или None)."""
|
|
with conn.cursor() as cur:
|
|
cur.execute(f"SELECT {_COLS} FROM pm_research_gap WHERE id = %s", (gap_id,))
|
|
return cur.fetchone()
|
|
|
|
|
|
def add_gap(conn, question: str, reason: str = "user_query", priority: float = 0.5) -> int:
|
|
"""Зарегистрировать пробел. Возвращает id."""
|
|
question = (question or "").strip()
|
|
if not question:
|
|
raise ValueError("question must not be empty")
|
|
if reason not in VALID_REASONS:
|
|
raise ValueError(f"reason must be one of {VALID_REASONS}")
|
|
priority = max(0.0, min(1.0, float(priority)))
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"INSERT INTO pm_research_gap (question, reason, priority, status) "
|
|
"VALUES (%s, %s, %s, 'open') RETURNING id",
|
|
(question, reason, priority),
|
|
)
|
|
row = cur.fetchone()
|
|
conn.commit()
|
|
return row["id"]
|
|
|
|
|
|
def update_gap(conn, gap_id: int, *, question: str | None = None,
|
|
reason: str | None = None, priority: float | None = None,
|
|
status: str | None = None) -> bool:
|
|
"""Обновить поля пробела. Возвращает True, если запись найдена.
|
|
|
|
Типичные переходы: status='researching' (взял в работу),
|
|
status='resolved' (закрыт), status='dismissed' (не актуален).
|
|
"""
|
|
sets: list[str] = []
|
|
vals: list = []
|
|
if question is not None:
|
|
question = question.strip()
|
|
if not question:
|
|
raise ValueError("question must not be empty")
|
|
sets.append("question = %s"); vals.append(question)
|
|
if reason is not None:
|
|
if reason not in VALID_REASONS:
|
|
raise ValueError(f"reason must be one of {VALID_REASONS}")
|
|
sets.append("reason = %s"); vals.append(reason)
|
|
if priority is not None:
|
|
sets.append("priority = %s"); vals.append(max(0.0, min(1.0, float(priority))))
|
|
if status is not None:
|
|
if status not in VALID_STATUSES:
|
|
raise ValueError(f"status must be one of {VALID_STATUSES}")
|
|
sets.append("status = %s"); vals.append(status)
|
|
|
|
if not sets:
|
|
return get_gap(conn, gap_id) is not None
|
|
|
|
vals.append(gap_id)
|
|
with conn.cursor() as cur:
|
|
cur.execute(f"UPDATE pm_research_gap SET {', '.join(sets)} WHERE id = %s", vals)
|
|
found = cur.rowcount > 0
|
|
conn.commit()
|
|
return found
|
|
|
|
|
|
def delete_gap(conn, gap_id: int) -> bool:
|
|
"""Удалить пробел. Возвращает True, если запись существовала."""
|
|
with conn.cursor() as cur:
|
|
cur.execute("DELETE FROM pm_research_gap WHERE id = %s", (gap_id,))
|
|
found = cur.rowcount > 0
|
|
conn.commit()
|
|
return found
|