Files
NotesAgentic/backend/app/contracts.py
T

912 lines
23 KiB
Python

from datetime import datetime
from enum import Enum
from typing import Annotated, Any, Literal
from pydantic import BaseModel, ConfigDict, Field, SecretStr
class Contract(BaseModel):
model_config = ConfigDict(extra="forbid")
class PageMeta(Contract):
total: int = 0
limit: int = 50
offset: int = 0
class ErrorDetail(Contract):
code: str
message: str
details: dict[str, Any] = Field(default_factory=dict)
class ErrorResponse(Contract):
error: ErrorDetail
class OperationResponse(Contract):
status: Literal["accepted", "completed"]
resource_id: str | None = None
message: str | None = None
# Workspace boundary (single configured Vault in Web development mode)
class WorkspaceInfo(Contract):
vault_id: str = "default"
name: str
path: str
file_count: int = 0
indexed_note_count: int = 0
requires_refresh: bool = False
class WorkspaceEntry(Contract):
entry_id: str
name: str
path: str
type: Literal["file", "folder"]
note_id: str | None = None
children: list["WorkspaceEntry"] = Field(default_factory=list)
class WorkspaceSnapshot(Contract):
workspace: WorkspaceInfo
items: list[WorkspaceEntry] = Field(default_factory=list)
class WorkspaceOpenRequest(Contract):
path: str | None = None
class FolderCreateRequest(Contract):
parent: str = ""
name: str = Field(min_length=1)
class FolderRenameRequest(Contract):
path: str
new_name: str = Field(min_length=1)
class FolderDeleteRequest(Contract):
path: str
# Notes and retrieval
class NoteBlock(Contract):
block_id: str
note_id: str
heading_path: list[str] = Field(default_factory=list)
start_offset: int
end_offset: int
content: str
content_hash: str
token_count: int
class NoteSummary(Contract):
note_id: str
title: str
file_path: str
tags: list[str] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class Note(NoteSummary):
markdown: str
blocks: list[NoteBlock] = Field(default_factory=list)
class NoteListResponse(Contract):
items: list[NoteSummary] = Field(default_factory=list)
page: PageMeta = Field(default_factory=PageMeta)
class NoteCreateRequest(Contract):
title: str = Field(min_length=1)
markdown: str = ""
folder: str | None = None
tags: list[str] = Field(default_factory=list)
class NoteUpdateRequest(Contract):
title: str | None = None
markdown: str | None = None
tags: list[str] | None = None
class NoteMoveRequest(Contract):
folder: str
class NoteRenameRequest(Contract):
file_name: str = Field(min_length=1)
class SearchMode(str, Enum):
fts = "fts"
vector = "vector"
hybrid = "hybrid"
class SearchRequest(Contract):
query: str = Field(min_length=1)
mode: SearchMode = SearchMode.hybrid
folders: list[str] = Field(default_factory=list)
note_ids: list[str] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)
created_from: datetime | None = None
created_to: datetime | None = None
updated_from: datetime | None = None
updated_to: datetime | None = None
limit: int = Field(default=20, ge=1, le=100)
offset: int = Field(default=0, ge=0)
include_snippet: bool = True
class Citation(Contract):
citation_id: str
note_id: str
block_id: str
file_path: str
heading_path: list[str] = Field(default_factory=list)
start_offset: int | None = None
end_offset: int | None = None
source_audio: str | None = None
start_time: float | None = None
end_time: float | None = None
speaker: str | None = None
class SearchResult(Contract):
note_id: str
block_id: str
title: str
file_path: str
heading_path: list[str] = Field(default_factory=list)
snippet: str | None = None
score: float
citation: Citation
class SearchResponse(Contract):
query: str
mode: SearchMode
items: list[SearchResult] = Field(default_factory=list)
page: PageMeta = Field(default_factory=PageMeta)
# Model, chat and tools
class MessageRole(str, Enum):
system = "system"
user = "user"
assistant = "assistant"
tool = "tool"
class Message(Contract):
role: MessageRole
content: str
name: str | None = None
tool_call_id: str | None = None
tool_calls: list["ToolCall"] = Field(default_factory=list)
class ToolDefinition(Contract):
name: str
description: str
parameters: dict[str, Any] = Field(default_factory=dict)
permission: str | None = None
source: Literal["builtin", "plugin", "mcp_server"] = "builtin"
class ToolCall(Contract):
tool_call_id: str
name: str
arguments: dict[str, Any] = Field(default_factory=dict)
class ToolResult(Contract):
tool_call_id: str
name: str
success: bool
output: Any | None = None
error_code: str | None = None
error_message: str | None = None
duration_ms: int | None = None
class ToolListResponse(Contract):
items: list[ToolDefinition] = Field(default_factory=list)
class ModelCapability(str, Enum):
chat = "chat"
vision = "vision"
tool_calling = "tool_calling"
reasoning = "reasoning"
streaming = "streaming"
structured_output = "structured_output"
embedding = "embedding"
class ModelRequest(Contract):
provider_id: str
model: str
system: str | None = None
messages: list[Message]
tools: list[ToolDefinition] = Field(default_factory=list)
temperature: float | None = Field(default=None, ge=0, le=2)
max_tokens: int | None = Field(default=None, ge=1)
response_format: dict[str, Any] | None = None
attachments: list[str] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
class ChatRequest(ModelRequest):
conversation_id: str | None = None
use_rag: bool = True
retrieval: SearchRequest | None = None
class ModelEventType(str, Enum):
text_delta = "TextDelta"
thinking_delta = "ThinkingDelta"
tool_call_start = "ToolCallStart"
tool_call_delta = "ToolCallDelta"
tool_call_end = "ToolCallEnd"
usage = "Usage"
error = "Error"
done = "Done"
class ModelEvent(Contract):
event: ModelEventType
sequence: int = 0
data: dict[str, Any] = Field(default_factory=dict)
timestamp: datetime
# Agent
class AgentRunStatus(str, Enum):
queued = "queued"
running = "running"
waiting_permission = "waiting_permission"
completed = "completed"
failed = "failed"
cancelled = "cancelled"
class AgentRunCreateRequest(Contract):
input: str = Field(min_length=1)
provider_id: str
model: str
skill_id: str | None = None
allowed_tools: list[str] = Field(default_factory=list)
max_steps: int = Field(default=10, ge=1, le=100)
tool_timeout_seconds: int = Field(default=30, ge=1)
run_timeout_seconds: int = Field(default=300, ge=1)
token_budget: int | None = Field(default=None, ge=1)
max_concurrent_tools: int = Field(default=1, ge=1)
allow_network: bool = False
metadata: dict[str, Any] = Field(default_factory=dict)
class AgentRun(Contract):
run_id: str
status: AgentRunStatus
input: str
provider_id: str
model: str
skill_id: str | None = None
current_step: int = 0
max_steps: int
token_budget: int | None = None
cancelled: bool = False
output: str | None = None
error_code: str | None = None
error_message: str | None = None
token_usage: int = 0
tool_results: list[ToolResult] = Field(default_factory=list)
citations: list[Citation] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class AgentRunListResponse(Contract):
items: list[AgentRun] = Field(default_factory=list)
page: PageMeta = Field(default_factory=PageMeta)
class AgentEventType(str, Enum):
run_started = "RunStarted"
text_delta = "TextDelta"
thinking_delta = "ThinkingDelta"
tool_call = "ToolCall"
tool_result = "ToolResult"
permission_required = "PermissionRequired"
usage = "Usage"
citation = "Citation"
model_call_started = "ModelCallStarted"
model_call_completed = "ModelCallCompleted"
model_call_failed = "ModelCallFailed"
permission_resolved = "PermissionResolved"
run_completed = "RunCompleted"
run_failed = "RunFailed"
run_cancelled = "RunCancelled"
class AgentEvent(Contract):
event: AgentEventType
run_id: str
sequence: int
data: dict[str, Any] = Field(default_factory=dict)
timestamp: datetime
class AgentTraceSummary(Contract):
model_calls: int = 0
tool_calls: int = 0
duration_ms: int = 0
token_usage: int = 0
errors: int = 0
class AgentTraceResponse(Contract):
run_id: str
status: AgentRunStatus
items: list[AgentEvent] = Field(default_factory=list)
next_sequence: int
has_more: bool = False
summary: AgentTraceSummary = Field(default_factory=AgentTraceSummary)
config_snapshot: dict[str, Any] = Field(default_factory=dict)
class PermissionDecisionRequest(Contract):
decision: Literal["allow_once", "allow_session", "deny"]
# Skills and plugins
class RetrievalConfig(Contract):
top_k: int = Field(default=10, ge=1, le=100)
rerank: bool = True
citation: bool = True
class SkillModelConfig(Contract):
required_capabilities: list[ModelCapability] = Field(default_factory=list)
class SkillManifest(Contract):
skill_id: str
name: str
version: str
description: str = ""
permissions: list[str] = Field(default_factory=list)
tools: list[str] = Field(default_factory=list)
retrieval: RetrievalConfig = Field(default_factory=RetrievalConfig)
model: SkillModelConfig = Field(default_factory=SkillModelConfig)
class SkillStatus(str, Enum):
installed = "installed"
disabled = "disabled"
ready = "ready"
dependency_missing = "dependency_missing"
permission_required = "permission_required"
error = "error"
class Skill(Contract):
manifest: SkillManifest
status: SkillStatus
enabled: bool = False
missing_dependencies: list[str] = Field(default_factory=list)
class SkillListResponse(Contract):
items: list[Skill] = Field(default_factory=list)
class ExtensionInstallRequest(Contract):
package_path: str
class PluginBackend(Contract):
type: Literal["mcp", "internal_rpc", "none"] = "none"
transport: Literal["stdio", "http", "none"] = "none"
command: str | None = None
args: list[str] = Field(default_factory=list)
startup_timeout_seconds: int = Field(default=10, ge=1, le=60)
tool_timeout_seconds: int = Field(default=30, ge=1, le=600)
class PluginContribution(Contract):
tools: list[str] = Field(default_factory=list)
commands: list[str] = Field(default_factory=list)
importers: list[str] = Field(default_factory=list)
exporters: list[str] = Field(default_factory=list)
panels: list[str] = Field(default_factory=list)
settings_sections: list[str] = Field(default_factory=list)
class PluginManifest(Contract):
plugin_id: str
name: str
version: str
description: str = ""
permissions: list[str] = Field(default_factory=list)
contributes: PluginContribution = Field(default_factory=PluginContribution)
backend: PluginBackend = Field(default_factory=PluginBackend)
class PluginStatus(str, Enum):
installed = "installed"
disabled = "disabled"
starting = "starting"
ready = "ready"
error = "error"
dependency_missing = "dependency_missing"
permission_required = "permission_required"
class Plugin(Contract):
manifest: PluginManifest
status: PluginStatus
enabled: bool = False
granted_permissions: list[str] = Field(default_factory=list)
error_message: str | None = None
class PluginListResponse(Contract):
items: list[Plugin] = Field(default_factory=list)
class PluginHostState(str, Enum):
stopped = "stopped"
starting = "starting"
ready = "ready"
unhealthy = "unhealthy"
error = "error"
class PluginHostStatus(Contract):
plugin_id: str
backend_type: Literal["mcp", "internal_rpc", "none"]
transport: Literal["stdio", "http", "none"]
status: PluginHostState
tools_count: int = 0
started_at: datetime | None = None
last_seen_at: datetime | None = None
protocol_version: str | None = None
server_name: str | None = None
server_version: str | None = None
error: str | None = None
# Independent user-managed MCP Server Registry. This is deliberately separate
# from Plugin manifests: a server can contribute tools without being a Plugin.
class McpServerTransport(str, Enum):
stdio = "stdio"
streamable_http = "streamable_http"
sse = "sse"
class McpServerConfig(Contract):
name: str = Field(min_length=1, max_length=80)
transport: McpServerTransport = McpServerTransport.stdio
command: str | None = Field(default=None, max_length=1024)
args: list[str] = Field(default_factory=list, max_length=64)
url: str | None = Field(default=None, max_length=4096)
headers: dict[str, str] = Field(default_factory=dict)
environment: dict[str, str] = Field(default_factory=dict)
secret_environment_keys: list[str] = Field(default_factory=list)
secret_header_keys: list[str] = Field(default_factory=list)
permissions: list[str] = Field(default_factory=list)
startup_timeout_seconds: float = Field(default=15, ge=1, le=120)
tool_timeout_seconds: float = Field(default=30, ge=1, le=300)
class McpServerCreateRequest(McpServerConfig):
pass
class McpServerUpdateRequest(McpServerConfig):
version: int = Field(ge=1)
class McpServerSecretWriteRequest(Contract):
secret: SecretStr = Field(min_length=1, max_length=32768)
class McpServerSecretStatus(Contract):
key: str
configured: bool
class McpServerTrustRequest(Contract):
command_digest: str = Field(min_length=64, max_length=64)
class McpServerStatus(Contract):
enabled: bool = False
status: PluginHostState = PluginHostState.stopped
tools_count: int = 0
protocol_version: str | None = None
remote_server_name: str | None = None
remote_server_version: str | None = None
error: str | None = None
last_tested_at: datetime | None = None
last_test_succeeded: bool | None = None
class McpServer(McpServerStatus):
server_id: str
version: int
name: str
transport: McpServerTransport
command: str | None = None
args: list[str] = Field(default_factory=list)
url: str | None = None
headers: dict[str, str] = Field(default_factory=dict)
environment: dict[str, str] = Field(default_factory=dict)
permissions: list[str] = Field(default_factory=list)
startup_timeout_seconds: float
tool_timeout_seconds: float
secret_environment: dict[str, bool] = Field(default_factory=dict)
secret_headers: dict[str, bool] = Field(default_factory=dict)
trusted: bool = False
command_digest: str
command_summary: str
class McpServerListResponse(Contract):
items: list[McpServer] = Field(default_factory=list)
class McpToolSummary(Contract):
name: str
remote_name: str
description: str
permission: str | None = None
class McpToolSummaryListResponse(Contract):
items: list[McpToolSummary] = Field(default_factory=list)
class PluginCommandLocation(str, Enum):
command_palette = "command_palette"
context_menu = "context_menu"
toolbar = "toolbar"
class PluginCommand(Contract):
command_id: str
plugin_id: str
title: str
description: str = ""
icon: str | None = None
locations: list[PluginCommandLocation] = Field(default_factory=list)
when: list[str] = Field(default_factory=list)
parameters: dict[str, Any] = Field(default_factory=dict)
enabled: bool = True
class PluginCommandListResponse(Contract):
items: list[PluginCommand] = Field(default_factory=list)
class PluginCommandContext(Contract):
vault_id: str | None = None
note_id: str | None = None
file_path: str | None = None
selection: str | None = None
class PluginCommandExecuteRequest(Contract):
arguments: dict[str, Any] = Field(default_factory=dict)
context: PluginCommandContext = Field(default_factory=PluginCommandContext)
class PluginNotificationEffectPayload(Contract):
level: Literal["info", "success", "warning", "error"] = "info"
message: str = Field(min_length=1, max_length=4096)
class PluginNavigateEffectPayload(Contract):
route: Literal[
"vault-entry",
"workspace",
"search",
"chat",
"agent",
"tasks",
"skills",
"plugins",
"themes",
"settings",
]
class PluginRefreshEffectPayload(Contract):
scope: Literal["workspace", "commands", "settings", "plugins"]
class PluginJobEffectPayload(Contract):
job_id: str = Field(
min_length=1,
max_length=128,
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]*$",
)
class PluginNoEffectPayload(Contract):
pass
class PluginNoEffect(Contract):
type: Literal["none"] = "none"
payload: PluginNoEffectPayload = Field(default_factory=PluginNoEffectPayload)
class PluginNotificationEffect(Contract):
type: Literal["notification"] = "notification"
payload: PluginNotificationEffectPayload
class PluginNavigateEffect(Contract):
type: Literal["navigate"] = "navigate"
payload: PluginNavigateEffectPayload
class PluginRefreshEffect(Contract):
type: Literal["refresh"] = "refresh"
payload: PluginRefreshEffectPayload
class PluginJobEffect(Contract):
type: Literal["job"] = "job"
payload: PluginJobEffectPayload
PluginCommandEffect = Annotated[
PluginNoEffect
| PluginNotificationEffect
| PluginNavigateEffect
| PluginRefreshEffect
| PluginJobEffect,
Field(discriminator="type"),
]
PLUGIN_COMMAND_EFFECT_TYPES = (
PluginNoEffect,
PluginNotificationEffect,
PluginNavigateEffect,
PluginRefreshEffect,
PluginJobEffect,
)
class PluginCommandResult(Contract):
command_id: str
status: Literal["completed"] = "completed"
effect: PluginCommandEffect = Field(default_factory=PluginNoEffect)
class PluginSettingType(str, Enum):
string = "string"
number = "number"
boolean = "boolean"
select = "select"
secret = "secret"
class PluginSettingField(Contract):
key: str
label: str
description: str = ""
type: PluginSettingType
required: bool = False
default: Any | None = None
minimum: float | None = None
maximum: float | None = None
options: list[str] = Field(default_factory=list)
class PluginSecretState(Contract):
configured: bool = False
class PluginSettingsSchema(Contract):
plugin_id: str
schema_version: int = Field(ge=1)
fields: list[PluginSettingField] = Field(default_factory=list)
values: dict[str, Any] = Field(default_factory=dict)
secrets: dict[str, PluginSecretState] = Field(default_factory=dict)
class PluginSettingsUpdateRequest(Contract):
schema_version: int = Field(ge=1)
values: dict[str, Any] = Field(default_factory=dict)
class PluginSecretWriteRequest(Contract):
secret: SecretStr
class PluginSecretStatus(Contract):
plugin_id: str
key: str
configured: bool
class PluginPermissionGrantRequest(Contract):
permissions: list[str] = Field(default_factory=list)
# Providers
class ProviderType(str, Enum):
mock = "mock"
openai_responses = "openai_responses"
openai_chat = "openai_chat"
openai_compatible = "openai_compatible"
anthropic_messages = "anthropic_messages"
ollama = "ollama"
class ProviderConfig(Contract):
provider_id: str
provider_type: ProviderType
name: str
base_url: str | None = None
default_model: str | None = None
credential_id: str | None = None
enabled: bool = True
capabilities: list[ModelCapability] = Field(default_factory=list)
class ProviderCreateRequest(Contract):
provider_type: ProviderType
name: str
base_url: str | None = None
default_model: str | None = None
credential_id: str | None = None
enabled: bool = True
class ProviderUpdateRequest(Contract):
name: str | None = None
base_url: str | None = None
default_model: str | None = None
credential_id: str | None = None
enabled: bool | None = None
class ProviderListResponse(Contract):
items: list[ProviderConfig] = Field(default_factory=list)
class ProviderPreset(Contract):
preset_id: str
name: str
provider_type: ProviderType
base_url: str
default_credential_id: str | None = None
requires_credential: bool = True
class ProviderPresetListResponse(Contract):
items: list[ProviderPreset] = Field(default_factory=list)
class CredentialWriteRequest(Contract):
api_key: SecretStr = Field(min_length=1, max_length=8192)
class CredentialStatus(Contract):
credential_id: str
configured: bool
class ModelInfo(Contract):
model: str
display_name: str
capabilities: list[ModelCapability] = Field(default_factory=list)
class ProviderModelsResponse(Contract):
provider_id: str
items: list[ModelInfo] = Field(default_factory=list)
class ProviderTestRequest(Contract):
provider_id: str
model: str | None = None
credential_context_id: str | None = None
class ProviderTestResponse(Contract):
provider_id: str
success: bool
latency_ms: int | None = None
message: str
# Tasks, media and index
class TaskStatus(str, Enum):
todo = "todo"
in_progress = "in_progress"
done = "done"
cancelled = "cancelled"
class Task(Contract):
task_id: str
title: str
description: str = ""
status: TaskStatus = TaskStatus.todo
note_id: str | None = None
due_at: datetime | None = None
created_at: datetime
updated_at: datetime
class TaskCreateRequest(Contract):
title: str = Field(min_length=1)
description: str = ""
note_id: str | None = None
due_at: datetime | None = None
class TaskUpdateRequest(Contract):
title: str | None = None
description: str | None = None
status: TaskStatus | None = None
note_id: str | None = None
due_at: datetime | None = None
class TaskListResponse(Contract):
items: list[Task] = Field(default_factory=list)
page: PageMeta = Field(default_factory=PageMeta)
class TranscriptionRequest(Contract):
attachment_id: str
language: str | None = None
diarization: bool = False
class TranscriptionJob(Contract):
job_id: str
attachment_id: str
status: Literal["queued", "processing", "completed", "failed"]
text: str | None = None
error_code: str | None = None
error_message: str | None = None
created_at: datetime
class IndexStatus(Contract):
status: Literal["idle", "queued", "running", "failed"] = "idle"
pending_jobs: int = 0
active_job_id: str | None = None
last_completed_at: datetime | None = None
error_message: str | None = None
class IndexRebuildRequest(Contract):
scope: Literal["all", "notes", "vectors"] = "all"
note_ids: list[str] = Field(default_factory=list)
force: bool = False
class IndexJob(Contract):
job_id: str
status: Literal["queued", "running", "completed", "failed"]
scope: Literal["all", "notes", "vectors"]
created_at: datetime