diff --git a/Dockerfile b/Dockerfile index 016aaa8..57ba4e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,30 @@ -FROM quay.ocp.banorte.com/golden/python-312:latest +FROM quay.ocp.banorte.com/golden/python-312:latest AS builder -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.7.12 /uv /uvx /bin/ + +ENV UV_COMPILE_BYTECODE=1 \ + UV_NO_CACHE=1 \ + UV_NO_DEV=1 \ + UV_LINK_MODE=copy WORKDIR /app -COPY . . +# Install dependencies first (cached layer as long as lockfile doesn't change) +COPY pyproject.toml uv.lock ./ +RUN uv sync --locked --no-install-project --no-editable -RUN uv sync +# Copy the rest of the project and install it +COPY . . +RUN uv sync --locked --no-editable + +# --- Final stage: no uv, no build artifacts --- +FROM quay.ocp.banorte.com/golden/python-312:latest + +WORKDIR /app + +COPY --from=builder /app/.venv /app/.venv +COPY config.yaml ./ ENV PATH="/app/.venv/bin:$PATH" -CMD ["uv", "run", "uvicorn", "rag_eval.server:app", "--host", "0.0.0.0"] +CMD ["uvicorn", "va_agent.server:app", "--host", "0.0.0.0"] diff --git a/src/va_agent/server.py b/src/va_agent/server.py index 91b8bbd..63d93cf 100644 --- a/src/va_agent/server.py +++ b/src/va_agent/server.py @@ -1,10 +1,142 @@ -"""FastAPI server exposing the RAG agent endpoint. +"""FastAPI server exposing the RAG agent endpoint.""" -NOTE: This file is a stub. The rag_eval module was removed in the -lean MCP implementation. This file is kept for reference but is not -functional. -""" +from __future__ import annotations -from fastapi import FastAPI +import logging +import uuid +from typing import Any -app = FastAPI(title="RAG Agent") +from fastapi import FastAPI, HTTPException +from google.genai.types import Content, Part +from pydantic import BaseModel, Field + +from va_agent.agent import runner + +logger = logging.getLogger(__name__) + +app = FastAPI(title="Vaia Agent") + + +# --------------------------------------------------------------------------- +# Request / Response models +# --------------------------------------------------------------------------- + + +class NotificationPayload(BaseModel): + """Notification context sent alongside a user query.""" + + text: str | None = None + parameters: dict[str, Any] = Field(default_factory=dict) + + +class QueryRequest(BaseModel): + """Incoming query request from the integration layer.""" + + phone_number: str + text: str + type: str = "conversation" + notification: NotificationPayload | None = None + language_code: str = "es" + + +class QueryResponse(BaseModel): + """Response returned to the integration layer.""" + + response_id: str + response_text: str + parameters: dict[str, Any] = Field(default_factory=dict) + confidence: float | None = None + + +class ErrorResponse(BaseModel): + """Standard error body.""" + + error: str + message: str + status: int + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _build_user_message(request: QueryRequest) -> str: + """Compose the text sent to the agent, including notification context.""" + if request.type == "notification" and request.notification: + parts = [request.text] + if request.notification.text: + parts.append( + f"\n[Notificación recibida]: {request.notification.text}" + ) + if request.notification.parameters: + formatted = ", ".join( + f"{k}: {v}" + for k, v in request.notification.parameters.items() + ) + parts.append(f"[Parámetros de notificación]: {formatted}") + return "\n".join(parts) + return request.text + + +# --------------------------------------------------------------------------- +# Endpoints +# --------------------------------------------------------------------------- + + +@app.post( + "/api/v1/query", + response_model=QueryResponse, + responses={ + 400: {"model": ErrorResponse}, + 500: {"model": ErrorResponse}, + 503: {"model": ErrorResponse}, + }, +) +async def query(request: QueryRequest) -> QueryResponse: + """Process a user message and return a generated response.""" + user_message = _build_user_message(request) + session_id = request.phone_number + user_id = request.phone_number + + new_message = Content( + role="user", + parts=[Part(text=user_message)], + ) + + try: + response_text = "" + async for event in runner.run_async( + user_id=user_id, + session_id=session_id, + new_message=new_message, + ): + if event.content and event.content.parts: + for part in event.content.parts: + if part.text and event.author != "user": + response_text += part.text + except ValueError as exc: + logger.exception("Bad request while running agent") + raise HTTPException( + status_code=400, + detail=ErrorResponse( + error="Bad Request", + message=str(exc), + status=400, + ).model_dump(), + ) from exc + except Exception as exc: + logger.exception("Internal error while running agent") + raise HTTPException( + status_code=500, + detail=ErrorResponse( + error="Internal Server Error", + message="Failed to generate response", + status=500, + ).model_dump(), + ) from exc + + return QueryResponse( + response_id=f"rag-resp-{uuid.uuid4()}", + response_text=response_text, + )