172 lines
5.0 KiB
Python
172 lines
5.0 KiB
Python
"""Schemas used by the board-onboarding assistant flow."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Literal, Self
|
|
from uuid import UUID
|
|
|
|
from pydantic import Field, field_validator, model_validator
|
|
from sqlmodel import SQLModel
|
|
|
|
from app.schemas.common import NonEmptyStr
|
|
|
|
_RUNTIME_TYPE_REFERENCES = (datetime, UUID, NonEmptyStr)
|
|
|
|
|
|
class BoardOnboardingStart(SQLModel):
|
|
"""Start signal for initializing onboarding conversation."""
|
|
|
|
|
|
class BoardOnboardingAnswer(SQLModel):
|
|
"""User answer payload for a single onboarding question."""
|
|
|
|
answer: NonEmptyStr
|
|
other_text: str | None = None
|
|
|
|
|
|
class BoardOnboardingConfirm(SQLModel):
|
|
"""Payload used to confirm generated onboarding draft fields."""
|
|
|
|
board_type: str
|
|
objective: str | None = None
|
|
success_metrics: dict[str, object] | None = None
|
|
target_date: datetime | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def validate_goal_fields(self) -> Self:
|
|
"""Require goal metadata when the board type is `goal`."""
|
|
if self.board_type == "goal" and (
|
|
not self.objective or not self.success_metrics
|
|
):
|
|
message = (
|
|
"Confirmed goal boards require objective and success_metrics"
|
|
)
|
|
raise ValueError(message)
|
|
return self
|
|
|
|
|
|
class BoardOnboardingQuestionOption(SQLModel):
|
|
"""Selectable option for an onboarding question."""
|
|
|
|
id: NonEmptyStr
|
|
label: NonEmptyStr
|
|
|
|
|
|
class BoardOnboardingAgentQuestion(SQLModel):
|
|
"""Question payload emitted by the onboarding assistant."""
|
|
|
|
question: NonEmptyStr
|
|
options: list[BoardOnboardingQuestionOption] = Field(min_length=1)
|
|
|
|
|
|
def _normalize_optional_text(value: object) -> object | None:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, str):
|
|
text = value.strip()
|
|
return text or None
|
|
return value
|
|
|
|
|
|
class BoardOnboardingUserProfile(SQLModel):
|
|
"""User-profile preferences gathered during onboarding."""
|
|
|
|
preferred_name: str | None = None
|
|
pronouns: str | None = None
|
|
timezone: str | None = None
|
|
notes: str | None = None
|
|
context: str | None = None
|
|
|
|
@field_validator(
|
|
"preferred_name",
|
|
"pronouns",
|
|
"timezone",
|
|
"notes",
|
|
"context",
|
|
mode="before",
|
|
)
|
|
@classmethod
|
|
def normalize_text(cls, value: object) -> object | None:
|
|
"""Trim optional free-form profile text fields."""
|
|
return _normalize_optional_text(value)
|
|
|
|
|
|
LeadAgentAutonomyLevel = Literal["ask_first", "balanced", "autonomous"]
|
|
LeadAgentVerbosity = Literal["concise", "balanced", "detailed"]
|
|
LeadAgentOutputFormat = Literal["bullets", "mixed", "narrative"]
|
|
LeadAgentUpdateCadence = Literal["asap", "hourly", "daily", "weekly"]
|
|
|
|
|
|
class BoardOnboardingLeadAgentDraft(SQLModel):
|
|
"""Editable lead-agent draft configuration."""
|
|
|
|
name: NonEmptyStr | None = None
|
|
# role, communication_style, emoji are expected keys.
|
|
identity_profile: dict[str, str] | None = None
|
|
autonomy_level: LeadAgentAutonomyLevel | None = None
|
|
verbosity: LeadAgentVerbosity | None = None
|
|
output_format: LeadAgentOutputFormat | None = None
|
|
update_cadence: LeadAgentUpdateCadence | None = None
|
|
custom_instructions: str | None = None
|
|
|
|
@field_validator(
|
|
"autonomy_level",
|
|
"verbosity",
|
|
"output_format",
|
|
"update_cadence",
|
|
"custom_instructions",
|
|
mode="before",
|
|
)
|
|
@classmethod
|
|
def normalize_text_fields(cls, value: object) -> object | None:
|
|
"""Trim optional lead-agent preference fields."""
|
|
return _normalize_optional_text(value)
|
|
|
|
@field_validator("identity_profile", mode="before")
|
|
@classmethod
|
|
def normalize_identity_profile(
|
|
cls,
|
|
value: object,
|
|
) -> object | None:
|
|
"""Normalize identity profile keys and values as trimmed strings."""
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, dict):
|
|
return value
|
|
normalized: dict[str, str] = {}
|
|
for raw_key, raw_val in value.items():
|
|
if raw_val is None:
|
|
continue
|
|
key = str(raw_key).strip()
|
|
if not key:
|
|
continue
|
|
val = str(raw_val).strip()
|
|
if val:
|
|
normalized[key] = val
|
|
return normalized or None
|
|
|
|
|
|
class BoardOnboardingAgentComplete(BoardOnboardingConfirm):
|
|
"""Complete onboarding draft produced by the onboarding assistant."""
|
|
|
|
status: Literal["complete"]
|
|
user_profile: BoardOnboardingUserProfile | None = None
|
|
lead_agent: BoardOnboardingLeadAgentDraft | None = None
|
|
|
|
|
|
BoardOnboardingAgentUpdate = BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion
|
|
|
|
|
|
class BoardOnboardingRead(SQLModel):
|
|
"""Stored onboarding session state returned by API endpoints."""
|
|
|
|
id: UUID
|
|
board_id: UUID
|
|
session_key: str
|
|
status: str
|
|
messages: list[dict[str, object]] | None = None
|
|
draft_goal: BoardOnboardingAgentComplete | None = None
|
|
created_at: datetime
|
|
updated_at: datetime
|