"""Strict request and response contracts for the LibBee agent.""" import time from typing import Literal, Optional from pydantic import BaseModel, ConfigDict, Field, field_validator class ChatMessage(BaseModel): model_config = ConfigDict(extra="forbid") role: Literal["user", "assistant"] content: str = Field(min_length=1, max_length=4000) class SearchContextPayload(BaseModel): model_config = ConfigDict(extra="ignore") context_id: str = Field(min_length=1, max_length=80) topic: str = Field(min_length=1, max_length=500) display_topic: Optional[str] = Field(default=None, max_length=500) intent: Literal["search_academic", "search_medical"] = "search_academic" source: Literal["primo", "pubmed"] = "primo" resource_type: Literal["articles", "books", "both"] = "articles" query_field: Literal["any", "title", "creator", "sub"] = "any" sort: Literal["rank", "date_d", "date_a"] = "rank" peer_reviewed: bool = False open_access: bool = False year_from: Optional[str] = Field(default=None, pattern=r"^\d{4}$") year_to: Optional[str] = Field(default=None, pattern=r"^\d{4}$") ai_tool_query: Optional[str] = Field(default=None, max_length=1000) primo_boolean_query: Optional[str] = Field(default=None, max_length=1500) created_at: float = Field(default_factory=time.time) clarification_needed: bool = False clarification_message: Optional[str] = Field(default=None, max_length=500) @field_validator("year_to") @classmethod def years_in_order(cls, value: Optional[str], info): start = info.data.get("year_from") if value and start and int(value) < int(start): raise ValueError("year_to must not be earlier than year_from") return value class ClientStatePayload(BaseModel): model_config = ConfigDict(extra="ignore") session_id: Optional[str] = Field(default=None, pattern=r"^[A-Za-z0-9_-]{8,80}$") active_search_context_id: Optional[str] = Field(default=None, max_length=80) recent_search_contexts: list[SearchContextPayload] = Field(default_factory=list, max_length=5) follow_up_action: Optional[str] = Field(default=None, max_length=80) follow_up_context_id: Optional[str] = Field(default=None, max_length=80) class AgentRequest(BaseModel): model_config = ConfigDict(extra="forbid") question: str = Field(min_length=1, max_length=2000) model: Literal["gpt", "claude"] = "gpt" history: list[ChatMessage] = Field(default_factory=list, max_length=12) client_state: Optional[ClientStatePayload] = None class AgentResponse(BaseModel): model_config = ConfigDict(extra="ignore") request_id: str = "" answer: str intent: str = "general" tools_used: list[str] = Field(default_factory=list) search_results: list[dict] = Field(default_factory=list) sources: list[dict] = Field(default_factory=list) model_used: str = "gpt" response_time: float = 0 corrected_query: Optional[str] = None natural_query: Optional[str] = None database_query: Optional[str] = None ask_librarian: Optional[bool] = None is_follow_up: bool = False follow_up_question: Optional[str] = None follow_up_suggestions: list[dict] = Field(default_factory=list) search_context: Optional[dict] = None ai_tool_query: Optional[str] = None primo_boolean_query: Optional[str] = None primo_search_url: Optional[str] = None primo_ai_url: Optional[str] = None pubmed_search_url: Optional[str] = None