Phase 2: Add reasoning and TipTap content endpoints

- 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
This commit is contained in:
Motoko
2026-03-30 23:11:44 +00:00
parent 0645b9c59c
commit bbbe42358d
5 changed files with 880 additions and 7 deletions

View File

@@ -1,6 +1,15 @@
from datetime import datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
class ReasoningType(str, Enum):
CHAIN = "chain"
IDEA = "idea"
CONTEXT = "context"
REFLECTION = "reflection"
class DocumentCreate(BaseModel):
@@ -18,6 +27,19 @@ 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
@@ -26,6 +48,67 @@ class TagInfo(BaseModel):
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
@@ -36,8 +119,14 @@ class DocumentResponse(BaseModel):
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}
model_config = {"from_attributes": True, "protected_namespaces": ()}
class DocumentBriefResponse(BaseModel):