# backend/app/schemas.py from __future__ import annotations from datetime import date, datetime from typing import Any, Optional from pydantic import BaseModel, ConfigDict, Field from .models import ClsType, InboxKind, InboxStatus, Prio, Sphere, TaskStatus # ---------- people ---------- class PersonOut(BaseModel): id: str name: str initial: str color: str is_me: bool # ---------- tree ---------- class ProjectNode(BaseModel): id: str folder_id: str parent_id: Optional[str] = None name: str tone: str sort_order: int pinned: bool task_count: int = 0 # 자신+하위 프로젝트의 작업 수 합 children: list[ProjectNode] = [] class FolderOut(BaseModel): id: str name: str tone: str icon: str sort_order: int is_system: bool projects: list[ProjectNode] = [] # 최상위 프로젝트(중첩 children 포함) class FolderCreate(BaseModel): name: str tone: Optional[str] = "ink" icon: Optional[str] = "folder" class FolderPatch(BaseModel): name: Optional[str] = None tone: Optional[str] = None icon: Optional[str] = None sort_order: Optional[int] = None class ProjectCreate(BaseModel): folder_id: str parent_id: Optional[str] = None name: str tone: Optional[str] = "ink" class ProjectPatch(BaseModel): name: Optional[str] = None tone: Optional[str] = None parent_id: Optional[str] = None sort_order: Optional[int] = None # ---------- tasks ---------- class CommentOut(BaseModel): id: str task_id: str person_id: str text: str created_at: datetime class TaskNode(BaseModel): id: str project_id: str parent_id: Optional[str] = None title: str status: TaskStatus assignee_id: Optional[str] = None due: Optional[date] = None prio: Prio notes: str est: str delegated: bool sort_order: int created_at: datetime updated_at: datetime comments: list[CommentOut] = [] children: list[TaskNode] = [] class TaskCreate(BaseModel): title: str project_id: str parent_id: Optional[str] = None status: Optional[TaskStatus] = TaskStatus.todo assignee_id: Optional[str] = None due: Optional[date] = None prio: Optional[Prio] = Prio.normal notes: Optional[str] = "" est: Optional[str] = "" class TaskPatch(BaseModel): title: Optional[str] = None project_id: Optional[str] = None parent_id: Optional[str] = None status: Optional[TaskStatus] = None assignee_id: Optional[str] = None due: Optional[date] = None prio: Optional[Prio] = None notes: Optional[str] = None est: Optional[str] = None delegated: Optional[bool] = None sort_order: Optional[int] = None class CommentCreate(BaseModel): person_id: str text: str # ---------- scaffold ---------- class ScaffoldItem(BaseModel): title: str est: str = "" class ScaffoldOut(BaseModel): kind: str # "연구 · 논문 작성" 등 icon: str # "brain"|"branch"|"file"|"list" items: list[ScaffoldItem] created: bool = False # True면 실제 생성됨, False면 미리보기 created_task_ids: list[str] = [] class ScaffoldRequest(BaseModel): create: bool = False # True면 하위작업 실제 생성, False면 미리보기 use_llm: bool = False # LLM 보강 사용 여부 # ---------- risks ---------- class RiskOut(BaseModel): kind: str # "지연 위험" | "업무 쏠림" | "의존성" icon: str # "clock" | "scale" | "link" tone: str # "coral" | "amber" | "violet" task_id: Optional[str] = None # 관련 task id (cta 대상) text: str # 한국어. 강조어를 ** ** 마크다운으로 감싼다 cta: Optional[str] = None # "작업 열기" | "후속 작업 보기" # ---------- inbox ---------- class ClassificationOut(BaseModel): id: Optional[str] = None inbox_item_id: str type: ClsType sphere: Sphere project_id: Optional[str] = None proj_label: str tone: str due_text: str when_text: str extra: str reason: str confidence: float model: str created_at: Optional[datetime] = None class InboxItemOut(BaseModel): id: str kind: InboxKind raw: str status: InboxStatus created_at: datetime materialized_task_id: Optional[str] = None classification: Optional[ClassificationOut] = None # 최신 1건 class CaptureRequest(BaseModel): kind: InboxKind = InboxKind.text raw: str class CaptureResponse(BaseModel): item: InboxItemOut classification: ClassificationOut class ReclassifyRequest(BaseModel): type: Optional[ClsType] = None # 사용자가 타입 강제 가능 class ConfirmResponse(BaseModel): item: InboxItemOut task: Optional[TaskNode] = None # type=task 면 생성된 작업 # ---------- llm ---------- class LLMHealth(BaseModel): reachable: bool provider: str # "ollama" | "heuristic" model: str host: Optional[str] = None detail: str = "" # ---------- dashboard ---------- class UserOut(BaseModel): name: str # 예: "지우" initial: str # 예: "지" class WeatherOut(BaseModel): temp: int # 현재 기온 (예: 24) cond: str # "맑음 · 한낮 28°" icon: str # 'cloudSun' 저장 → 표시용 'sun' 으로 매핑 class BriefingOut(BaseModel): today: str # 히어로 날짜 라벨 (예: "6월 7일 일요일") weather: WeatherOut commute: str sleep: str note: str # HTML 허용 class EventOut(BaseModel): id: str time: str title: str tag: str dur: str tone: str soon: bool class ApprovalSummaryOut(BaseModel): id: str icon: str tone: str title: str time: str # risk=="high" 만 포함, 최대 3건. detail/cta/alt/undo_label 미포함. class GoalOut(BaseModel): id: str title: str pct: int sub: str tone: str # tone=키('blue' 등) class InboxRecentOut(BaseModel): id: str kind: str raw: str type: str proj_label: str tone: str # 평탄화 형태: 최신 classification 에서 type/proj_label/tone 을 끌어와 채운다. class TaskSummaryItem(BaseModel): id: str title: str prio: str project: str class TaskSummaryOut(BaseModel): open_count: int # 미완료 작업 수 items: list[TaskSummaryItem] class BadgesOut(BaseModel): appr: int # 결재함 대기(=high risk 건수) task: int # 미완료 작업 수(상단 작업 배지) noti: int # 알림 배지 (시드 상수 6) class DashboardOut(BaseModel): user: UserOut briefing: BriefingOut saved_today: str # "47분" today_routed: int # 7 schedule: list[EventOut] task_summary: TaskSummaryOut goals: list[GoalOut] approvals_summary: list[ApprovalSummaryOut] # high-risk 만, 최대 3건 inbox_recent: list[InboxRecentOut] # 최근 3 (평탄화 형태) badges: BadgesOut # 재귀 모델 전방참조 해소 ProjectNode.model_rebuild() TaskNode.model_rebuild() # ==================== phase-7: 결재함 + 자동화 ==================== # ---------- 결재함 ---------- class ApprovalOut(BaseModel): id: str icon: str tone: str risk: str # "low" | "high" time: str title: str detail: str cta: str alt: str undo_label: str status: str # pending|approved|executed|undone source: str rule_id: Optional[str] = None sort_order: int class ApprovalLogOut(BaseModel): id: str time: str text: str approval_id: Optional[str] = None class AutonomyOut(BaseModel): level: str # approval_first|mixed|full_auto class AutonomyPatch(BaseModel): level: str # 셋 중 하나만 허용(검증) class ApprovalQueueOut(BaseModel): """결재함 페이지 응답 정본. 자율성 레벨로 큐를 분기한 결과.""" autonomy: str saved_today: str # "47분" auto_count_night: int # 7 pending: list[ApprovalOut] done: list[ApprovalOut] log: list[ApprovalLogOut] badges_appr: int # 상단 결재함 배지 = pending 수 # ---------- 자동화 ---------- class FlowOut(BaseModel): trigger: str cond: Optional[str] = None action: str class RuleOut(BaseModel): id: str name: str cat: str # mail|cal|focus|life trigger: str cond: Optional[str] = None action: str on: bool last: str runs: int fresh: bool source: str class RuleCreate(BaseModel): name: str cat: str trigger: str cond: Optional[str] = None action: str source: Optional[str] = "user" class RulePatch(BaseModel): name: Optional[str] = None cat: Optional[str] = None trigger: Optional[str] = None cond: Optional[str] = None action: Optional[str] = None on: Optional[bool] = None class ParseRequest(BaseModel): text: str class ParsePreviewOut(BaseModel): matched: bool name: str cat: str # mail|cal|focus|life parse: FlowOut model: str # "llm:" | "heuristic" | "example" confidence: float class SuggestionOut(BaseModel): id: str pattern: str offer: FlowOut offer_name: str offer_cat: str status: str class RunLogOut(BaseModel): id: str time: str rule: str text: str undone: bool class AutomationStatsOut(BaseModel): active: int runs_week: int saved: str class AutomationPageOut(BaseModel): """자동화 페이지 응답 정본(한 번에 모든 뷰 데이터).""" stats: AutomationStatsOut rules: list[RuleOut] suggests: list[SuggestionOut] log: list[RunLogOut] examples: list[ParsePreviewOut] # ==================== phase-8: 일정 + 회의 도우미 ==================== class CalendarOut(BaseModel): id: str name: str tone: str on: bool count: int = 0 class EventActionOut(BaseModel): text: str when: str class CalEventOut(BaseModel): id: str day: int start: str end: str title: str cal: str loc: str = "" note: str = "" soon: bool = False people: list[str] = [] actions: list[EventActionOut] = [] has_meeting: bool = False meet_label: str = "" class FocusBlockOut(BaseModel): id: str day: int start: str end: str title: str type: str tag: str task_id: Optional[str] = None auto: bool = True sort_order: int = 0 class MeetingActionOut(BaseModel): id: str idx: int text: str who: str when: str = "" added: bool materialized_task_id: Optional[str] = None class MeetingOut(BaseModel): id: str event_id: str phase: str one_on_one: bool = False meta: str = "" starts_in: str = "" summary: list[str] = [] decisions: list[str] = [] agenda: list[str] = [] last_meeting: dict[str, Any] = {} last_actions: list[dict[str, Any]] = [] insights: list[str] = [] docs: list[str] = [] person: dict[str, Any] = {} promises: list[dict[str, Any]] = [] signals: list[dict[str, Any]] = [] talking_points: list[str] = [] actions: list[MeetingActionOut] = [] class DayBundleOut(BaseModel): day: int events: list[CalEventOut] focus_blocks: list[FocusBlockOut] class WeekBundleOut(BaseModel): today: int week: list[int] weekdays: list[str] calendars: list[CalendarOut] events: list[CalEventOut] focus_blocks: list[FocusBlockOut] class MaterializeResponse(BaseModel): action: MeetingActionOut task: Optional[TaskNode] = None all_added: bool = False class FocusSuggestRequest(BaseModel): day: int min_minutes: int = 20 create: bool = False class FocusSuggestResponse(BaseModel): day: int suggestions: list[FocusBlockOut] created: bool = False MaterializeResponse.model_rebuild() # ==================== phase-9: 메일 + 알림 ==================== class MailAccountOut(BaseModel): id: str name: str email: str tone: str kind: str unread: int class MailFolderOut(BaseModel): id: str name: str icon: str class AiTaskOut(BaseModel): text: str project: str due: str = "" prio: str = "" class AiEventOut(BaseModel): title: str date: str = "" day: str = "" time: str = "" dur: str = "" place: str = "" class AiReplyOut(BaseModel): tone: str preview: str body: str class AiFileOut(BaseModel): project: str reason: str class AiAnalysisOut(BaseModel): summary: str = "" priority: str = "보통" category: str = "" tasks: list[AiTaskOut] = [] events: list[AiEventOut] = [] replies: list[AiReplyOut] = [] file: Optional[AiFileOut] = None class EmailRowOut(BaseModel): model_config = ConfigDict(populate_by_name=True) id: str account: str from_: str = Field(alias="from") to: str subject: str time: str read: bool starred: bool has_attach: bool labels: list[str] = [] preview: str ai: Optional[AiAnalysisOut] = None class EmailDetailOut(EmailRowOut): date: str body: list[str] = [] attachments: list[dict] = [] class ExtractRequest(BaseModel): kind: str # task | event | file index: int = 0 class ExtractResponse(BaseModel): kind: str task: Optional[TaskNode] = None event_id: Optional[str] = None project: Optional[str] = None message: str class ExtractAllResponse(BaseModel): created_task_ids: list[str] = [] created_event_ids: list[str] = [] filed_project: Optional[str] = None message: str class ReplyDraftRequest(BaseModel): reply_index: Optional[int] = None tone: Optional[str] = None class ReplyDraftOut(BaseModel): to: str from_account: str subject: str body: str class SendRequest(BaseModel): from_account: str to: str subject: str body: str in_reply_to: Optional[str] = None class SendResponse(BaseModel): approval_id: str status: str message: str # ---------- 알림 ---------- class NotificationOut(BaseModel): id: str bucket: str src: str app: str title: str sum: str time: str why: str tone: str act: str = "" class TriageOut(BaseModel): now: list[NotificationOut] later: list[NotificationOut] held: list[NotificationOut] counts: dict stats: dict class ReclassifyNotifRequest(BaseModel): to_bucket: str class GuardOut(BaseModel): on: bool until: str held: int quiet: str class GuardPatch(BaseModel): on: bool class DigestOut(BaseModel): id: str time: str name: str desc: str count: int done: bool class SenderRuleOut(BaseModel): model_config = ConfigDict(populate_by_name=True) id: str who: str from_label: str = Field(serialization_alias="from") rule: str tone: str bucket: str class SenderRuleCreate(BaseModel): who: str from_label: str rule: str bucket: str = "now" tone: str = "ink" ExtractResponse.model_rebuild() # ==================== phase-10: 리서치 + 여행 ==================== class CollectionOut(BaseModel): id: str name: str tone: str n: int class SourceOut(BaseModel): model_config = ConfigDict(populate_by_name=True) id: str kind: str title: str from_: str = Field(default="", serialization_alias="from") col: Optional[str] = None learned: bool = False class ResearchHomeOut(BaseModel): collections: list[CollectionOut] = [] sources: list[SourceOut] = [] prompts: list[str] = [] entries: list[dict] = [] class ReportOut(BaseModel): id: str title: str asked: str = "" meta: str = "" counts: list[dict] = [] synthesis: str = "" cross: list[dict] = [] note: str = "" class QAOut(BaseModel): id: str = "" q: str = "" a: str = "" refs: list[dict] = [] class ChartOut(BaseModel): id: str title: str asked: str = "" unit: str = "" bars: list[dict] = [] insight: str = "" caution: str = "" class StartResearchRequest(BaseModel): query: str class StartResearchOut(BaseModel): status: str # ready | queued report_id: Optional[str] = None report: Optional[ReportOut] = None queued_text: Optional[str] = None class AskRequest(BaseModel): q: str collection_id: Optional[str] = None class AskOut(BaseModel): a: str refs: list[dict] = [] grounded: bool = True model: str = "" # ---------- 여행 ---------- class PlanRequest(BaseModel): text: str class PlanResultOut(BaseModel): id: str idx: int custom: bool = False title: str meta: str = "" summary: str = "" weather: str = "" transport: list[dict] = [] stay: list[dict] = [] days: list[dict] = [] budget: dict = {} checklist: list[str] = [] sources: list[str] = [] research: Optional[list[dict]] = None class WatchToggleRequest(BaseModel): watch: Optional[bool] = None class SavedTripOut(BaseModel): id: str title: str tag: str = "" note: str = "" now: str = "" delta: str = "" down: bool = True watch: bool = True hint: str = "" spark: list[int] = [] class UpcomingTripOut(BaseModel): trip: dict route: dict stay: dict prep: list[dict] = [] days: list[dict] = [] check: list[dict] = [] expense: dict # ==================== phase-11: 라이프 케어 ==================== from .models import ConnectorDomain, InsightKind, KnowledgeType # noqa: E402 class ConnectorSourceOut(BaseModel): id: str domain: ConnectorDomain name: str kind: str tone: str on: bool last: str class RingOut(BaseModel): label: str val: str unit: str goal: str pct: int icon: str tone: str class VitalOut(BaseModel): label: str val: str unit: str trend: str note: str tone: str class SleepStageOut(BaseModel): label: str pct: int tone: str class SleepOut(BaseModel): total: str score: int stages: list[SleepStageOut] week: list[int] note: str class HabitOut(BaseModel): id: str title: str goal: str done: int total: int streak: int icon: str tone: str at: str automation_rule_id: Optional[str] = None class HealthOut(BaseModel): rings: list[RingOut] vitals: list[VitalOut] sleep: SleepOut coach: str habits: list[HabitOut] class BudgetOut(BaseModel): spent: int limit: int pct: int delta_pct: int days_left: int class CategoryOut(BaseModel): name: str amt: int pct: int icon: str tone: str over: bool class SubOut(BaseModel): id: str name: str date: str in_days: int amt: int icon: str tone: str note: str paused: bool class InsightOut(BaseModel): id: str kind: InsightKind icon: str title: str body: str action: str action_ref: str class FinanceOut(BaseModel): budget: BudgetOut cats: list[CategoryOut] subs: list[SubOut] coach: str insights: list[InsightOut] class KnowledgeCollectionOut(BaseModel): name: str count: int tone: str class KnowledgeItemOut(BaseModel): id: str type: KnowledgeType title: str src: str time: str tone: str icon: str tags: list[str] excerpt: str ai: str class KnowledgeStatsOut(BaseModel): items: int this_week: int collections: int class KnowledgeOut(BaseModel): stats: KnowledgeStatsOut suggested: list[str] collections: list[KnowledgeCollectionOut] items: list[KnowledgeItemOut] class LifeOverviewOut(BaseModel): sources: dict[str, list[ConnectorSourceOut]] health: HealthOut finance: FinanceOut knowledge: KnowledgeOut class HabitTickResponse(BaseModel): habit: HabitOut class SubPauseRequest(BaseModel): paused: bool class SubPauseResponse(BaseModel): sub: SubOut approval_id: Optional[str] = None class InsightActResponse(BaseModel): approval_id: Optional[str] = None task_id: Optional[str] = None message: str class KnowledgeAskRequest(BaseModel): q: str class KnowledgeAskResponse(BaseModel): q: str body: str sources: list[str] class KnowledgeSuggestActRequest(BaseModel): item_id: str target: str class KnowledgeSuggestActResponse(BaseModel): task_id: Optional[str] = None event_id: Optional[str] = None message: str # ==================== phase-12: 여정 + 하루 마감 ==================== class JourneyPersonOut(BaseModel): id: str name: str initial: str color: str me: bool = False tasks: int = 0 role: str = "" class JourneyStageOut(BaseModel): id: str title: str time: str tone: str n: str class RoutineCardOut(BaseModel): node: str icon: str title: str meta: str tag: str tool: str toolTone: str = "" class TaskCardOut(BaseModel): node: str title: str meta: str who: str done: bool = False class MeetCardOut(BaseModel): node: str time: str title: str meta: str who: str soon: bool = False class TileCardOut(BaseModel): node: str hi: bool = False icon: str tt: str ts: str class JourneyCardsOut(BaseModel): s1: list[RoutineCardOut] s2: list[TaskCardOut] s3: list[MeetCardOut] s4: list[TileCardOut] class JourneyLinkOut(BaseModel): from_node: str to_node: str tone: str dash: bool = False class JourneyRowOut(BaseModel): node: str title: str star: bool = False statusKey: str statusLabel: str startTime: str who: str class GaugeOut(BaseModel): pct: int tone: str val: str unit: str label: str sub: str class JourneyMetaOut(BaseModel): stageCount: int itemCount: int collabCount: int peopleMore: str = "+3" class JourneyHeaderOut(BaseModel): dateLabel: str weather: str class JourneyTableOut(BaseModel): subtitle: str class JourneyOut(BaseModel): header: JourneyHeaderOut meta: JourneyMetaOut people: list[JourneyPersonOut] stages: list[JourneyStageOut] cards: JourneyCardsOut links: list[JourneyLinkOut] rows: list[JourneyRowOut] table: JourneyTableOut gauges: list[GaugeOut] gaugeFoot: str # ==================== phase-13: 외부 연동 & 데이터 커넥터 ==================== class ConnectorStatusOut(BaseModel): id: str domain: str provider: str name: str # "회사" / "우리카드" / "Apple Health" kind: str # "Lumi Workspace" / "신용/체크" / "건강 앱" tone: str # blue|violet|coral|green|amber|ink|faint mode: str # mock | csv | healthkit | real env_mode: str # CONNECTOR_ 값(real 허용 여부) state: str # connected|disconnected|syncing|error|token_expired on: bool # state==connected (라이프 카드 on 호환) last: str # last_label last_synced_at: Optional[datetime] = None error_detail: str = "" @classmethod def from_account(cls, a, env_mode: str) -> ConnectorStatusOut: dom = a.domain.value if hasattr(a.domain, "value") else a.domain mode = a.mode.value if hasattr(a.mode, "value") else a.mode state = a.state.value if hasattr(a.state, "value") else a.state return cls( id=a.id, domain=dom, provider=a.provider, name=a.name, kind=a.kind, tone=a.tone, mode=mode, env_mode=env_mode, state=state, on=(state == "connected"), last=a.last_label, last_synced_at=a.last_synced_at, error_detail=a.error_detail, ) class OAuthStartOut(BaseModel): authorize_url: str class SyncResultOut(BaseModel): domain: str account_id: str seen: int = 0 upserted: int = 0 skipped: int = 0 errors: int = 0 events_published: list[str] = Field(default_factory=list) detail: str = "" class ImportResultOut(BaseModel): entity_type: str imported: int = 0 skipped: int = 0 errors: int = 0 detail: str = "" # ==================== phase-14: 능동 에이전트 + 멀티모달 ==================== class ErrandStartIn(BaseModel): kind: str # booking|refund|cancel|support title: str goal: str = "" target: Optional[str] = None tone: Optional[str] = None class ErrandStepOut(BaseModel): id: str seq: int phase: str tool: Optional[str] = None label: str detail: Optional[str] = None external_effect: bool state: str class ErrandTaskOut(BaseModel): id: str kind: str title: str goal: str target: Optional[str] = None tone: str status: str approval_id: Optional[str] = None result_summary: Optional[str] = None model: str steps: list[ErrandStepOut] = Field(default_factory=list) class ProactiveCardOut(BaseModel): id: str kind: str icon: str tone: str title: str why: str cta: Optional[str] = None action_kind: Optional[str] = None detected_at: str status: str class ProactiveAcceptOut(BaseModel): id: str action_kind: str created_id: Optional[str] = None message: str class WeeklyReviewOut(BaseModel): id: str week_label: str deep_work_h: float meeting_h: float done_count: int auto_count: int saved_minutes: int patterns: list[dict] = Field(default_factory=list) note: Optional[str] = None class WorkerRunOut(BaseModel): job: str ok: bool detail: str class TranscribeOut(BaseModel): text: str seconds: float model: str confidence: float class CaptionOut(BaseModel): text: str ocr: str model: str confidence: float # ==================== phase-15: 인증/멀티유저 + 운영 ==================== class LoginIn(BaseModel): email: str password: str class MeOut(BaseModel): id: str name: str initial: str email: str role: str # member | admin class TokenOut(BaseModel): id: str name: str token: Optional[str] = None # 발급 시 1회만 평문 created_at: str class ReadyOut(BaseModel): ready: bool checks: dict[str, str] # ==================== 설정(settings) ==================== class ProfileUpdateIn(BaseModel): """프로필 편집. 모두 선택 — 주어진 필드만 갱신.""" name: Optional[str] = None role: Optional[str] = None class TokenInfoOut(BaseModel): """토큰 목록 항목(평문 미포함).""" id: str name: str created_at: str last_used_at: Optional[str] = None revoked: bool = False class LLMConfigOut(BaseModel): """유효 LLM 연결 설정 + 선택지 + 보조 프로바이더(읽기전용).""" provider: str # 유효(오버레이>env): auto | ollama | heuristic model: str host: str timeout: float provider_options: list[str] = ["auto", "ollama", "heuristic"] overridden: bool = False # 런타임 오버레이가 활성인가(env 기본과 다른가) # 보조 프로바이더(읽기전용, env 관리) embed_provider: str = "" embed_model: str = "" agent_provider: str = "" stt_provider: str = "" vision_provider: str = "" class LLMUpdateIn(BaseModel): """LLM 연결 저장. None/빈 문자열은 '기본으로 되돌리기'.""" provider: Optional[str] = None model: Optional[str] = None host: Optional[str] = None timeout: Optional[float] = None class LLMTestIn(BaseModel): """연결 테스트(저장 없이). 미지정 필드는 유효 설정값 사용.""" provider: Optional[str] = None host: Optional[str] = None model: Optional[str] = None class LLMTestOut(BaseModel): reachable: bool provider: str model: str host: Optional[str] = None models: list[str] = [] # ollama /api/tags 결과 detail: str = "" class SystemConfigOut(BaseModel): """런타임/환경 상태 스냅샷(읽기전용). env(.env)로 관리됨을 명시.""" env: str # dev | prod auth_enabled: bool worker_enabled: bool database: str # sqlite | postgres | ... sync_interval_minutes: int sync_page_size: int web_search_provider: str connector_modes: dict[str, str] # {calendar: mock, mail: mock, ...}