Files
claudia-docs-api/app/schemas/document.py
Motoko 204badb964 feat: Add role-based API tokens for Claudia Docs
- Add api_tokens table with role-based access (researcher, developer, viewer)
- Add POST /auth/token/generate endpoint for creating tokens
- Add GET /auth/tokens endpoint for listing user's tokens
- Add DELETE /auth/tokens/{token_id} endpoint for revoking tokens
- Add agent_type field to documents (research, development, general)
- Implement role-based access control for documents:
  - researcher: access to research and general documents
  - developer: access to development and general documents
  - viewer: read-only access
- Update document model and schemas with agent_type field
- Add comprehensive tests for API token functionality
- All existing tests pass (73 total)
2026-03-31 01:46:51 +00:00

268 lines
6.5 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
agent_type: str | None = "general" # research, development, general
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]
# =============================================================================
# Phase 3: Link Detection & Graph Schemas
# =============================================================================
class DetectLinksRequest(BaseModel):
content: str = Field(..., max_length=5_000_000) # ~5MB limit
class BrokenLink(BaseModel):
reference: str
reason: str # "document_not_found" | "invalid_format"
class DetectLinksResponse(BaseModel):
document_id: str
outgoing_links: list[str]
links_detected: int
links_broken: int
broken_links: list[BrokenLink] = []
class BacklinkItem(BaseModel):
document_id: str
title: str
project_id: str
project_name: str
excerpt: str
updated_at: datetime
class BacklinksResponse(BaseModel):
document_id: str
backlinks_count: int
backlinks: list[BacklinkItem]
class OutgoingLinkItem(BaseModel):
document_id: str
title: str
project_id: str
project_name: str
exists: bool
updated_at: datetime | None
class OutgoingLinksResponse(BaseModel):
document_id: str
outgoing_links_count: int
outgoing_links: list[OutgoingLinkItem]
class LinkItem(BaseModel):
document_id: str
title: str
anchor_text: str | None = None
class LinksResponse(BaseModel):
document_id: str
outgoing_links: list[LinkItem]
backlinks: list[LinkItem]
class GraphNode(BaseModel):
id: str
title: str
type: str = "document"
class GraphEdge(BaseModel):
source: str
target: str
type: str = "reference"
class GraphStats(BaseModel):
total_documents: int
total_references: int
orphaned_documents: int
class GraphResponse(BaseModel):
project_id: str
nodes: list[GraphNode]
edges: list[GraphEdge]
stats: GraphStats
# =============================================================================
# Phase 3: Export Schemas
# =============================================================================
class DocumentExportResponse(BaseModel):
"""Used for JSON export format."""
id: str
title: str
content: str
tiptap_content: dict[str, Any] | None = None
created_at: datetime
updated_at: datetime
metadata: dict[str, Any] = Field(default_factory=dict)
class ProjectExportDocument(BaseModel):
id: str
title: str
content: str
tiptap_content: dict[str, Any] | None = None
outgoing_links: list[str] = []
metadata: dict[str, Any] = Field(default_factory=dict)
class ProjectExportResponse(BaseModel):
project: dict[str, Any]
documents: list[ProjectExportDocument]
exported_at: datetime
format_version: str = "3.0"