5809ad3adb
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>
87 lines
2.8 KiB
Python
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
|