"""Internal domain models for the knowledge module. These are plain dataclasses passed between the extractor, the vault writer and the repository. The Gateway router defines its own Pydantic request/response models on top of the dicts the service returns. """ from __future__ import annotations from dataclasses import dataclass, field # Allowed note source types (see product-knowledge-base-dev.md §7.1). SOURCE_TYPES = ("thread", "search", "tool", "file", "manual", "report") # Allowed lifecycle states. STATUSES = ("draft", "approved", "archived") # obsidian-wiki wiki-capture categories. CATEGORIES = ("synthesis", "concepts", "references", "decision", "journal") @dataclass class SourceDraft: """A normalized knowledge source (search result, tool call, thread, …).""" source_type: str thread_id: str | None = None message_id: str | None = None tool_name: str | None = None title: str | None = None url: str | None = None snippet: str | None = None raw: dict | None = None @dataclass class NoteDraft: """A knowledge note ready to be persisted + written to the vault. Produced by the extractor (from a thread / search / tool result) or built directly for a manual note. ``content_md`` here is the *body* — the vault writer prepends obsidian-wiki frontmatter. """ title: str summary: str content_md: str category: str = "synthesis" source_type: str = "manual" source_id: str | None = None status: str = "approved" confidence: float = 0.0 # User-assigned directory (slash-joined path); None = unfiled. folder: str | None = None tags: list[str] = field(default_factory=list) key_points: list[str] = field(default_factory=list) actions: list[str] = field(default_factory=list) related: list[str] = field(default_factory=list) sources: list[SourceDraft] = field(default_factory=list) # Phase 4: extracted graph signal. entities=[{name,type}], # relations=[{from,to,type}] (names refer to entities or this note's title). entities: list[dict] = field(default_factory=list) relations: list[dict] = field(default_factory=list) # Stable identity hint used for manifest dedup (e.g. ``thread:thread_id``). source_key: str | None = None def normalize_status(status: str | None) -> str: """Coerce an arbitrary status string to a valid lifecycle state.""" if status in STATUSES: return status return "approved" def normalize_source_type(source_type: str | None) -> str: """Coerce an arbitrary source-type string to a valid value.""" if source_type in SOURCE_TYPES: return source_type return "manual"