Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/app/features/assistant/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Assistant feature package."""
79 changes: 79 additions & 0 deletions backend/app/features/assistant/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

from time import perf_counter
from uuid import UUID, uuid4

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session, joinedload, selectinload

from app.common.dependencies import get_database_session
from app.core.logger import _setup_custom_logger
from app.features.assistant.schemas import SimulationSummaryResponse
from app.features.assistant.service import build_simulation_summary
from app.features.simulation.models import Simulation
from app.features.user.manager import current_active_user
from app.features.user.models import User

router = APIRouter(prefix="/simulations", tags=["Simulation Assistant"])
logger = _setup_custom_logger(__name__)


@router.post(
"/{sim_id}/summary",
response_model=SimulationSummaryResponse,
responses={
200: {"description": "Deterministic summary generated successfully."},
401: {"description": "Unauthorized."},
404: {"description": "Simulation not found."},
},
)
def summarize_simulation(
sim_id: UUID,
db: Session = Depends(get_database_session),
user: User = Depends(current_active_user),
) -> SimulationSummaryResponse:
"""Generate a deterministic read-only summary for one simulation."""

start = perf_counter()
trace_id = uuid4()

simulation = (
db.query(Simulation)
.options(
joinedload(Simulation.case),
joinedload(Simulation.machine),
selectinload(Simulation.artifacts),
selectinload(Simulation.links),
)
.filter(Simulation.id == sim_id)
.one_or_none()
)

if simulation is None:
duration_ms = (perf_counter() - start) * 1000
logger.info(
"simulation_summary trace_id=%s simulation_id=%s user_id=%s success=false "
"status=not_found latency_ms=%.2f citation_count=0 caveat_count=0",
trace_id,
sim_id,
user.id,
duration_ms,
)
raise HTTPException(status_code=404, detail="Simulation not found")

summary = build_simulation_summary(simulation)
summary = summary.model_copy(update={"trace_id": trace_id})

duration_ms = (perf_counter() - start) * 1000
logger.info(
"simulation_summary trace_id=%s simulation_id=%s user_id=%s success=true "
"latency_ms=%.2f citation_count=%d caveat_count=%d",
trace_id,
simulation.id,
user.id,
duration_ms,
len(summary.citations),
len(summary.caveats),
)

return summary
52 changes: 52 additions & 0 deletions backend/app/features/assistant/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import Literal
from uuid import UUID

from pydantic import Field

from app.common.schemas.base import CamelOutBaseModel


class SummaryCitationOut(CamelOutBaseModel):
"""Metadata citation for a deterministic simulation summary."""

source_type: Literal[
"simulation_field",
"case_field",
"machine_field",
"artifact",
"external_link",
] = Field(..., description="Kind of SimBoard record referenced by the summary.")
path: str = Field(
...,
description="Stable field path or related-record selector used by the summary.",
)
label: str = Field(..., description="Human-readable label for the cited source.")


class SimulationSummaryResponse(CamelOutBaseModel):
"""Structured response returned by the deterministic summary endpoint."""

answer: str = Field(
..., description="Deterministic summary prose for the simulation."
)
citations: list[SummaryCitationOut] = Field(
default_factory=list,
description="Metadata citations backing claims in the answer.",
)
assumptions: list[str] = Field(
default_factory=list,
description="Explicit assumptions used by the formatter.",
)
caveats: list[str] = Field(
default_factory=list,
description="Missing-data or weak-signal warnings for the summary.",
)
limitations: list[str] = Field(
default_factory=list,
description="Known limits of this deterministic summary implementation.",
)
suggested_followups: list[str] = Field(
default_factory=list,
description="Non-agentic follow-up checks derived from available metadata.",
)
trace_id: UUID = Field(..., description="Trace ID for request review and logs.")
Loading
Loading