pounce/backend/app/api/llm_agent.py
Yves Gugger 8f6e13ffcf
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
LLM Agent: tool-calling endpoint + HunterCompanion uses /llm/agent
2025-12-17 14:30:25 +01:00

73 lines
2.3 KiB
Python

"""
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:]})