from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, ConfigDict, Field, model_validator # Notebook models class NotebookCreate(BaseModel): name: str = Field(..., description="Name of the notebook") description: str = Field(default="", description="Description of the notebook") class NotebookUpdate(BaseModel): name: Optional[str] = Field(None, description="Name of the notebook") description: Optional[str] = Field(None, description="Description of the notebook") archived: Optional[bool] = Field( None, description="Whether the notebook is archived" ) class NotebookResponse(BaseModel): id: str name: str description: str archived: bool created: str updated: str source_count: int note_count: int # Search models class SearchRequest(BaseModel): query: str = Field(..., description="Search query") type: Literal["text", "vector"] = Field("text", description="Search type") limit: int = Field(100, description="Maximum number of results", le=1000) search_sources: bool = Field(True, description="Include sources in search") search_notes: bool = Field(True, description="Include notes in search") minimum_score: float = Field( 0.2, description="Minimum score for vector search", ge=0, le=1 ) class SearchResponse(BaseModel): results: List[Dict[str, Any]] = Field(..., description="Search results") total_count: int = Field(..., description="Total number of results") search_type: str = Field(..., description="Type of search performed") class AskRequest(BaseModel): question: str = Field(..., description="Question to ask the knowledge base") strategy_model: str = Field(..., description="Model ID for query strategy") answer_model: str = Field(..., description="Model ID for individual answers") final_answer_model: str = Field(..., description="Model ID for final answer") class DirectAskRequest(BaseModel): """Request for direct AI queries (without RAG)""" question: str = Field(..., description="Question to ask AI") model_id: Optional[str] = Field(None, description="Model ID to use (optional)") class AskResponse(BaseModel): answer: str = Field(..., description="Final answer from the knowledge base") question: str = Field(..., description="Original question") # Models API models class ModelCreate(BaseModel): name: str = Field(..., description="Model name (e.g., gpt-5-mini, claude, gemini)") provider: str = Field( ..., description="Provider name (e.g., openai, anthropic, gemini)" ) type: str = Field( ..., description="Model type (language, embedding, text_to_speech, speech_to_text)", ) class ModelResponse(BaseModel): id: str name: str provider: str type: str created: str updated: str class DefaultModelsResponse(BaseModel): default_chat_model: Optional[str] = None default_transformation_model: Optional[str] = None large_context_model: Optional[str] = None default_text_to_speech_model: Optional[str] = None default_speech_to_text_model: Optional[str] = None default_embedding_model: Optional[str] = None default_tools_model: Optional[str] = None class ProviderAvailabilityResponse(BaseModel): available: List[str] = Field(..., description="List of available providers") unavailable: List[str] = Field(..., description="List of unavailable providers") supported_types: Dict[str, List[str]] = Field( ..., description="Provider to supported model types mapping" ) # Transformations API models class TransformationCreate(BaseModel): name: str = Field(..., description="Transformation name") title: str = Field(..., description="Display title for the transformation") description: str = Field( ..., description="Description of what this transformation does" ) prompt: str = Field(..., description="The transformation prompt") apply_default: bool = Field( False, description="Whether to apply this transformation by default" ) class TransformationUpdate(BaseModel): name: Optional[str] = Field(None, description="Transformation name") title: Optional[str] = Field( None, description="Display title for the transformation" ) description: Optional[str] = Field( None, description="Description of what this transformation does" ) prompt: Optional[str] = Field(None, description="The transformation prompt") apply_default: Optional[bool] = Field( None, description="Whether to apply this transformation by default" ) class TransformationResponse(BaseModel): id: str name: str title: str description: str prompt: str apply_default: bool created: str updated: str class TransformationExecuteRequest(BaseModel): model_config = ConfigDict(protected_namespaces=()) transformation_id: str = Field( ..., description="ID of the transformation to execute" ) input_text: str = Field(..., description="Text to transform") model_id: str = Field(..., description="Model ID to use for the transformation") class TransformationExecuteResponse(BaseModel): model_config = ConfigDict(protected_namespaces=()) output: str = Field(..., description="Transformed text") transformation_id: str = Field(..., description="ID of the transformation used") model_id: str = Field(..., description="Model ID used") # Default Prompt API models class DefaultPromptResponse(BaseModel): transformation_instructions: str = Field( ..., description="Default transformation instructions" ) class DefaultPromptUpdate(BaseModel): transformation_instructions: str = Field( ..., description="Default transformation instructions" ) # Notes API models class NoteCreate(BaseModel): title: Optional[str] = Field(None, description="Note title") content: str = Field(..., description="Note content") note_type: Optional[str] = Field("human", description="Type of note (human, ai)") notebook_id: Optional[str] = Field( None, description="Notebook ID to add the note to" ) class NoteUpdate(BaseModel): title: Optional[str] = Field(None, description="Note title") content: Optional[str] = Field(None, description="Note content") note_type: Optional[str] = Field(None, description="Type of note (human, ai)") class NoteResponse(BaseModel): id: str title: Optional[str] content: Optional[str] note_type: Optional[str] created: str updated: str # Embedding API models class EmbedRequest(BaseModel): item_id: str = Field(..., description="ID of the item to embed") item_type: str = Field(..., description="Type of item (source, note)") async_processing: bool = Field( False, description="Process asynchronously in background" ) class EmbedResponse(BaseModel): success: bool = Field(..., description="Whether embedding was successful") message: str = Field(..., description="Result message") item_id: str = Field(..., description="ID of the item that was embedded") item_type: str = Field(..., description="Type of item that was embedded") command_id: Optional[str] = Field( None, description="Command ID for async processing" ) # Rebuild request/response models class RebuildRequest(BaseModel): mode: Literal["existing", "all"] = Field( ..., description="Rebuild mode: 'existing' only re-embeds items with embeddings, 'all' embeds everything", ) include_sources: bool = Field(True, description="Include sources in rebuild") include_notes: bool = Field(True, description="Include notes in rebuild") include_insights: bool = Field(True, description="Include insights in rebuild") class RebuildResponse(BaseModel): command_id: str = Field(..., description="Command ID to track progress") total_items: int = Field(..., description="Estimated number of items to process") message: str = Field(..., description="Status message") class RebuildProgress(BaseModel): processed: int = Field(..., description="Number of items processed") total: int = Field(..., description="Total items to process") percentage: float = Field(..., description="Progress percentage") class RebuildStats(BaseModel): sources: int = Field(0, description="Sources processed") notes: int = Field(0, description="Notes processed") insights: int = Field(0, description="Insights processed") failed: int = Field(0, description="Failed items") class RebuildStatusResponse(BaseModel): command_id: str = Field(..., description="Command ID") status: str = Field(..., description="Status: queued, running, completed, failed") progress: Optional[RebuildProgress] = None stats: Optional[RebuildStats] = None started_at: Optional[str] = None completed_at: Optional[str] = None error_message: Optional[str] = None # Settings API models class SettingsResponse(BaseModel): default_content_processing_engine_doc: Optional[str] = None default_content_processing_engine_url: Optional[str] = None default_embedding_option: Optional[str] = None auto_delete_files: Optional[str] = None youtube_preferred_languages: Optional[List[str]] = None class SettingsUpdate(BaseModel): default_content_processing_engine_doc: Optional[str] = None default_content_processing_engine_url: Optional[str] = None default_embedding_option: Optional[str] = None auto_delete_files: Optional[str] = None youtube_preferred_languages: Optional[List[str]] = None # Sources API models class AssetModel(BaseModel): file_path: Optional[str] = None url: Optional[str] = None class SourceCreate(BaseModel): # Backward compatibility: support old single notebook_id notebook_id: Optional[str] = Field( None, description="Notebook ID to add the source to (deprecated, use notebooks)" ) # New multi-notebook support notebooks: Optional[List[str]] = Field( None, description="List of notebook IDs to add the source to" ) # Required fields type: str = Field(..., description="Source type: link, upload, or text") url: Optional[str] = Field(None, description="URL for link type") file_path: Optional[str] = Field(None, description="File path for upload type") content: Optional[str] = Field(None, description="Text content for text type") title: Optional[str] = Field(None, description="Source title") transformations: Optional[List[str]] = Field( default_factory=list, description="Transformation IDs to apply" ) embed: bool = Field(False, description="Whether to embed content for vector search") delete_source: bool = Field( False, description="Whether to delete uploaded file after processing" ) # New async processing support async_processing: bool = Field( False, description="Whether to process source asynchronously" ) @model_validator(mode="after") def validate_notebook_fields(self): # Ensure only one of notebook_id or notebooks is provided if self.notebook_id is not None and self.notebooks is not None: raise ValueError( "Cannot specify both 'notebook_id' and 'notebooks'. Use 'notebooks' for multi-notebook support." ) # Convert single notebook_id to notebooks array for internal processing if self.notebook_id is not None: self.notebooks = [self.notebook_id] # Keep notebook_id for backward compatibility in response # Set empty array if no notebooks specified (allow sources without notebooks) if self.notebooks is None: self.notebooks = [] return self class SourceUpdate(BaseModel): title: Optional[str] = Field(None, description="Source title") topics: Optional[List[str]] = Field(None, description="Source topics") class SourceResponse(BaseModel): id: str title: Optional[str] topics: Optional[List[str]] asset: Optional[AssetModel] full_text: Optional[str] embedded: bool embedded_chunks: int file_available: Optional[bool] = None created: str updated: str # New fields for async processing command_id: Optional[str] = None status: Optional[str] = None processing_info: Optional[Dict] = None # Notebook associations notebooks: Optional[List[str]] = None class SourceListResponse(BaseModel): id: str title: Optional[str] topics: Optional[List[str]] asset: Optional[AssetModel] embedded: bool # Boolean flag indicating if source has embeddings embedded_chunks: int # Number of embedded chunks insights_count: int created: str updated: str file_available: Optional[bool] = None # Status fields for async processing command_id: Optional[str] = None status: Optional[str] = None processing_info: Optional[Dict[str, Any]] = None # Context API models class ContextConfig(BaseModel): sources: Dict[str, str] = Field( default_factory=dict, description="Source inclusion config {source_id: level}" ) notes: Dict[str, str] = Field( default_factory=dict, description="Note inclusion config {note_id: level}" ) class ContextRequest(BaseModel): notebook_id: str = Field(..., description="Notebook ID to get context for") context_config: Optional[ContextConfig] = Field( None, description="Context configuration" ) class ContextResponse(BaseModel): notebook_id: str sources: List[Dict[str, Any]] = Field(..., description="Source context data") notes: List[Dict[str, Any]] = Field(..., description="Note context data") total_tokens: Optional[int] = Field(None, description="Estimated token count") # Insights API models class SourceInsightResponse(BaseModel): id: str source_id: str insight_type: str content: str created: str updated: str class SaveAsNoteRequest(BaseModel): notebook_id: Optional[str] = Field(None, description="Notebook ID to add note to") class CreateSourceInsightRequest(BaseModel): model_config = ConfigDict(protected_namespaces=()) transformation_id: str = Field(..., description="ID of transformation to apply") model_id: Optional[str] = Field( None, description="Model ID (uses default if not provided)" ) # Source status response class SourceStatusResponse(BaseModel): status: Optional[str] = Field(None, description="Processing status") message: str = Field(..., description="Descriptive message about the status") processing_info: Optional[Dict[str, Any]] = Field( None, description="Detailed processing information" ) command_id: Optional[str] = Field(None, description="Command ID if available") # Error response class ErrorResponse(BaseModel): error: str message: str