""" Hunter Companion API Endpoint This is the main endpoint for the Hunter Companion chat. Uses code-first architecture: intent detection via pattern matching, tool execution, and template-based responses. LLM is NOT used for routing. """ from __future__ import annotations import json from typing import Any, Literal, Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user from app.database import get_db from app.models.subscription import Subscription from app.models.user import User from app.services.hunter_companion import process_message router = APIRouter(prefix="/llm", tags=["LLM"]) def _tier_level(tier: str) -> int: t = (tier or "").lower() if t == "tycoon": return 3 if t == "trader": return 2 return 1 async def _get_user_tier(db: AsyncSession, user: User) -> str: res = await db.execute(select(Subscription).where(Subscription.user_id == user.id)) sub = res.scalar_one_or_none() if not sub: return "scout" return sub.tier.value 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.3, ge=0.0, le=2.0) stream: bool = True async def _generate_sse_response(content: str): """Generate SSE-formatted response chunks.""" # Split content into chunks for streaming effect chunk_size = 20 for i in range(0, len(content), chunk_size): chunk = content[i:i + chunk_size] data = {"choices": [{"delta": {"content": chunk}}]} yield f"data: {json.dumps(data)}\n\n".encode() yield b"data: [DONE]\n\n" @router.post("/agent") async def hunter_companion_chat( payload: AgentRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """ Hunter Companion Chat Endpoint - Trader/Tycoon: Full access to all features - Scout: Blocked (403) """ # Check tier tier = await _get_user_tier(db, current_user) if _tier_level(tier) < 2: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Hunter Companion requires Trader or Tycoon plan." ) # Get the last user message user_messages = [m for m in payload.messages if m.role == "user"] if not user_messages: raise HTTPException(status_code=400, detail="No user message provided") last_message = user_messages[-1].content # Process the message (code-first, no LLM for routing) try: response = await process_message( db=db, user=current_user, message=last_message, path=payload.path, ) except Exception as e: raise HTTPException( status_code=500, detail=f"Processing failed: {type(e).__name__}: {e}" ) # Return as SSE stream (for frontend compatibility) if payload.stream: return StreamingResponse( _generate_sse_response(response), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, ) # Non-stream response return {"content": response}