RICS / app /api /agentic.py
StormShadow308's picture
Speed up generation and ingestion; add live report progress on /status.
c893230
Raw
History Blame Contribute Delete
5.42 kB
"""Agentic RICS inspection endpoints."""
from __future__ import annotations
import logging
from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import update
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, ReportStatus
from app.jobs.models import GenerationJob, JobType
from app.jobs.queue import dispatch_or_enqueue
from app.services.generation import mark_report_generation_failed, run_agentic_full_report_job
from app.agentic.runtime_status import is_openai_inspector_live
from app.agentic.tools import list_tool_specs
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post(
"/reports/{report_id}/agentic/generate",
summary="Generate a full RICS-style report using an agentic pipeline",
)
async def agentic_generate_report(
report_id: str,
request: Request,
body: dict,
db: AsyncSession = Depends(get_db),
_rl: None = Depends(rate_limit.check_generate),
) -> dict:
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")
bullets_by_section = body.get("bullets_by_section") or {}
if not isinstance(bullets_by_section, dict) or not bullets_by_section:
raise HTTPException(status_code=422, detail="bullets_by_section must be a non-empty object")
ai_percent = body.get("ai_percent", 50)
retrieval_level = body.get("retrieval_level", "paragraph")
reference_document_ids = body.get("reference_document_ids") or []
similarity_scan = bool(body.get("similarity_scan", False))
peer_sections = body.get("peer_sections") if isinstance(body.get("peer_sections"), dict) else {}
sim_excl = body.get("similarity_exclude_document_ids")
similarity_exclude_document_ids = (
[str(x) for x in sim_excl] if isinstance(sim_excl, list) else []
)
interference_level = body.get("interference_level")
il_arg = str(interference_level).strip() if interference_level is not None else None
try:
ai_percent_i = int(ai_percent)
except Exception: # noqa: BLE001
ai_percent_i = 50
ai_percent_i = max(0, min(100, ai_percent_i))
bullets_map = {
str(k): list(v) for k, v in bullets_by_section.items() if isinstance(v, list)
}
cas_result = await db.execute(
update(Report)
.where(
Report.id == report_id,
Report.tenant_id == tenant_id,
Report.status != ReportStatus.generating,
)
.values(
status=ReportStatus.generating,
generation_started_at=datetime.now(UTC),
generation_section_total=len(bullets_map) if bullets_map else None,
)
)
if cas_result.rowcount == 0:
raise HTTPException(
status_code=409,
detail="A generation task is already running for this report.",
)
await db.commit()
ref_ids = (
[str(x) for x in reference_document_ids]
if isinstance(reference_document_ids, list)
else None
)
peer_map = {str(k): str(v) for k, v in (peer_sections or {}).items()}
agentic_job = GenerationJob(
job_type=JobType.agentic_full,
report_id=report_id,
tenant_id=tenant_id,
payload={
"bullets_by_section": bullets_map,
"ai_percent": ai_percent_i,
"retrieval_level": str(retrieval_level),
"reference_document_ids": ref_ids,
"similarity_scan": similarity_scan,
"peer_sections": peer_map,
"similarity_exclude_document_ids": similarity_exclude_document_ids or None,
"interference_level": il_arg,
},
)
async def _run_agentic_task() -> None:
try:
await run_agentic_full_report_job(
report_id,
tenant_id,
bullets_by_section=bullets_map,
ai_percent=ai_percent_i,
retrieval_level=str(retrieval_level),
reference_document_ids=ref_ids,
similarity_scan=similarity_scan,
peer_sections=peer_map,
similarity_exclude_document_ids=similarity_exclude_document_ids or None,
interference_level=il_arg,
)
except Exception as exc: # noqa: BLE001
logger.exception("Agentic background task failed report=%s", report_id)
await mark_report_generation_failed(report_id, tenant_id, str(exc))
queue_mode = await dispatch_or_enqueue(job=agentic_job, inline_factory=_run_agentic_task)
inspector_mode = "openai_tool_agent" if is_openai_inspector_live() else "legacy_pipeline"
queue_hint = " (Redis worker)" if queue_mode == "redis" else ""
return {
"report_id": report_id,
"status": "generating",
"message": (
f"Agentic generation queued{queue_hint}. "
f"Poll GET /reports/{report_id}/status or "
f"GET /reports/{report_id}/sections for results."
),
"inspector_mode": inspector_mode,
"ai_percent": ai_percent_i,
"retrieval_level": str(retrieval_level),
"agent_tools": [s.name for s in list_tool_specs()],
}