"""Runtime hierarchical RAG: re-index user-edited section text without a full PDF re-ingest.""" from __future__ import annotations import logging from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from app.api import rate_limit from app.db.database import get_db from app.db.models import Report from app.models.schemas import RuntimeRagSyncBody, RuntimeRagSyncResponse from app.services.runtime_rag_index import ( runtime_section_vector_doc_id, upsert_runtime_section_index_async, ) logger = logging.getLogger(__name__) router = APIRouter() @router.post( "/reports/{report_id}/rag/runtime/sync", response_model=RuntimeRagSyncResponse, summary="Re-index edited section text for hierarchical RAG", ) async def sync_runtime_section_rag( report_id: str, body: RuntimeRagSyncBody, request: Request, db: AsyncSession = Depends(get_db), _rl: None = Depends(rate_limit.check_read), ) -> RuntimeRagSyncResponse: """Embed the given text under a virtual document so retrieval sees it immediately. Chunks use the same document → section → paragraph hierarchy as file ingest. """ tenant_id: str = request.state.tenant_id report = await db.get(Report, report_id) if report is None or report.tenant_id != tenant_id: raise HTTPException(status_code=404, detail="Report not found") vid = runtime_section_vector_doc_id(report_id, body.section_code) title = (body.section_title or "").strip() or body.section_code n = await upsert_runtime_section_index_async( tenant_id=tenant_id, virtual_doc_id=vid, section_title=title, body_text=body.text, ) from app.retrieval.semantic_cache import invalidate_semantic_cache_for_tenant await invalidate_semantic_cache_for_tenant(tenant_id) logger.info("runtime RAG sync report=%s section=%s chunks=%d", report_id, body.section_code, n) return RuntimeRagSyncResponse(virtual_doc_id=vid, chunks_indexed=n)