""" IntakeAgent endpoint — POST /api/intake. Self-gating: decides whether a build prompt is specific enough to build directly. If not, asks ONE adaptive clarifying question at a time (with selectable options) until it has enough context, then emits a consolidated build brief. Stateless: the FE passes the full Q&A history on every call. No DB writes. """ import logging from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from agent.llm import PLANNER_CHAIN, chat_structured from agent.prompts import intake_prompt from agent.states import IntakeStep from auth.dependencies import get_current_user from db.models import User logger = logging.getLogger(__name__) router = APIRouter(prefix="/intake", tags=["intake"]) # ponytail: simple guard — never ask more than this many questions before forcing a build. _MAX_HISTORY = 5 class QAPair(BaseModel): question: str answer: str class IntakeRequest(BaseModel): prompt: str history: list[QAPair] = Field(default_factory=list) session_id: str | None = None # ponytail: kept for backward compat, not used def _synthesize_brief(prompt: str, history: list[QAPair]) -> str: """Fold the original prompt + all answers into a single brief (cap fallback).""" answers = "; ".join(h.answer.strip() for h in history if h.answer.strip()) return f"{prompt.strip()} ({answers})" if answers else prompt.strip() @router.post("", response_model=IntakeStep) def intake( req: IntakeRequest, current_user: User = Depends(get_current_user), ) -> IntakeStep: if not req.prompt.strip(): raise HTTPException(status_code=400, detail="prompt must not be empty") history = [h.model_dump() for h in req.history] step = chat_structured( intake_prompt(req.prompt, history), IntakeStep, chain=PLANNER_CHAIN, # ponytail: 2000 (not 600) so reasoning-model fallbacks have room to emit the JSON # after their thinking tokens — 600 left them returning no JSON. max_tokens=2000, ) # HARD CAP: stop asking after enough questions — force a build. # ponytail: simple guard against an indecisive model looping forever. # Carry the intake's framework decision through the fallback paths (default "react"). if len(req.history) >= _MAX_HISTORY and not step.ready: brief = step.brief or _synthesize_brief(req.prompt, req.history) return IntakeStep(ready=True, brief=brief, framework=step.framework) # If the model says ready but gave no brief, fall back to the prompt. if step.ready and not (step.brief and step.brief.strip()): return IntakeStep(ready=True, brief=req.prompt.strip(), framework=step.framework) return step