Files
simbarag/blueprints/rag/source_api.py
T
ryan 5809ad3adb
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
Add durable chat runs, shared agent runtime, reliable jobs, history tools and citations
Shared runtime
- One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for
  web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions,
  memories, summaries and metrics match across channels.
- Serialize turns per checkpoint thread; /query validates input and returns 409
  while a durable run owns the conversation.

Reconnectable chat (durable runs)
- POST /runs creates an idempotent, owner-scoped run; GET .../events replays an
  ordered SSE log by cursor, so generation survives refresh and device switches.
  Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter.
- Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of
  polling; keep buffered text when a run is stopped.
- Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering
  the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded.

Scheduled messages and cron
- Atomic delivery claims, attempt history and explicit retries; ambiguous sends
  become "uncertain" and are never resent automatically.
- Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner.
- Cancelling never rewrites a started delivery; series stop via tombstones.
  Legacy sent rows are not forked into new series. Malformed bodies return 400.

History, integrations and retrieval
- Message-content search, pinned conversations, JSON/Markdown export and a
  memory editor.
- Integration status panel and YNAB data-freshness provenance.
- Date-filtered document/note search with stable citations and a source viewer.

Migration 20 is generated by aerich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:11:04 -04:00

87 lines
2.8 KiB
Python

"""Read-only source chunks from the same authenticated shared retrieval corpus."""
import asyncio
import os
from urllib.parse import urlsplit
from uuid import UUID
from quart import Blueprint, jsonify
from sqlalchemy import text
from blueprints.conversation.artifacts_util import (
obsidian_source_ref,
vault_relative_path,
)
from blueprints.users.decorators import jwt_access_required
from .retrieval import document_date
sources_blueprint = Blueprint("sources_api", __name__, url_prefix="/api/sources")
def _load_chunk(identifier: str) -> dict | None:
from .logic import _get_collection_id, _get_engine
collection = _get_collection_id()
if not collection:
return None
with _get_engine().connect() as conn:
row = conn.execute(
text(
"SELECT document, cmetadata FROM langchain_pg_embedding WHERE id=:id AND collection_id=:collection"
),
{"id": identifier, "collection": collection},
).first()
if row is None:
return None
metadata = row[1] or {}
source = metadata.get("source")
if source not in {"obsidian", "paperless"}:
return None
result = {
"id": identifier,
"source": source,
"content": row[0],
"date": document_date(metadata),
"headings": [metadata[k] for k in ("h1", "h2", "h3") if metadata.get(k)],
}
if source == "obsidian":
path = vault_relative_path(metadata.get("filepath") or "")
result.update(title=metadata.get("title") or path or "Note", path=path)
result["original_url"] = obsidian_source_ref(path).get("url") if path else None
else:
result.update(
title=metadata.get("filename") or "Document",
document_type=metadata.get("document_type"),
)
base = os.getenv("BASE_URL", "").rstrip("/")
base = base if "://" in base else "http://" + base if base else ""
parsed = urlsplit(base)
document_id = metadata.get("document_id")
result["original_url"] = (
f"{base}/documents/{document_id}/details"
if parsed.scheme in {"http", "https"}
and parsed.hostname
and not parsed.username
and str(document_id).isdigit()
else None
)
return result
@sources_blueprint.get("/<source_id>")
@jwt_access_required
async def get_source(source_id):
try:
identifier = str(UUID(source_id))
except ValueError:
return jsonify(error="source not found"), 404
try:
result = await asyncio.to_thread(_load_chunk, identifier)
except Exception:
return jsonify(error="Source storage is unavailable. Please try again."), 503
if result is None:
return jsonify(error="source not found"), 404
response = jsonify(result)
response.headers["Cache-Control"] = "no-store"
return response