open-notebook/api/models.py
Luis Novo df0986cee0
Feature/oss 312 notebook item counts (#175)
* fix: small issue where users cant change podcast segments

* feat: display source and note counts on notebook cards (OSS-312)

Add item counters to notebook listing page showing the number of sources
and notes in each notebook. Counts are displayed in a footer section with
FileText and StickyNote icons for visual consistency with ContextIndicator.

Backend changes:
- Add source_count and note_count to NotebookResponse model
- Update /notebooks endpoint to use SurrealDB graph traversal query
- Query: count(<-reference.in) for sources, count(<-artifact.in) for notes
- Update all notebook endpoints to include counts

Frontend changes:
- Add source_count and note_count to TypeScript NotebookResponse interface
- Add footer section to NotebookCard component
- Display counts with FileText and StickyNote icons (h-3 w-3)
- Use border-top separator and muted-foreground styling

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* style: use colorful badges for notebook counts matching ContextIndicator

Update notebook card counts to use Badge components with primary color
styling instead of plain text, matching the visual style of the
ContextIndicator component in the chat window.

Changes:
- Replace plain text divs with Badge components
- Apply text-primary and border-primary/50 styling
- Use same spacing (gap-1.5, px-1.5, py-0.5) as ContextIndicator
- Remove bullet separator (not needed with badge layout)

Visual result matches the colorful badges shown in chat context.

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-19 16:03:36 -03:00

422 lines
14 KiB
Python

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 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
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