refactor(schema): 重构数据库模型和 schema,清理废弃表
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
"""drop duplicate indexes on llm_factory and llms
|
||||||
|
|
||||||
|
Revision ID: 20260407_0003
|
||||||
|
Revises: 20260407_0002
|
||||||
|
Create Date: 2026-04-07 00:00:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "20260407_0003"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "20260407_0002"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_llm_factory_name")
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_llms_model_code")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("CREATE INDEX ix_llm_factory_name ON llm_factory(name)")
|
||||||
|
op.execute("CREATE INDEX ix_llms_model_code ON llms(model_code)")
|
||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from .agent_chat_message import AgentChatMessage
|
from .agent_chat_message import AgentChatMessage
|
||||||
from .agent_chat_session import AgentChatSession
|
from .agent_chat_session import AgentChatSession
|
||||||
from .auth_user import AuthUser
|
from .auth_user import AuthUser
|
||||||
|
from .invite_code import InviteCode
|
||||||
from .llm import Llm
|
from .llm import Llm
|
||||||
from .llm_factory import LlmFactory
|
from .llm_factory import LlmFactory
|
||||||
from .points_ledger import PointsLedger
|
from .points_ledger import PointsLedger
|
||||||
@@ -14,6 +15,7 @@ __all__ = [
|
|||||||
"AgentChatMessage",
|
"AgentChatMessage",
|
||||||
"AgentChatSession",
|
"AgentChatSession",
|
||||||
"AuthUser",
|
"AuthUser",
|
||||||
|
"InviteCode",
|
||||||
"Llm",
|
"Llm",
|
||||||
"LlmFactory",
|
"LlmFactory",
|
||||||
"PointsLedger",
|
"PointsLedger",
|
||||||
|
|||||||
@@ -21,6 +21,4 @@ class Llm(TimestampMixin, SoftDeleteMixin, Base):
|
|||||||
nullable=False,
|
nullable=False,
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
model_code: Mapped[str] = mapped_column(
|
model_code: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||||
String(50), nullable=False, unique=True, index=True
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ class LlmFactory(TimestampMixin, SoftDeleteMixin, Base):
|
|||||||
id: Mapped[uuid.UUID] = mapped_column(
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
)
|
)
|
||||||
name: Mapped[str] = mapped_column(
|
name: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
||||||
String(50), nullable=False, unique=True, index=True
|
|
||||||
)
|
|
||||||
request_url: Mapped[str] = mapped_column(String(255), nullable=False)
|
request_url: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
avatar: Mapped[str | None] = mapped_column(Text, nullable=True)
|
avatar: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|||||||
@@ -34,3 +34,8 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base):
|
|||||||
server_default=text("'{}'::jsonb"),
|
server_default=text("'{}'::jsonb"),
|
||||||
default=dict,
|
default=dict,
|
||||||
)
|
)
|
||||||
|
referred_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
ForeignKey("profiles.id", ondelete="SET NULL"),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
from schemas.agent.ui_hints import UiHintsPayload
|
from schemas.agent.ui_hints import UiHintsPayload
|
||||||
from schemas.domain.divination import DerivedDivinationData
|
from schemas.domain.divination import DerivedDivinationData
|
||||||
@@ -45,7 +45,6 @@ class WorkerAgentOutputLite(BaseModel):
|
|||||||
|
|
||||||
status: RunStatus = RunStatus.SUCCESS
|
status: RunStatus = RunStatus.SUCCESS
|
||||||
sign_level: Literal["上上签", "中上签", "中下签", "下下签"]
|
sign_level: Literal["上上签", "中上签", "中下签", "下下签"]
|
||||||
summary: str = Field(min_length=1, max_length=300)
|
|
||||||
conclusion: list[str] = Field(min_length=1, max_length=6)
|
conclusion: list[str] = Field(min_length=1, max_length=6)
|
||||||
focus_points: list[str] = Field(default_factory=list, max_length=6)
|
focus_points: list[str] = Field(default_factory=list, max_length=6)
|
||||||
advice: list[str] = Field(min_length=1, max_length=6)
|
advice: list[str] = Field(min_length=1, max_length=6)
|
||||||
@@ -53,20 +52,6 @@ class WorkerAgentOutputLite(BaseModel):
|
|||||||
answer: str = Field(min_length=1, max_length=4000)
|
answer: str = Field(min_length=1, max_length=4000)
|
||||||
error: ErrorInfo | None = None
|
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)
|
|
||||||
divination_derived: DerivedDivinationData | None = None
|
|
||||||
|
|
||||||
@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):
|
class WorkerAgentOutputRich(WorkerAgentOutputLite):
|
||||||
ui_hints: UiHintsPayload | None = None
|
ui_hints: UiHintsPayload | None = None
|
||||||
@@ -75,6 +60,8 @@ class WorkerAgentOutputRich(WorkerAgentOutputLite):
|
|||||||
class AgentOutput(WorkerAgentOutputRich):
|
class AgentOutput(WorkerAgentOutputRich):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
divination_derived: DerivedDivinationData | None = None
|
||||||
|
|
||||||
|
|
||||||
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
|
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
|
||||||
|
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
|
||||||
|
|
||||||
|
|
||||||
class SessionStateSnapshot(BaseModel):
|
|
||||||
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
|
|
||||||
|
|
||||||
pass
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -7,6 +8,7 @@ from uuid import UUID
|
|||||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||||
|
|
||||||
from ..enums import (
|
from ..enums import (
|
||||||
|
PointsBizType,
|
||||||
PointsChangeType,
|
PointsChangeType,
|
||||||
PointsOperatorType,
|
PointsOperatorType,
|
||||||
)
|
)
|
||||||
@@ -76,3 +78,51 @@ def parse_points_ledger_metadata(
|
|||||||
if change_type == PointsChangeType.GRANT:
|
if change_type == PointsChangeType.GRANT:
|
||||||
return GrantLedgerMetadata.model_validate(payload)
|
return GrantLedgerMetadata.model_validate(payload)
|
||||||
return AdjustLedgerMetadata.model_validate(payload)
|
return AdjustLedgerMetadata.model_validate(payload)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
TodoOrder = Annotated[int, Field(ge=0)]
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -41,17 +41,22 @@ class PreferenceSettings(BaseModel):
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationSettings(BaseModel):
|
||||||
|
allow_notifications: bool = True
|
||||||
|
allow_vibration: bool = True
|
||||||
|
|
||||||
|
|
||||||
class ProfileSettingsV1(BaseModel):
|
class ProfileSettingsV1(BaseModel):
|
||||||
version: Literal[1] = 1
|
version: Literal[1] = 1
|
||||||
preferences: PreferenceSettings = Field(default_factory=PreferenceSettings)
|
preferences: PreferenceSettings = Field(default_factory=PreferenceSettings)
|
||||||
privacy: dict = Field(default_factory=dict)
|
privacy: dict[str, object] = Field(default_factory=dict)
|
||||||
notification: dict = Field(default_factory=dict)
|
notification: NotificationSettings = Field(default_factory=NotificationSettings)
|
||||||
|
|
||||||
|
|
||||||
ProfileSettingsUnion = ProfileSettingsV1
|
ProfileSettingsUnion = ProfileSettingsV1
|
||||||
|
|
||||||
|
|
||||||
def parse_profile_settings(raw: dict | None) -> ProfileSettingsUnion:
|
def parse_profile_settings(raw: dict[str, object] | None) -> ProfileSettingsUnion:
|
||||||
payload = dict(raw or {})
|
payload = dict(raw or {})
|
||||||
payload.setdefault("version", 1)
|
payload.setdefault("version", 1)
|
||||||
return ProfileSettingsV1.model_validate(payload)
|
return ProfileSettingsV1.model_validate(payload)
|
||||||
|
|||||||
@@ -208,15 +208,11 @@ class HistoryAgentOutput(BaseModel):
|
|||||||
|
|
||||||
status: Literal["success", "failed"] | None = None
|
status: Literal["success", "failed"] | None = None
|
||||||
sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None
|
sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None
|
||||||
summary: str | None = None
|
|
||||||
conclusion: list[str] = Field(default_factory=list)
|
conclusion: list[str] = Field(default_factory=list)
|
||||||
focus_points: list[str] = Field(default_factory=list)
|
focus_points: list[str] = Field(default_factory=list)
|
||||||
advice: list[str] = Field(default_factory=list)
|
advice: list[str] = Field(default_factory=list)
|
||||||
keywords: list[str] = Field(default_factory=list)
|
keywords: list[str] = Field(default_factory=list)
|
||||||
answer: str | None = None
|
answer: str | None = None
|
||||||
key_points: list[str] = Field(default_factory=list)
|
|
||||||
result_type: str | None = None
|
|
||||||
suggested_actions: list[str] = Field(default_factory=list)
|
|
||||||
divination_derived: DerivedDivinationData | None = None
|
divination_derived: DerivedDivinationData | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -109,11 +109,7 @@ def _extract_worker_agent_output(
|
|||||||
try:
|
try:
|
||||||
agent_output = AgentOutput.model_validate(agent_output_data)
|
agent_output = AgentOutput.model_validate(agent_output_data)
|
||||||
except Exception:
|
except Exception:
|
||||||
normalized_payload = _normalize_agent_output_payload(agent_output_data)
|
return None
|
||||||
try:
|
|
||||||
agent_output = AgentOutput.model_validate(normalized_payload)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not agent_output:
|
if not agent_output:
|
||||||
return None
|
return None
|
||||||
@@ -123,37 +119,6 @@ def _extract_worker_agent_output(
|
|||||||
return payload or None
|
return payload or None
|
||||||
|
|
||||||
|
|
||||||
def _normalize_agent_output_payload(agent_output_data: Any) -> dict[str, Any] | None:
|
|
||||||
if not isinstance(agent_output_data, dict):
|
|
||||||
return None
|
|
||||||
normalized = dict(agent_output_data)
|
|
||||||
derived = normalized.get("divination_derived")
|
|
||||||
if isinstance(derived, dict):
|
|
||||||
normalized["divination_derived"] = _normalize_divination_derived(derived)
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_divination_derived(value: Any) -> Any:
|
|
||||||
if isinstance(value, dict):
|
|
||||||
result: dict[str, Any] = {}
|
|
||||||
for key, item in value.items():
|
|
||||||
normalized_key = _snake_to_camel(key)
|
|
||||||
result[normalized_key] = _normalize_divination_derived(item)
|
|
||||||
return result
|
|
||||||
if isinstance(value, list):
|
|
||||||
return [_normalize_divination_derived(item) for item in value]
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _snake_to_camel(value: str) -> str:
|
|
||||||
if "_" not in value:
|
|
||||||
return value
|
|
||||||
parts = value.split("_")
|
|
||||||
if not parts:
|
|
||||||
return value
|
|
||||||
return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def mime_to_suffix(mime_type: str) -> str:
|
def mime_to_suffix(mime_type: str) -> str:
|
||||||
mapping = {
|
mapping = {
|
||||||
"image/png": "png",
|
"image/png": "png",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from models.points_ledger import PointsLedger
|
from models.points_ledger import PointsLedger
|
||||||
from models.user_points import UserPoints
|
from models.user_points import UserPoints
|
||||||
from schemas.shared.points import ApplyPointsChangeCommand
|
from schemas.domain.points import ApplyPointsChangeCommand
|
||||||
|
|
||||||
|
|
||||||
class PointsRepository:
|
class PointsRepository:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from uuid import UUID, uuid4
|
|||||||
from core.http.errors import ApiProblemError, problem_payload
|
from core.http.errors import ApiProblemError, problem_payload
|
||||||
from schemas.domain.points import ConsumeLedgerMetadata, PointsChargeSnapshot
|
from schemas.domain.points import ConsumeLedgerMetadata, PointsChargeSnapshot
|
||||||
from schemas.enums import PointsBizType, PointsChangeType, PointsOperatorType
|
from schemas.enums import PointsBizType, PointsChangeType, PointsOperatorType
|
||||||
from schemas.shared.points import ApplyPointsChangeCommand
|
from schemas.domain.points import ApplyPointsChangeCommand
|
||||||
from v1.points.repository import PointsRepository
|
from v1.points.repository import PointsRepository
|
||||||
|
|
||||||
RUN_POINTS_COST = 20
|
RUN_POINTS_COST = 20
|
||||||
|
|||||||
@@ -160,7 +160,9 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi
|
|||||||
### 2) `TEXT_MESSAGE_END`
|
### 2) `TEXT_MESSAGE_END`
|
||||||
|
|
||||||
- Standard final answer event.
|
- Standard final answer event.
|
||||||
- Existing fields remain canonical: `sign_level`, `summary`, `conclusion`, `focus_points`, `advice`, `keywords`, `answer`.
|
- Existing fields remain canonical: `sign_level`, `conclusion`, `focus_points`, `advice`, `keywords`, `answer`.
|
||||||
|
- Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `ai_language` preference unless user explicitly requests otherwise.
|
||||||
|
- Canonical six-yao terms remain Chinese in protocol text (for example: 世爻、应爻、动爻、静爻、六亲、六神、伏神、月建、日辰、月破、日冲、空亡、五行旺衰).
|
||||||
|
|
||||||
Frontend should combine:
|
Frontend should combine:
|
||||||
|
|
||||||
@@ -189,15 +191,11 @@ Frontend should combine:
|
|||||||
"agent_output": {
|
"agent_output": {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"sign_level": "中上签",
|
"sign_level": "中上签",
|
||||||
"summary": "...",
|
|
||||||
"conclusion": ["..."],
|
"conclusion": ["..."],
|
||||||
"focus_points": ["..."],
|
"focus_points": ["..."],
|
||||||
"advice": ["..."],
|
"advice": ["..."],
|
||||||
"keywords": ["..."],
|
"keywords": ["..."],
|
||||||
"answer": "...",
|
"answer": "...",
|
||||||
"key_points": ["..."],
|
|
||||||
"result_type": "structured_payload",
|
|
||||||
"suggested_actions": ["..."],
|
|
||||||
"divination_derived": {
|
"divination_derived": {
|
||||||
"binaryCode": "101001",
|
"binaryCode": "101001",
|
||||||
"changedBinaryCode": "100001",
|
"changedBinaryCode": "100001",
|
||||||
|
|||||||
Reference in New Issue
Block a user