""" LLM Agent endpoint (Tool Calling, Trader/Tycoon). This endpoint runs a small tool loop: - LLM requests tools via strict JSON - Backend executes read-only tools against live DB/API helpers - Final answer is streamed to the client (SSE) using the gateway """ from __future__ import annotations from typing import Any, Literal, Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.database import get_db from app.models.user import User from app.services.llm_agent import run_agent, stream_final_answer router = APIRouter(prefix="/llm", tags=["LLM"]) class ChatMessage(BaseModel): role: Literal["system", "user", "assistant"] content: str class AgentRequest(BaseModel): messages: list[ChatMessage] = Field(default_factory=list, min_length=1) path: str = Field(default="/terminal/hunt") model: Optional[str] = None temperature: float = Field(default=0.7, ge=0.0, le=2.0) stream: bool = True @router.post("/agent") async def llm_agent( payload: AgentRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): try: convo = await run_agent( db, current_user, messages=[m.model_dump() for m in payload.messages], path=payload.path, model=payload.model, temperature=payload.temperature, ) except PermissionError as e: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"Agent failed: {type(e).__name__}: {e}") if payload.stream: return StreamingResponse( stream_final_answer(convo, model=payload.model, temperature=payload.temperature), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, ) # Non-stream fallback: produce final answer in one shot by asking the model again. # (kept simple; streaming path is preferred) return JSONResponse({"ok": True, "messages": convo[-8:]})