mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-09-10 22:41:12 +00:00
177 lines
6.4 KiB
Python
177 lines
6.4 KiB
Python
import logging
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
from urllib.parse import urlsplit
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.config import settings
|
|
|
|
|
|
def _database_runtime_status() -> dict:
|
|
"""Return safe-to-expose database mode metadata for diagnostics."""
|
|
resolved_url = settings.resolved_database_url
|
|
status = {
|
|
"mode": settings.db_mode,
|
|
"url_scheme": urlsplit(resolved_url).scheme or "unknown",
|
|
}
|
|
if settings.db_mode == "local":
|
|
status["local_db_path"] = str(settings.local_db_path)
|
|
else:
|
|
# Do not expose DATABASE_URL: it may contain credentials.
|
|
status["database_url_set"] = bool(settings.database_url.strip())
|
|
return status
|
|
|
|
|
|
def _resolve_git_sha() -> str:
|
|
"""Return the short git SHA of the backend directory, or 'unknown'."""
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "rev-parse", "--short", "HEAD"],
|
|
cwd=pathlib.Path(__file__).resolve().parent.parent,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5,
|
|
)
|
|
if result.returncode == 0:
|
|
return result.stdout.strip()
|
|
except Exception:
|
|
pass
|
|
return "unknown"
|
|
|
|
class SecretGuardFilter(logging.Filter):
|
|
def filter(self, record):
|
|
if isinstance(record.msg, str) and re.search(r'sk-[a-zA-Z0-9_\-]+', record.msg):
|
|
record.msg = re.sub(r'sk-[a-zA-Z0-9_\-]+', '[REDACTED_SECRET]', record.msg)
|
|
return True
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
"""Create and configure the FastAPI application."""
|
|
app = FastAPI(
|
|
title=settings.app_name,
|
|
description="Cross-agent memory handoff system",
|
|
version="0.1.0",
|
|
)
|
|
|
|
# CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Secret Guard
|
|
for handler in logging.root.handlers:
|
|
handler.addFilter(SecretGuardFilter())
|
|
|
|
from app.api.routes import (
|
|
chat,
|
|
checkpoint,
|
|
claims,
|
|
commits,
|
|
context_git,
|
|
current,
|
|
lineage,
|
|
metrics,
|
|
repos,
|
|
worktrees,
|
|
)
|
|
|
|
# V2 Routes (Git for memory)
|
|
app.include_router(repos.router, prefix="/api/v2", tags=["repos"])
|
|
app.include_router(commits.router, prefix="/api/v2", tags=["commits"])
|
|
app.include_router(context_git.router, prefix="/api/v2", tags=["commits"])
|
|
|
|
# V4 Routes (Chat workspace)
|
|
app.include_router(chat.router, prefix="/api/v4", tags=["chat-v4"])
|
|
|
|
# V5 Routes
|
|
app.include_router(checkpoint.router, prefix="/api/v5/checkpoint", tags=["checkpoint-v5"])
|
|
app.include_router(lineage.router, prefix="/api/v5", tags=["lineage-v5"])
|
|
app.include_router(claims.router, prefix="/api/v5", tags=["claims-v5"])
|
|
app.include_router(metrics.router, prefix="/api/v5", tags=["metrics-v5"])
|
|
app.include_router(current.router, prefix="/api/v5", tags=["current-v5"])
|
|
app.include_router(worktrees.router, prefix="/api/v5", tags=["worktrees-v5"])
|
|
|
|
# ── Capabilities manifest ────────────────────────────────────────
|
|
# Computed once at startup so /health is zero-cost at request time.
|
|
_git_sha = _resolve_git_sha()
|
|
|
|
# Hardcoded feature flags matching the route modules included above.
|
|
# When a new feature ships (new route module, new query param, new
|
|
# schema shape), add it here so agents can detect stale backends.
|
|
_capabilities = [
|
|
"claims", # /api/v5/claims
|
|
"structured_tasks", # task objects with intent_hint/blocked_by/status
|
|
"task_ids", # structured task ids + claim.task_id support
|
|
"checkpoint_notes", # /api/v5/checkpoint/{id}/notes
|
|
"branch_disposition", # PATCH /api/v5/lineage/branches/disposition
|
|
"freshness", # since_commit_id on state endpoint
|
|
"compact_state", # --compact mode on state brief
|
|
"worktrees", # /api/v5/worktrees
|
|
"worktree_binding", # claims can bind to worktrees + state drift summary
|
|
"activation_health", # /health includes DB mode + provider confidence
|
|
]
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
from app.config_loader import providers_status
|
|
|
|
return {
|
|
"status": "ok",
|
|
"git_sha": _git_sha,
|
|
"capabilities": _capabilities,
|
|
"database": _database_runtime_status(),
|
|
"providers": providers_status(),
|
|
}
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
import logging
|
|
|
|
from app.config_loader import providers_status
|
|
|
|
logger = logging.getLogger("smriti.startup")
|
|
|
|
status = providers_status()
|
|
logger.info("Provider runtime validation:")
|
|
for name, info in status.items():
|
|
if info.get("missing_package", False):
|
|
logger.warning(f" - {name}: disabled (package not installed)")
|
|
elif not info.get("has_key", False):
|
|
logger.info(f" - {name}: disabled (no API key configured)")
|
|
elif not info.get("enabled", False):
|
|
logger.info(f" - {name}: disabled (explicitly disabled in config)")
|
|
else:
|
|
logger.info(f" - {name}: configured and ready")
|
|
|
|
# Specific check for the background provider — this is what
|
|
# powers checkpoint extraction, drafting, and review. If it's
|
|
# not configured, every LLM-backed endpoint will silently
|
|
# return mock content, which is the single most confusing
|
|
# failure mode in local development.
|
|
bg = status.get("background_intelligence", {})
|
|
if not bg.get("configured"):
|
|
bg_provider = bg.get("provider", "openai")
|
|
logger.warning(
|
|
"⚠ Background intelligence provider '%s' is NOT configured. "
|
|
"Checkpoint extraction, drafting, and review will return mock "
|
|
"content instead of real LLM output. To fix: set %s in .env "
|
|
"or add the key to config/providers.yaml.",
|
|
bg_provider,
|
|
{
|
|
"openai": "OPENAI_API_KEY",
|
|
"anthropic": "ANTHROPIC_API_KEY",
|
|
"openrouter": "OPENROUTER_API_KEY",
|
|
}.get(bg_provider, f"{bg_provider.upper()}_API_KEY"),
|
|
)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|