Kansho/backend/retrieval.py
2026-08-25 13:57:23 +02:00

176 lines
6.3 KiB
Python

"""Swappable retrieval interface. Specs name what to select; recency/SQL is implementation."""
from __future__ import annotations
from typing import Any
from db import get_db, row_to_dict
from journal_body import plain_text
# Implementation caps. Not part of the selection-spec contract.
_DAY_MESSAGE_CAP = 80
_SPACE_ENTRY_LIMIT = 5
_SPACE_ENTRY_EXCERPT_CHARS = 400
_SPACE_SOURCE_CONV_LIMIT = 3
_SPACE_SOURCE_EXCERPT_CHARS = 400
_SPACE_SOURCE_MESSAGE_CAP = 8
def retrieve(profile_id: str, spec: dict[str, Any]) -> list[dict]:
kind = spec.get("kind")
if kind == "day_messages":
return _day_messages(profile_id, spec)
if kind == "space_entries":
return _space_entries(profile_id, spec)
if kind == "space_recent_sources":
return _space_recent_sources(profile_id, spec)
if kind == "writing_profile":
return _writing_profile(profile_id)
return []
def _day_messages(profile_id: str, spec: dict[str, Any]) -> list[dict]:
journal_day_id = spec.get("journal_day_id")
conversation_ids = spec.get("conversation_ids") or []
if not journal_day_id:
return []
with get_db() as conn:
if conversation_ids:
placeholders = ",".join("?" * len(conversation_ids))
rows = conn.execute(
f"""
SELECT m.*, c.title AS conversation_title
FROM messages m
JOIN conversations c ON c.id = m.conversation_id
WHERE m.profile_id = ? AND c.journal_day_id = ? AND m.conversation_id IN ({placeholders})
ORDER BY c.created, m.seq
LIMIT ?
""",
(profile_id, journal_day_id, *conversation_ids, _DAY_MESSAGE_CAP),
).fetchall()
else:
rows = conn.execute(
"""
SELECT m.*, c.title AS conversation_title
FROM messages m
JOIN conversations c ON c.id = m.conversation_id
WHERE m.profile_id = ? AND c.journal_day_id = ?
ORDER BY c.created, m.seq
LIMIT ?
""",
(profile_id, journal_day_id, _DAY_MESSAGE_CAP),
).fetchall()
return [row_to_dict(row) for row in rows]
def _space_entries(profile_id: str, spec: dict[str, Any]) -> list[dict]:
space_id = spec.get("space_id")
exclude_day_id = spec.get("exclude_day_id")
if not space_id:
return []
params: list[Any] = [profile_id, space_id]
exclude_sql = ""
if exclude_day_id:
exclude_sql = "AND e.journal_day_id != ?"
params.append(exclude_day_id)
params.append(_SPACE_ENTRY_LIMIT)
with get_db() as conn:
rows = conn.execute(
f"""
SELECT e.id AS entry_id, e.journal_day_id, d.calendar_date, v.title, v.body, v.origin
FROM journal_entries e
JOIN journal_days d ON d.id = e.journal_day_id
JOIN journal_entry_versions v ON v.id = e.current_version_id
WHERE e.profile_id = ? AND e.space_id = ? AND e.deleted_at IS NULL {exclude_sql}
ORDER BY d.calendar_date DESC, e.updated DESC
LIMIT ?
""",
params,
).fetchall()
result = []
for row in rows:
item = row_to_dict(row)
item["excerpt"] = plain_text(item.get("body") or "")[:_SPACE_ENTRY_EXCERPT_CHARS]
result.append(item)
return result
def _space_recent_sources(profile_id: str, spec: dict[str, Any]) -> list[dict]:
"""Recent original conversations in the same space. Recency is the current heuristic."""
space_id = spec.get("space_id")
exclude_conversation_id = spec.get("exclude_conversation_id")
exclude_journal_day_id = spec.get("exclude_journal_day_id")
if not space_id:
return []
params: list[Any] = [profile_id, space_id]
extra = ""
if exclude_conversation_id:
extra += " AND c.id != ?"
params.append(exclude_conversation_id)
if exclude_journal_day_id:
extra += " AND (c.journal_day_id IS NULL OR c.journal_day_id != ?)"
params.append(exclude_journal_day_id)
params.append(_SPACE_SOURCE_CONV_LIMIT)
with get_db() as conn:
conversations = conn.execute(
f"""
SELECT c.id, c.updated, c.journal_day_id
FROM conversations c
WHERE c.profile_id = ? AND c.space_id = ? {extra}
ORDER BY c.updated DESC, c.created DESC
LIMIT ?
""",
params,
).fetchall()
result = []
for conv in conversations:
messages = conn.execute(
"""
SELECT body FROM messages
WHERE profile_id = ? AND conversation_id = ? AND role = 'user'
ORDER BY seq
LIMIT ?
""",
(profile_id, conv["id"], _SPACE_SOURCE_MESSAGE_CAP),
).fetchall()
excerpt = " ".join((row["body"] or "").strip() for row in messages if (row["body"] or "").strip())
excerpt = excerpt[:_SPACE_SOURCE_EXCERPT_CHARS]
if not excerpt:
continue
result.append(
{
"conversation_id": conv["id"],
"journal_day_id": conv["journal_day_id"],
"excerpt": excerpt,
}
)
return result
def _writing_profile(profile_id: str) -> list[dict]:
with get_db() as conn:
row = row_to_dict(
conn.execute(
"SELECT compiled_brief, updated FROM writing_profiles WHERE profile_id = ?",
(profile_id,),
).fetchone()
)
sources = [
row_to_dict(item)
for item in conn.execute(
"""
SELECT id, kind, body, entry_id, weight, created
FROM writing_profile_sources
WHERE profile_id = ?
ORDER BY CASE kind
WHEN 'journal_entry' THEN 0
WHEN 'imported_text' THEN 1
ELSE 2
END, created DESC
""",
(profile_id,),
).fetchall()
]
if not row and not sources:
return []
return [{"compiled_brief": (row or {}).get("compiled_brief") or "", "sources": sources}]