feat: 实现起卦、设置与积分系统

This commit is contained in:
qzl
2026-04-03 16:56:47 +08:00
parent 31594558eb
commit f245eec5f6
170 changed files with 20728 additions and 328 deletions
-18
View File
@@ -7,16 +7,7 @@ from schemas.agent.forwarded_props import (
from schemas.agent.forwarded_props import RuntimeMode
from schemas.agent.runtime_models import (
AgentOutput,
ConstraintItem,
ExecutionMode,
KeyEntity,
NormalizedTaskInput,
ResultTyping,
ResultType,
RouterAgentOutput,
RunStatus,
TaskType,
TaskTyping,
ToolAgentOutput,
ToolStatus,
WorkerAgentOutputLite,
@@ -36,19 +27,10 @@ from schemas.agent.ui_hints import (
__all__ = [
"AgentType",
"AgentOutput",
"ConstraintItem",
"ExecutionMode",
"ForwardedPropsPayload",
"KeyEntity",
"NormalizedTaskInput",
"ResultTyping",
"ClientTimeContext",
"ResultType",
"RouterAgentOutput",
"RunStatus",
"RuntimeMode",
"TaskType",
"TaskTyping",
"SystemAgentLLMConfig",
"SystemVisibilityBit",
"ToolAgentOutput",
@@ -0,0 +1,29 @@
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field
class ContextSource(str, Enum):
LATEST_CHAT = "latest_chat"
class ContextWindowMode(str, Enum):
DAY = "day"
NUMBER = "number"
class MessageContextConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
source: ContextSource = ContextSource.LATEST_CHAT
window_mode: ContextWindowMode = ContextWindowMode.DAY
window_count: int = Field(default=2, ge=1, le=200)
class RuntimeConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_tools: list[str] = Field(default_factory=list, max_length=0)
context: MessageContextConfig = Field(default_factory=MessageContextConfig)
+24 -120
View File
@@ -1,70 +1,15 @@
from __future__ import annotations
from enum import Enum
from typing import Any
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
from schemas.agent.ui_hints import UiHintsPayload
class TaskType(str, Enum):
KNOWLEDGE = "knowledge"
RECOMMENDATION = "recommendation"
PLANNING = "planning"
SCHEDULING = "scheduling"
REMINDER_MANAGEMENT = "reminder_management"
TODO_MANAGEMENT = "todo_management"
COMMUNICATION_DRAFTING = "communication_drafting"
INFORMATION_ORGANIZATION = "information_organization"
STATUS_TRACKING = "status_tracking"
TRANSACTION_ASSIST = "transaction_assist"
ACTION_EXECUTION = "action_execution"
TROUBLESHOOTING = "troubleshooting"
UNKNOWN = "unknown"
class ResultType(str, Enum):
DIRECT_ANSWER = "direct_answer"
OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation"
ACTION_PLAN = "action_plan"
SCHEDULE_PROPOSAL = "schedule_proposal"
TODO_LIST = "todo_list"
DRAFT_MESSAGE = "draft_message"
SUMMARY = "summary"
PROGRESS_SUMMARY = "progress_summary"
DIAGNOSIS_REPORT = "diagnosis_report"
STRUCTURED_PAYLOAD = "structured_payload"
EXECUTION_REPORT = "execution_report"
CLARIFICATION_REQUEST = "clarification_request"
SAFETY_BLOCK = "safety_block"
ERROR_REPORT = "error_report"
UNKNOWN = "unknown"
class TaskTyping(BaseModel):
model_config = ConfigDict(extra="forbid")
primary: TaskType
secondary: list[TaskType] = Field(default_factory=list, max_length=3)
class ResultTyping(BaseModel):
model_config = ConfigDict(extra="forbid")
primary: ResultType
secondary: list[ResultType] = Field(default_factory=list, max_length=3)
class ExecutionMode(str, Enum):
ONESTEP = "onestep"
TOOL_ASSISTED = "tool_assisted"
MULTISTEP = "multistep"
class RunStatus(str, Enum):
SUCCESS = "success"
PARTIAL_SUCCESS = "partial_success"
FAILED = "failed"
@@ -74,59 +19,6 @@ class ToolStatus(str, Enum):
PARTIAL = "partial"
class KeyEntity(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
type: str
value: str | None = None
@field_validator("value", mode="before")
@classmethod
def normalize_value(cls, value: object) -> object:
if value is None:
return None
if isinstance(value, str):
return value
if isinstance(value, bool | int | float):
return str(value)
return value
class ConstraintItem(BaseModel):
model_config = ConfigDict(extra="forbid")
key: str
value: str
required: bool = True
@field_validator("value", mode="before")
@classmethod
def normalize_value(cls, value: object) -> object:
if isinstance(value, bool | int | float):
return str(value)
return value
class NormalizedTaskInput(BaseModel):
model_config = ConfigDict(extra="forbid")
user_text: str
multimodal_summary: list[str] = Field(default_factory=list)
context_summary: str = Field(default="", max_length=2000)
class RouterAgentOutput(BaseModel):
model_config = ConfigDict(extra="forbid")
normalized_task_input: NormalizedTaskInput
key_entities: list[KeyEntity] = Field(default_factory=list)
constraints: list[ConstraintItem] = Field(default_factory=list)
task_typing: TaskTyping
execution_mode: ExecutionMode
result_typing: ResultTyping
class ErrorInfo(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -151,12 +43,28 @@ class WorkerAgentOutputLite(BaseModel):
model_config = ConfigDict(extra="forbid")
status: RunStatus = RunStatus.SUCCESS
answer: str
key_points: list[str] = Field(default_factory=list)
result_type: ResultType = ResultType.UNKNOWN
suggested_actions: list[str] = Field(default_factory=list)
sign_level: Literal["上上签", "中上签", "中下签"]
summary: str = Field(min_length=1, max_length=300)
conclusion: list[str] = Field(min_length=1, max_length=6)
focus_points: list[str] = Field(default_factory=list, max_length=6)
advice: list[str] = Field(min_length=1, max_length=6)
keywords: list[str] = Field(min_length=3, max_length=8)
answer: str = Field(min_length=1, max_length=4000)
error: ErrorInfo | None = None
# Backward-compatible shadow fields for legacy consumers.
key_points: list[str] = Field(default_factory=list, max_length=6)
result_type: str = Field(default="structured_payload")
suggested_actions: list[str] = Field(default_factory=list, max_length=6)
@model_validator(mode="after")
def sync_compatibility_fields(self) -> WorkerAgentOutputLite:
if not self.key_points and self.focus_points:
self.key_points = list(self.focus_points)
if not self.suggested_actions and self.advice:
self.suggested_actions = list(self.advice)
return self
class WorkerAgentOutputRich(WorkerAgentOutputLite):
ui_hints: UiHintsPayload | None = None
@@ -169,9 +77,5 @@ class AgentOutput(WorkerAgentOutputRich):
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
def resolve_worker_output_model(
execution_mode: ExecutionMode,
) -> type[WorkerAgentOutputLite]:
if execution_mode == ExecutionMode.ONESTEP:
return WorkerAgentOutputLite
return WorkerAgentOutputRich
def resolve_worker_output_model() -> type[WorkerAgentOutputLite]:
return WorkerAgentOutputLite
+1
View File
@@ -0,0 +1 @@
"""Reusable domain schemas shared across backend modules."""
+124
View File
@@ -0,0 +1,124 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Protocol
from uuid import UUID
from core.agentscope.tools.tool_config import AgentTool
from pydantic import BaseModel, ConfigDict, Field, model_validator
from schemas.enums import AutomationJobStatus, ScheduleType
class AutomationJobLike(Protocol):
id: UUID
owner_id: UUID
bootstrap_key: str | None
title: str
config: dict[str, object]
next_run_at: datetime
timezone: str
last_run_at: datetime | None
status: AutomationJobStatus
created_by: UUID | None
created_at: datetime
updated_at: datetime
class ContextSource(str, Enum):
LATEST_CHAT = "latest_chat"
class ContextWindowMode(str, Enum):
DAY = "day"
NUMBER = "number"
class MessageContextConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
source: ContextSource = ContextSource.LATEST_CHAT
window_mode: ContextWindowMode = ContextWindowMode.DAY
window_count: int = Field(default=2, ge=1, le=200)
class ScheduleRunAt(BaseModel):
model_config = ConfigDict(extra="forbid")
hour: int = Field(default=8, ge=0, le=23)
minute: int = Field(default=0, ge=0, le=59)
class ScheduleConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
type: ScheduleType
run_at: ScheduleRunAt
weekdays: list[int] | None = None
@model_validator(mode="after")
def validate_weekdays(self) -> "ScheduleConfig":
if self.type == ScheduleType.WEEKLY:
if not self.weekdays:
raise ValueError("weekdays is required when schedule type is weekly")
invalid = [day for day in self.weekdays if day < 1 or day > 7]
if invalid:
raise ValueError("weekdays must be within 1-7")
self.weekdays = sorted(set(self.weekdays))
else:
self.weekdays = None
return self
class RuntimeConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_tools: list[AgentTool] = Field(default_factory=list, max_length=32)
context: MessageContextConfig = Field(default_factory=MessageContextConfig)
class AutomationJobConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_tools: list[AgentTool] | None = Field(default=None, max_length=32)
context: MessageContextConfig | None = None
input_template: str | None = Field(default=None, min_length=1, max_length=4000)
schedule: ScheduleConfig | None = None
class AutomationJob(BaseModel):
model_config = ConfigDict(extra="forbid")
id: UUID
owner_id: UUID
bootstrap_key: str | None = Field(default=None, min_length=1, max_length=64)
title: str = Field(..., min_length=1, max_length=255)
config: AutomationJobConfig
next_run_at: datetime
timezone: str = Field(default="UTC", min_length=1, max_length=50)
last_run_at: datetime | None = None
status: AutomationJobStatus
created_by: UUID | None = None
created_at: datetime
updated_at: datetime
@classmethod
def from_orm(cls, obj: object) -> "AutomationJob":
return cls(
id=getattr(obj, "id"),
owner_id=getattr(obj, "owner_id"),
bootstrap_key=getattr(obj, "bootstrap_key"),
title=getattr(obj, "title"),
config=AutomationJobConfig.model_validate(getattr(obj, "config", {}) or {}),
next_run_at=getattr(obj, "next_run_at"),
timezone=getattr(obj, "timezone"),
last_run_at=getattr(obj, "last_run_at"),
status=getattr(obj, "status"),
created_by=getattr(obj, "created_by"),
created_at=getattr(obj, "created_at"),
updated_at=getattr(obj, "updated_at"),
)
@property
def is_system(self) -> bool:
return self.bootstrap_key is not None
@@ -0,0 +1,76 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any, ClassVar
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from schemas.agent.runtime_models import AgentOutput
from ..agent import AgentType, ToolAgentOutput
class UserMessageAttachment(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
bucket: str
path: str
mime_type: str
class AgentChatMessageMetadata(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
run_id: str
agent_type: AgentType | None = None
user_message_attachments: list[UserMessageAttachment] | None = None
tool_agent_output: ToolAgentOutput | None = None
agent_output: AgentOutput | None = None
class AgentChatMessage(BaseModel):
"""Canonical schema aligned with `messages` table columns."""
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
id: UUID
seq: int
role: str
content: str
model_code: str | None = None
tool_name: str | None = None
input_tokens: int = Field(default=0, ge=0)
output_tokens: int = Field(default=0, ge=0)
cost: Decimal = Field(default=Decimal("0"))
latency_ms: int | None = Field(default=None, ge=0)
metadata: AgentChatMessageMetadata | dict[str, object] | None = None
timestamp: datetime
def extract_user_message_attachments(
metadata: AgentChatMessageMetadata | dict[str, object] | None,
) -> list[UserMessageAttachment]:
if metadata is None:
return []
if isinstance(metadata, AgentChatMessageMetadata):
raw_value: Any = metadata.user_message_attachments
else:
raw_value = metadata.get("user_message_attachments")
if raw_value is None:
return []
raw_items: list[Any]
if isinstance(raw_value, list):
raw_items = raw_value
else:
raw_items = [raw_value]
attachments: list[UserMessageAttachment] = []
for item in raw_items:
try:
attachments.append(UserMessageAttachment.model_validate(item))
except Exception:
continue
return attachments
@@ -0,0 +1,11 @@
from __future__ import annotations
from typing import ClassVar
from pydantic import BaseModel, ConfigDict
class SessionStateSnapshot(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
pass
+81
View File
@@ -0,0 +1,81 @@
from __future__ import annotations
import json
from typing import ClassVar, Literal, Union
from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import InboxMessageStatus, InboxMessageType
__all__ = [
"InboxMessageType",
"InboxMessageStatus",
"CalendarInviteContent",
"CalendarUpdateContent",
"CalendarDeleteContent",
"FriendshipContent",
"CalendarContent",
"InboxMessageContent",
"parse_calendar_content",
]
class CalendarInviteContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["invite"]
permission: int = Field(..., description="权限: 1=view, 4=edit, 8=invite")
action: Literal["pending"] = "pending"
class CalendarUpdateContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["update"]
title: str = Field(..., description="事件标题")
action: Literal["updated"] = "updated"
class CalendarDeleteContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["delete"]
title: str = Field(..., description="事件标题")
action: Literal["deleted"] = "deleted"
class FriendshipContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["request"]
message: str | None = Field(None, description="好友申请消息")
CalendarContent = Union[
CalendarInviteContent,
CalendarUpdateContent,
CalendarDeleteContent,
]
InboxMessageContent = Union[
CalendarInviteContent,
CalendarUpdateContent,
CalendarDeleteContent,
FriendshipContent,
]
def parse_calendar_content(content: str | None) -> CalendarContent | None:
if not content:
return None
try:
data = json.loads(content)
content_type = data.get("type")
if content_type == "invite":
return CalendarInviteContent(**data)
if content_type == "update":
return CalendarUpdateContent(**data)
if content_type == "delete":
return CalendarDeleteContent(**data)
raise ValueError(f"Unknown calendar content type: {content_type}")
except Exception:
return None
+11
View File
@@ -0,0 +1,11 @@
from __future__ import annotations
from typing import ClassVar
from pydantic import BaseModel, ConfigDict
class InviteCodeRewardConfig(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
pass
+43
View File
@@ -0,0 +1,43 @@
from __future__ import annotations
from datetime import datetime
from typing import ClassVar, Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from schemas.domain.memory_content import (
TeamMember,
UserMemoryContent,
UserPreferences,
WorkHabit,
WorkProfileContent,
WorkProject,
)
from schemas.enums import MemoryStatus, MemoryType
class MemoryModel(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(
extra="forbid", from_attributes=True
)
id: UUID
owner_id: UUID
memory_type: Literal["user", "work"]
content: UserMemoryContent | WorkProfileContent
status: MemoryStatus
created_at: datetime
updated_at: datetime
__all__ = [
"MemoryModel",
"MemoryStatus",
"MemoryType",
"TeamMember",
"UserMemoryContent",
"UserPreferences",
"WorkHabit",
"WorkProfileContent",
"WorkProject",
]
@@ -0,0 +1,192 @@
from __future__ import annotations
from datetime import date, datetime
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
Weekday = Literal["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
ProjectStatus = Literal["planned", "active", "paused", "completed"]
PreferenceLevel = Literal["like", "neutral", "avoid"]
MemorySource = Literal["user", "inferred", "calendar", "email", "agent"]
class MemoryMeta(BaseModel):
source: MemorySource | None = Field(default=None, description="记忆来源")
confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="置信度")
last_updated_at: datetime | None = Field(default=None, description="最后更新时间")
class TimeWindow(BaseModel):
weekdays: list[Weekday] = Field(default_factory=list, description="适用星期")
start: str = Field(description="开始时间,HH:MM")
end: str = Field(description="结束时间,HH:MM")
class PersonMemory(BaseModel):
name: str = Field(description="人物姓名")
relationship: str | None = Field(
default=None, description="与用户关系,如家人/同事/导师/朋友"
)
role: str | None = Field(default=None, description="角色,如老板/导师/合作方")
preferred_contact_channel: str | None = Field(
default=None, description="偏好联系方式"
)
notes: str | None = Field(default=None, description="补充说明")
meta: MemoryMeta = Field(default_factory=MemoryMeta)
class PlaceMemory(BaseModel):
name: str = Field(description="地点名称")
category: str | None = Field(
default=None, description="地点类别,如home/office/gym/cafe"
)
address: str | None = Field(default=None, description="地址")
timezone: str | None = Field(default=None, description="地点时区")
commute_minutes: int | None = Field(default=None, ge=0, description="典型通勤时长")
preference: PreferenceLevel | None = Field(default=None, description="地点偏好")
notes: str | None = Field(default=None, description="补充说明")
meta: MemoryMeta = Field(default_factory=MemoryMeta)
class UserPreferences(BaseModel):
communication_style: str | None = Field(
default=None, description="沟通风格,如简洁直接"
)
language_preference: list[str] = Field(default_factory=list, description="语言偏好")
location_preference: str | None = Field(
default=None, description="地点偏好,如喜欢远程"
)
work_lifestyle: str | None = Field(default=None, description="作息方式,如早睡早起")
notification_preference: list[str] = Field(
default_factory=list, description="通知方式偏好"
)
class SchedulingPreferences(BaseModel):
productive_windows: list[TimeWindow] = Field(
default_factory=list, description="高效率时段"
)
preferred_meeting_windows: list[TimeWindow] = Field(
default_factory=list, description="偏好的会议时段"
)
no_meeting_windows: list[TimeWindow] = Field(
default_factory=list, description="尽量不安排会议的时段"
)
deep_work_windows: list[TimeWindow] = Field(
default_factory=list, description="深度工作时段"
)
preferred_meeting_duration_minutes: list[int] = Field(
default_factory=lambda: [30, 60], description="偏好的会议时长"
)
meeting_buffer_minutes: int | None = Field(
default=None, ge=0, description="会议间缓冲时间"
)
max_meetings_per_day: int | None = Field(
default=None, ge=0, description="单日会议上限"
)
notes: str | None = Field(default=None, description="其他排程说明")
class RecurringRoutine(BaseModel):
name: str = Field(description="周期性安排名称")
description: str | None = Field(default=None, description="周期性安排描述")
cadence: str | None = Field(
default=None, description="频率,如daily/weekly/monthly"
)
time_windows: list[TimeWindow] = Field(
default_factory=list, description="通常发生时段"
)
importance: str | None = Field(default=None, description="重要程度")
meta: MemoryMeta = Field(default_factory=MemoryMeta)
class UserMemoryContent(BaseModel):
model_config = ConfigDict(extra="allow")
occupation: str | None = Field(default=None, description="职业")
timezone: str | None = Field(default=None, description="时区")
primary_language: str | None = Field(default=None, description="主要语言")
people: list[PersonMemory] = Field(default_factory=list, description="重要人物")
places: list[PlaceMemory] = Field(default_factory=list, description="常去地点")
preferences: UserPreferences = Field(default_factory=UserPreferences)
scheduling_preferences: SchedulingPreferences = Field(
default_factory=SchedulingPreferences
)
interests: list[str] = Field(default_factory=list, description="兴趣爱好")
avoid_topics: list[str] = Field(default_factory=list, description="不想讨论的话题")
custom_rules: list[str] = Field(default_factory=list, description="用户自定义规则")
recurring_routines: list[RecurringRoutine] = Field(
default_factory=list, description="周期性习惯/安排"
)
class Milestone(BaseModel):
name: str = Field(description="里程碑名称")
due_date: date | None = Field(default=None, description="截止日期")
status: str | None = Field(default=None, description="状态")
notes: str | None = Field(default=None, description="补充说明")
class WorkProject(BaseModel):
name: str = Field(description="项目名")
description: str | None = Field(default=None, description="项目描述")
status: ProjectStatus | None = Field(default=None, description="项目状态")
priority: str | None = Field(default=None, description="项目优先级")
deadline: date | None = Field(default=None, description="项目截止时间")
collaborators: list[str] = Field(default_factory=list, description="协作人")
key_milestones: list[Milestone] = Field(
default_factory=list, description="关键里程碑"
)
notes: str | None = Field(default=None, description="补充说明")
meta: MemoryMeta = Field(default_factory=MemoryMeta)
class WorkHabit(BaseModel):
available_hours: list[TimeWindow] = Field(
default_factory=list, description="常规可工作时间"
)
deep_work_blocks: list[TimeWindow] = Field(
default_factory=list, description="偏好的深度工作时间"
)
preferred_meeting_windows: list[TimeWindow] = Field(
default_factory=list, description="偏好的会议时间"
)
no_meeting_windows: list[TimeWindow] = Field(
default_factory=list, description="不希望开会的时间"
)
preferred_meeting_duration_minutes: list[int] = Field(
default_factory=lambda: [30, 60], description="偏好的会议时长"
)
notification_channel: str | None = Field(default=None, description="首选沟通渠道")
notes: str | None = Field(default=None, description="补充说明")
class TeamMember(BaseModel):
name: str = Field(description="成员姓名")
role: str | None = Field(default=None, description="团队角色")
relationship: str | None = Field(
default=None, description="关系,如直属上级/同事/合作方"
)
preferred_contact_channel: str | None = Field(
default=None, description="偏好沟通渠道"
)
notes: str | None = Field(default=None, description="补充说明")
meta: MemoryMeta = Field(default_factory=MemoryMeta)
class WorkProfileContent(BaseModel):
model_config = ConfigDict(extra="allow")
occupation: str | None = Field(default=None, description="职业身份")
expertise: list[str] = Field(default_factory=list, description="专业领域")
preferred_tools: list[str] = Field(default_factory=list, description="惯用工具")
current_projects: list[WorkProject] = Field(
default_factory=list, description="长期项目画像"
)
work_habits: WorkHabit = Field(default_factory=WorkHabit)
team_members: list[TeamMember] = Field(default_factory=list, description="团队成员")
team_context: str | None = Field(default=None, description="团队背景")
work_rules: list[str] = Field(
default_factory=list, description="工作规则或默认原则"
)
+78
View File
@@ -0,0 +1,78 @@
from __future__ import annotations
from decimal import Decimal
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, model_validator
from ..enums import (
PointsChangeType,
PointsOperatorType,
)
class PointsChargeSnapshot(BaseModel):
model_config = ConfigDict(extra="forbid")
message_id: UUID
message_seq: int = Field(ge=1)
model_code: str = Field(min_length=1, max_length=50)
input_tokens: int = Field(ge=0)
output_tokens: int = Field(ge=0)
cost: Decimal = Field(ge=Decimal("0"))
class PointsLedgerMetadataBase(BaseModel):
model_config = ConfigDict(extra="forbid")
schema_version: Literal[1] = 1
operator_type: PointsOperatorType
run_id: str = Field(min_length=1, max_length=128)
request_id: str | None = Field(default=None, max_length=128)
ext: dict[str, object] = Field(default_factory=dict)
class RegisterLedgerMetadata(PointsLedgerMetadataBase):
pass
class ConsumeLedgerMetadata(PointsLedgerMetadataBase):
charge: PointsChargeSnapshot
class GrantLedgerMetadata(PointsLedgerMetadataBase):
charge: PointsChargeSnapshot | None = None
class AdjustLedgerMetadata(PointsLedgerMetadataBase):
charge: PointsChargeSnapshot | None = None
@model_validator(mode="after")
def validate_ticket(self) -> "AdjustLedgerMetadata":
ticket_id = self.ext.get("ticket_id")
if not isinstance(ticket_id, str) or not ticket_id.strip():
raise ValueError("ext.ticket_id is required for adjust")
return self
PointsLedgerMetadata = (
RegisterLedgerMetadata
| ConsumeLedgerMetadata
| GrantLedgerMetadata
| AdjustLedgerMetadata
)
def parse_points_ledger_metadata(
*,
change_type: PointsChangeType,
payload: dict[str, object],
) -> PointsLedgerMetadata:
if change_type == PointsChangeType.REGISTER:
return RegisterLedgerMetadata.model_validate(payload)
if change_type == PointsChangeType.CONSUME:
return ConsumeLedgerMetadata.model_validate(payload)
if change_type == PointsChangeType.GRANT:
return GrantLedgerMetadata.model_validate(payload)
return AdjustLedgerMetadata.model_validate(payload)
+43
View File
@@ -0,0 +1,43 @@
from __future__ import annotations
from enum import Enum
from typing import ClassVar, Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import ScheduleItemSourceType, ScheduleItemStatus
__all__ = [
"AttachmentType",
"ScheduleItemMetadataAttachment",
"ScheduleItemMetadata",
"ScheduleItemSourceType",
"ScheduleItemStatus",
]
class AttachmentType(str, Enum):
DOCUMENT = "document"
REMINDER = "reminder"
class ScheduleItemMetadataAttachment(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
name: str
type: AttachmentType
visible_to: list[UUID] = Field(default_factory=list)
url: str | None = None
note: str | None = None
content: str | None = None
class ScheduleItemMetadata(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
color: str | None = Field(default=None, pattern=r"^#[0-9A-Fa-f]{6}$")
location: str | None = None
notes: str | None = None
attachments: list[ScheduleItemMetadataAttachment] = Field(default_factory=list)
reminder_minutes: int | None = Field(default=None, ge=0, le=10080)
version: Literal[1] = 1
+7
View File
@@ -0,0 +1,7 @@
from __future__ import annotations
from typing import Annotated
from pydantic import Field
TodoOrder = Annotated[int, Field(ge=0)]
+153
View File
@@ -0,0 +1,153 @@
from __future__ import annotations
from enum import Enum
class ScheduleItemStatus(str, Enum):
ACTIVE = "active"
ARCHIVED = "archived"
class ScheduleItemSourceType(str, Enum):
MANUAL = "manual"
IMPORTED = "imported"
AGENT_GENERATED = "agent_generated"
class AutomationJobStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class ScheduleType(str, Enum):
DAILY = "daily"
WEEKLY = "weekly"
class MemoryType(str, Enum):
USER = "user"
WORK = "work"
class MemoryStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class TodoStatus(str, Enum):
PENDING = "pending"
DONE = "done"
CANCELED = "canceled"
class TodoPriority(int, Enum):
IMPORTANT_URGENT = 1
IMPORTANT_NOT_URGENT = 2
NOT_IMPORTANT_URGENT = 3
NOT_IMPORTANT_NOT_URGENT = 4
class AgentChatMessageRole(str, Enum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
TOOL = "tool"
class AgentChatSessionStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class SessionType(str, Enum):
CHAT = "chat"
AUTOMATION = "automation"
class PointsChangeType(str, Enum):
REGISTER = "register"
CONSUME = "consume"
GRANT = "grant"
ADJUST = "adjust"
class PointsBizType(str, Enum):
CHAT = "chat"
class PointsOperatorType(str, Enum):
USER = "user"
SYSTEM = "system"
ADMIN = "admin"
class InboxMessageType(str, Enum):
FRIEND_REQUEST = "friend_request"
CALENDAR = "calendar"
SYSTEM = "system"
GROUP = "group"
class InboxMessageStatus(str, Enum):
PENDING = "pending"
ACCEPTED = "accepted"
REJECTED = "rejected"
DISMISSED = "dismissed"
class SubscriptionStatus(str, Enum):
ACTIVE = "active"
PENDING = "pending"
UNSUBSCRIBED = "unsubscribed"
class NotifyLevel(str, Enum):
ALL = "all"
MENTIONS = "mentions"
NONE = "none"
class SubscriptionPermission(int, Enum):
VIEW = 1
INVITE = 2
EDIT = 4
DELETE = 8
OWNER = 15 # VIEW | INVITE | EDIT | DELETE
class FriendshipStatus(str, Enum):
PENDING = "pending"
ACCEPTED = "accepted"
BLOCKED = "blocked"
DECLINED = "declined"
CANCELED = "canceled"
class InviteCodeStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
EXPIRED = "expired"
class GroupStatus(str, Enum):
ACTIVE = "active"
ARCHIVED = "archived"
class GroupMemberRole(str, Enum):
OWNER = "owner"
ADMIN = "admin"
MEMBER = "member"
class GroupMemberSource(str, Enum):
INVITED = "invited"
JOINED = "joined"
class GroupMemberStatus(str, Enum):
ACTIVE = "active"
MUTED = "muted"
REMOVED = "removed"
+1
View File
@@ -0,0 +1 @@
"""Shared schemas used across multiple domain modules."""
+88
View File
@@ -0,0 +1,88 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, model_validator
from ..domain.points import PointsLedgerMetadata
from ..enums import PointsBizType, PointsChangeType
class UserPointsSnapshot(BaseModel):
model_config = ConfigDict(from_attributes=True)
user_id: UUID
balance: int = Field(ge=0)
frozen_balance: int = Field(ge=0)
lifetime_earned: int = Field(ge=0)
lifetime_spent: int = Field(ge=0)
version: int = Field(ge=0)
created_at: datetime
updated_at: datetime
class PointsLedgerEntry(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
user_id: UUID
direction: Literal[1, -1]
amount: int = Field(gt=0)
balance_after: int = Field(ge=0)
change_type: PointsChangeType
biz_type: PointsBizType | None = None
biz_id: UUID | None = None
event_id: str = Field(min_length=1, max_length=64)
operator_id: UUID | None = None
metadata: PointsLedgerMetadata = Field(validation_alias="metadata_json")
created_at: datetime
class ApplyPointsChangeCommand(BaseModel):
model_config = ConfigDict(extra="forbid")
user_id: UUID
change_type: PointsChangeType
biz_type: PointsBizType | None = None
biz_id: UUID | None = None
event_id: str = Field(min_length=1, max_length=64)
amount: int = Field(gt=0)
direction: Literal[1, -1]
operator_id: UUID | None = None
metadata: PointsLedgerMetadata
occurred_at: datetime | None = None
@model_validator(mode="after")
def validate_change_type_contract(self) -> "ApplyPointsChangeCommand":
if self.change_type == PointsChangeType.REGISTER:
if (
self.direction != 1
or self.biz_type is not None
or self.biz_id is not None
):
raise ValueError("register must use direction=1 and no biz binding")
return self
if self.change_type == PointsChangeType.CONSUME:
if (
self.direction != -1
or self.biz_type != PointsBizType.CHAT
or self.biz_id is None
):
raise ValueError("consume must use direction=-1 and chat binding")
return self
if self.change_type == PointsChangeType.GRANT:
if (
self.direction != 1
or self.biz_type != PointsBizType.CHAT
or self.biz_id is None
):
raise ValueError("grant must use direction=1 and chat binding")
return self
if self.biz_type != PointsBizType.CHAT or self.biz_id is None:
raise ValueError("adjust must use chat binding")
return self
+72
View File
@@ -0,0 +1,72 @@
from __future__ import annotations
import re
from typing import Literal
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from pydantic import BaseModel, ConfigDict, Field, field_validator
_BCP47_PATTERN = re.compile(r"^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$")
_COUNTRY_PATTERN = re.compile(r"^[A-Z]{2}$")
class PreferenceSettings(BaseModel):
interface_language: str = "zh-CN"
ai_language: str = "zh-CN"
timezone: str = "Asia/Shanghai"
country: str = "CN"
@field_validator("interface_language", "ai_language")
@classmethod
def validate_language(cls, value: str) -> str:
if not _BCP47_PATTERN.fullmatch(value):
raise ValueError("language must be a valid BCP-47 tag")
return value
@field_validator("timezone")
@classmethod
def validate_timezone(cls, value: str) -> str:
try:
ZoneInfo(value)
except ZoneInfoNotFoundError as exc:
raise ValueError("timezone must be a valid IANA timezone") from exc
return value
@field_validator("country")
@classmethod
def validate_country(cls, value: str) -> str:
normalized = value.upper()
if not _COUNTRY_PATTERN.fullmatch(normalized):
raise ValueError("country must be an ISO 3166-1 alpha-2 code")
return normalized
class ProfileSettingsV1(BaseModel):
version: Literal[1] = 1
preferences: PreferenceSettings = Field(default_factory=PreferenceSettings)
privacy: dict = Field(default_factory=dict)
notification: dict = Field(default_factory=dict)
ProfileSettingsUnion = ProfileSettingsV1
def parse_profile_settings(raw: dict | None) -> ProfileSettingsUnion:
payload = dict(raw or {})
payload.setdefault("version", 1)
return ProfileSettingsV1.model_validate(payload)
def upgrade_to_latest(settings: ProfileSettingsUnion) -> ProfileSettingsV1:
return settings
class UserContext(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
username: str
email: str | None = None
avatar_url: str | None = None
bio: str | None = None
settings: ProfileSettingsUnion | None = None