""" routers/meta.py — /health, /stats, /tags, /graph endpoints. """ from __future__ import annotations from fastapi import APIRouter import httpx from core.database import get_pool from core.settings import Settings from models.responses import HealthResponse, StatsResponse, TagCount, GraphResponse, GraphNode, GraphEdge router = APIRouter(tags=['meta']) def _get_settings() -> Settings: from main import app_settings return app_settings @router.get('/health', response_model=HealthResponse) async def health(): settings = _get_settings() db_status = 'ok' ollama_status = 'ok' try: pool = await get_pool() async with pool.acquire() as conn: await conn.fetchval('SELECT 1') except Exception: db_status = 'error' try: async with httpx.AsyncClient(timeout=5.0) as client: resp = await client.get(f'{settings.ollama_url}/api/tags') if resp.status_code != 200: ollama_status = 'error' except Exception: ollama_status = 'unavailable' overall = 'ok' if db_status == 'ok' else 'degraded' return HealthResponse( status=overall, database=db_status, ollama=ollama_status, version=settings.app_version, ) @router.get('/stats', response_model=StatsResponse) async def stats(): settings = _get_settings() pool = await get_pool() async with pool.acquire() as conn: docs = await conn.fetchval('SELECT COUNT(*) FROM documents') chunks = await conn.fetchval('SELECT COUNT(*) FROM chunks') relations = await conn.fetchval('SELECT COUNT(*) FROM relations') tags_count = await conn.fetchval( "SELECT COUNT(DISTINCT tag) FROM documents, unnest(tags) AS tag" ) last_indexed = await conn.fetchval( 'SELECT MAX(indexed_at) FROM documents' ) return StatsResponse( total_documents=docs or 0, total_chunks=chunks or 0, total_relations=relations or 0, total_tags=tags_count or 0, last_indexed=last_indexed, embedding_model=settings.embedding_model, chat_model=settings.chat_model, ) @router.get('/tags', response_model=list[TagCount]) async def list_tags(): pool = await get_pool() async with pool.acquire() as conn: rows = await conn.fetch( """ SELECT tag, COUNT(*) AS count FROM documents, unnest(tags) AS tag GROUP BY tag ORDER BY count DESC, tag """ ) return [TagCount(tag=row['tag'], count=row['count']) for row in rows] @router.get('/graph', response_model=GraphResponse) async def knowledge_graph(limit: int = 200): pool = await get_pool() async with pool.acquire() as conn: doc_rows = await conn.fetch( 'SELECT id, title, path, tags, word_count FROM documents LIMIT $1', limit, ) rel_rows = await conn.fetch( """ SELECT r.source_doc_id::text, r.target_doc_id::text, r.relation_type, r.label FROM relations r WHERE r.target_doc_id IS NOT NULL LIMIT $1 """, limit * 3, ) nodes = [ GraphNode( id=str(row['id']), title=row['title'] or '', path=row['path'], tags=list(row['tags'] or []), word_count=row['word_count'], ) for row in doc_rows ] edges = [ GraphEdge( source=row['source_doc_id'], target=row['target_doc_id'], relation_type=row['relation_type'], label=row['label'], ) for row in rel_rows ] return GraphResponse(nodes=nodes, edges=edges)