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>
117 lines
4.0 KiB
Python
117 lines
4.0 KiB
Python
"""Authenticated, secret-free status of configured integrations and cached data."""
|
|
|
|
import asyncio
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
from quart import Blueprint, jsonify
|
|
from quart_jwt_extended import get_jwt_identity
|
|
|
|
from blueprints.users.decorators import jwt_access_required
|
|
from blueprints.users.models import User
|
|
from blueprints.ynab.sync import _current_state, ynab_configured
|
|
|
|
integrations_blueprint = Blueprint(
|
|
"integrations_api", __name__, url_prefix="/api/integrations"
|
|
)
|
|
|
|
|
|
def configuration_status(name: str, configured: bool) -> dict:
|
|
return {
|
|
"name": name,
|
|
"status": "configured" if configured else "not_configured",
|
|
"last_success_at": None,
|
|
"message": "Configured; connection has not been checked."
|
|
if configured
|
|
else "Not configured.",
|
|
}
|
|
|
|
|
|
async def _ynab_status() -> dict:
|
|
status = configuration_status("YNAB", ynab_configured())
|
|
if not ynab_configured():
|
|
return status
|
|
try:
|
|
state = await _current_state()
|
|
if state is None or state.last_synced_at is None:
|
|
return {
|
|
**status,
|
|
"status": "unavailable",
|
|
"message": "No synchronized budget data is available.",
|
|
}
|
|
stamp = state.last_synced_at
|
|
if stamp.tzinfo is None:
|
|
stamp = stamp.replace(tzinfo=timezone.utc)
|
|
age = max(0, (datetime.now(timezone.utc) - stamp).total_seconds())
|
|
stale = age > 120 or bool(state.last_error)
|
|
return {
|
|
**status,
|
|
"status": "stale" if stale else "ready",
|
|
"last_success_at": stamp.isoformat(),
|
|
"message": "Using cached budget data; synchronization is overdue or failed."
|
|
if stale
|
|
else "Budget data is current.",
|
|
}
|
|
except Exception:
|
|
return {
|
|
**status,
|
|
"status": "unavailable",
|
|
"message": "Budget synchronization status is unavailable.",
|
|
}
|
|
|
|
|
|
async def integration_statuses(*, is_admin: bool) -> list[dict]:
|
|
result = [await _ynab_status()]
|
|
notes = configuration_status("Document search", bool(os.getenv("DATABASE_URL")))
|
|
try:
|
|
from blueprints.rag.logic import get_vector_store_stats
|
|
|
|
stats = await asyncio.to_thread(get_vector_store_stats)
|
|
count = stats.get("total_documents", 0)
|
|
notes.update(
|
|
status="ready" if count else "unavailable",
|
|
indexed_chunks=count,
|
|
message=f"{count} indexed document chunks available."
|
|
if count
|
|
else "No documents are indexed yet.",
|
|
)
|
|
except Exception:
|
|
notes.update(
|
|
status="unavailable", message="Document index status is unavailable."
|
|
)
|
|
result.append(notes)
|
|
for name, keys in [
|
|
("Mealie", ("MEALIE_BASE_URL", "MEALIE_API_TOKEN")),
|
|
("Obsidian", ("OBSIDIAN_VAULT_PATH",)),
|
|
("Web search", ("TAVILY_API_KEY",)),
|
|
("Local model", ("LLAMA_SERVER_URL",)),
|
|
("Fallback model", ("OPENAI_API_KEY",)),
|
|
("SimbaKit", ("SIMBAKIT_URL",)),
|
|
]:
|
|
result.append(configuration_status(name, all(os.getenv(k) for k in keys)))
|
|
if is_admin:
|
|
for name, keys in [
|
|
("Homebox", ("HOMEBOX_URL", "HOMEBOX_TOKEN")),
|
|
(
|
|
"BookStack",
|
|
("BOOKSTACK_BASE_URL", "BOOKSTACK_TOKEN_ID", "BOOKSTACK_TOKEN_SECRET"),
|
|
),
|
|
("Google", ("GOOGLE_TOKEN_FILE",)),
|
|
("iMessage", ("SENDBLUE_API_KEY", "SENDBLUE_API_SECRET")),
|
|
("Email", ("MAILGUN_API_KEY", "MAILGUN_DOMAIN")),
|
|
]:
|
|
result.append(configuration_status(name, all(os.getenv(k) for k in keys)))
|
|
return result
|
|
|
|
|
|
@integrations_blueprint.get("/")
|
|
@jwt_access_required
|
|
async def status():
|
|
user = await User.get(id=get_jwt_identity())
|
|
return jsonify(
|
|
{
|
|
"checked_at": datetime.now(timezone.utc).isoformat(),
|
|
"integrations": await integration_statuses(is_admin=user.is_admin()),
|
|
}
|
|
)
|