- Extend Document model with reasoning_type, confidence, reasoning_steps, model_source, tiptap_content fields
- Add new endpoints:
- GET /documents/{id}/reasoning - Get reasoning metadata
- PATCH /documents/{id}/reasoning - Update reasoning metadata
- GET /documents/{id}/reasoning-panel - Get reasoning panel data for UI
- POST /documents/{id}/reasoning-steps - Add reasoning step
- DELETE /documents/{id}/reasoning-steps/{step} - Delete reasoning step
- GET /documents/{id}/content?format=tiptap|markdown - Get content in TipTap or Markdown
- PUT /documents/{id}/content - Update content (supports both TipTap JSON and Markdown)
- Add TipTap to Markdown conversion
- Update database schema with new columns
- Add comprehensive tests for all new endpoints
- All 37 tests passing
148 lines
3.8 KiB
Python
148 lines
3.8 KiB
Python
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class ReasoningType(str, Enum):
|
|
CHAIN = "chain"
|
|
IDEA = "idea"
|
|
CONTEXT = "context"
|
|
REFLECTION = "reflection"
|
|
|
|
|
|
class DocumentCreate(BaseModel):
|
|
title: str
|
|
content: str = ""
|
|
folder_id: str | None = None
|
|
|
|
|
|
class DocumentUpdate(BaseModel):
|
|
title: str | None = None
|
|
folder_id: str | None = None
|
|
|
|
|
|
class DocumentContentUpdate(BaseModel):
|
|
content: str = Field(..., max_length=1_000_000) # 1MB limit
|
|
|
|
|
|
# Phase 2: TipTap Content Update - Union to support both legacy string and new TipTap/Markdown formats
|
|
class TipTapContentUpdate(BaseModel):
|
|
content: dict[str, Any] | str # TipTap JSON object or Markdown string
|
|
format: str = "tiptap"
|
|
|
|
@field_validator("format")
|
|
@classmethod
|
|
def validate_format(cls, v: str) -> str:
|
|
if v not in ("tiptap", "markdown"):
|
|
raise ValueError("format must be 'tiptap' or 'markdown'")
|
|
return v
|
|
|
|
|
|
class TagInfo(BaseModel):
|
|
id: str
|
|
name: str
|
|
color: str
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# Phase 2: Reasoning Step Schema
|
|
class ReasoningStep(BaseModel):
|
|
step: int
|
|
thought: str = Field(..., max_length=2000)
|
|
conclusion: str | None = Field(None, max_length=2000)
|
|
|
|
|
|
# Phase 2: Reasoning Metadata Schema
|
|
class ReasoningMetadata(BaseModel):
|
|
reasoning_type: ReasoningType | None = None
|
|
confidence: float | None = None
|
|
reasoning_steps: list[ReasoningStep] = []
|
|
model_source: str | None = None
|
|
|
|
model_config = {"protected_namespaces": ()}
|
|
|
|
@field_validator("confidence")
|
|
@classmethod
|
|
def validate_confidence(cls, v: float | None) -> float | None:
|
|
if v is not None and (v < 0.0 or v > 1.0):
|
|
raise ValueError("confidence must be between 0.0 and 1.0")
|
|
return v
|
|
|
|
|
|
# Phase 2: Reasoning Update Schema
|
|
class ReasoningUpdate(BaseModel):
|
|
reasoning_type: ReasoningType | None = None
|
|
confidence: float | None = None
|
|
reasoning_steps: list[ReasoningStep] | None = None
|
|
model_source: str | None = None
|
|
|
|
model_config = {"protected_namespaces": ()}
|
|
|
|
@field_validator("confidence")
|
|
@classmethod
|
|
def validate_confidence(cls, v: float | None) -> float | None:
|
|
if v is not None and (v < 0.0 or v > 1.0):
|
|
raise ValueError("confidence must be between 0.0 and 1.0")
|
|
return v
|
|
|
|
|
|
# Phase 2: Add Reasoning Step Schema
|
|
class ReasoningStepAdd(BaseModel):
|
|
thought: str = Field(..., min_length=1, max_length=2000)
|
|
conclusion: str | None = Field(None, max_length=2000)
|
|
|
|
|
|
# Phase 2: TipTap Content Response
|
|
class TipTapContentResponse(BaseModel):
|
|
content: dict[str, Any] | str
|
|
format: str
|
|
|
|
|
|
# Phase 2: Reasoning Panel Schema
|
|
class ReasoningPanel(BaseModel):
|
|
document_id: str
|
|
has_reasoning: bool
|
|
reasoning: ReasoningMetadata | None = None
|
|
editable: bool = True
|
|
|
|
|
|
class DocumentResponse(BaseModel):
|
|
id: str
|
|
title: str
|
|
content: str
|
|
project_id: str
|
|
folder_id: str | None
|
|
path: str
|
|
tags: list[TagInfo] = []
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
# Phase 2: new fields
|
|
reasoning_type: ReasoningType | None = None
|
|
confidence: float | None = None
|
|
reasoning_steps: list[ReasoningStep] = []
|
|
model_source: str | None = None
|
|
tiptap_content: dict[str, Any] | None = None
|
|
|
|
model_config = {"from_attributes": True, "protected_namespaces": ()}
|
|
|
|
|
|
class DocumentBriefResponse(BaseModel):
|
|
"""Brief document for list views without content."""
|
|
id: str
|
|
title: str
|
|
project_id: str
|
|
folder_id: str | None
|
|
path: str
|
|
tags: list[TagInfo] = []
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class DocumentListResponse(BaseModel):
|
|
documents: list[DocumentBriefResponse]
|