You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1428 lines
28 KiB
Python

# 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:<model>" | "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_<DOMAIN> 값(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, ...}