Files
simbarag/blueprints/integrations/__init__.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

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()),
}
)