refactor: 重构 schemas 结构,统一枚举定义

This commit is contained in:
qzl
2026-03-25 12:36:31 +08:00
parent 389f5248fc
commit d22ded21f8
122 changed files with 774 additions and 1456 deletions
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<item android:drawable="@color/launchBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<item android:drawable="@color/launchBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
<color name="launchBackground">#EFF8FF</color>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -1,23 +1,17 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
class AuthBootScreen extends StatelessWidget {
const AuthBootScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
backgroundColor: AppColors.authBackgroundTop,
return Scaffold(
backgroundColor: const Color(0xFFEFF8FF),
body: SafeArea(
child: Center(
child: AppLoadingIndicator(
variant: AppLoadingVariant.surface,
size: 28,
strokeWidth: 2.5,
color: AppColors.authPrimaryButton,
trackColor: AppColors.authPrimaryButtonDisabled,
child: Image.asset(
'assets/branding/assistant_octopus_foreground.png',
width: 260,
),
),
),
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
enum ScheduleSourceType { manual, imported, agentGenerated }
enum ScheduleStatus { active, completed, canceled, archived }
enum ScheduleStatus { active, archived }
class ScheduleItemModel {
final String id;
@@ -279,9 +279,7 @@ ScheduleSourceType _sourceTypeFromApi(String? raw) {
ScheduleStatus _statusFromApi(String? raw) {
switch (raw) {
case 'completed':
return ScheduleStatus.completed;
case 'canceled':
return ScheduleStatus.canceled;
case 'archived':
return ScheduleStatus.archived;
case 'active':
@@ -294,10 +292,6 @@ String _statusToApi(ScheduleStatus status) {
switch (status) {
case ScheduleStatus.active:
return 'active';
case ScheduleStatus.completed:
return 'completed';
case ScheduleStatus.canceled:
return 'canceled';
case ScheduleStatus.archived:
return 'archived';
}
+1
View File
@@ -41,6 +41,7 @@ flutter:
uses-material-design: true
assets:
- assets/images/
- assets/branding/
flutter_launcher_icons:
android: true
+4
View File
@@ -45,6 +45,10 @@ This file governs `backend/**` only. Keep it minimal, enforceable, and non-dupli
- Use service-role DB access only in backend.
- Soft delete uses `deleted_at`; reads must exclude deleted records by default.
- Alembic is the only schema migration source of truth.
- Database migrations use `./infra/scripts/dev-migrate.sh`:
- `migrate` - run migrations only
- `init-data` - seed data only
- `bootstrap` - migrate + init-data
## Agent Runtime & Tools
@@ -0,0 +1,54 @@
"""converge schedule item status to active or archived
Revision ID: 202603250001
Revises: 202603240001
Create Date: 2026-03-25 10:00:00
"""
from typing import Sequence, Union
from alembic import op
revision: str = "202603250001"
down_revision: Union[str, Sequence[str], None] = "202603240001"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"""
UPDATE public.schedule_items
SET status = 'archived'
WHERE status IN ('completed', 'canceled')
"""
)
op.execute(
"""
ALTER TABLE public.schedule_items
DROP CONSTRAINT IF EXISTS chk_schedule_item_status
"""
)
op.execute(
"""
ALTER TABLE public.schedule_items
ADD CONSTRAINT chk_schedule_item_status
CHECK (status IN ('active', 'archived'))
"""
)
def downgrade() -> None:
op.execute(
"""
ALTER TABLE public.schedule_items
DROP CONSTRAINT IF EXISTS chk_schedule_item_status
"""
)
op.execute(
"""
ALTER TABLE public.schedule_items
ADD CONSTRAINT chk_schedule_item_status
CHECK (status IN ('active', 'completed', 'canceled', 'archived'))
"""
)
@@ -7,8 +7,9 @@ from uuid import UUID
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
from models.agent_chat_message import AgentChatMessage
from models.agent_chat_session import AgentChatSession
from schemas.enums import AgentChatMessageRole, AgentChatSessionStatus
class MessageRepository:
+2 -3
View File
@@ -6,12 +6,11 @@ from uuid import UUID
from core.agentscope.events.persistence import MessageRepository, SessionRepository
from core.logging import get_logger
from models.agent_chat_message import AgentChatMessageRole
from models.agent_chat_session import AgentChatSessionStatus
from schemas.enums import AgentChatMessageRole, AgentChatSessionStatus
from schemas.agent.system_agent import AgentType
from schemas.agent.runtime_models import AgentOutput, RouterAgentOutput, ToolAgentOutput
from schemas.agent.visibility import SystemVisibilityBit, bit_mask
from schemas.messages.chat_message import AgentChatMessageMetadata
from schemas.domain.chat_message import AgentChatMessageMetadata
class EventStore(Protocol):
@@ -9,7 +9,7 @@ from uuid import UUID
import redis.asyncio as redis
from core.config.settings import config
from core.logging import get_logger
from schemas.user import (
from schemas.shared.user import (
UserContext,
parse_profile_settings,
)
@@ -2,7 +2,7 @@ from __future__ import annotations
import json
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
def _wrap_section(section: str, content: str) -> str:
@@ -17,8 +17,8 @@ from core.agentscope.prompts.route_prompt import build_frontend_route_prompt
from core.agentscope.prompts.tool_prompt import build_tools_prompt
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
from schemas.agent.forwarded_props import ClientTimeContext
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user.context import UserContext
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.shared.user import UserContext
def _wrap_section(section: str, content: str) -> str:
@@ -6,9 +6,9 @@ from ag_ui.core.types import RunAgentInput
from agentscope.message import Msg
from core.agentscope.runtime.runner import AgentScopeRunner
from core.logging import get_logger
from schemas.automation import RuntimeConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user import UserContext
from schemas.domain.automation import RuntimeConfig
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.shared.user import UserContext
logger = get_logger("core.agentscope.runtime.orchestrator")
@@ -40,9 +40,9 @@ from schemas.agent.system_agent import (
AgentType,
SystemAgentLLMConfig,
)
from schemas.automation import RuntimeConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user import UserContext
from schemas.domain.automation import RuntimeConfig
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.shared.user import UserContext
from services.litellm.service import LiteLLMService
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
+4 -4
View File
@@ -20,13 +20,13 @@ from core.config.settings import config
from core.db.session import AsyncSessionLocal
from core.logging import get_logger
from core.taskiq.app import worker_agent_broker, worker_automation_broker
from schemas.automation import MessageContextConfig, RuntimeConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.messages.chat_message import (
from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.domain.chat_message import (
AgentChatMessageMetadata,
extract_user_message_attachments,
)
from schemas.user import UserContext
from schemas.shared.user import UserContext
from services.base.redis import get_or_init_redis_client
from services.base.supabase import supabase_service
from v1.agent.repository import AgentRepository
@@ -6,7 +6,7 @@ from typing import Any, Protocol
from schemas.agent.visibility import SystemVisibilityBit, bit_mask
from schemas.automation import ContextWindowMode, MessageContextConfig
from schemas.domain.automation import ContextWindowMode, MessageContextConfig
_DEFAULT_CONTEXT_WINDOW_USER_MESSAGES = 20
@@ -84,7 +84,7 @@ class CalendarWriteOperation(BaseModel):
le=10080,
description="Reminder minutes before event start.",
)
status: Literal["active", "completed", "canceled", "archived"] | None = Field(
status: Literal["active", "archived"] | None = Field(
default=None,
description="Optional status for update action.",
)
@@ -163,6 +163,10 @@ async def calendar_read(
) -> ToolResponse:
"""Read calendar events with optional keyword filtering and pagination.
Status semantics for returned events:
- active: Event is actionable.
- archived: Event is historical/expired and should not trigger reminders.
Args:
query: Optional keyword used to filter events by text fields.
page: Page number starting from 1.
@@ -15,9 +15,9 @@ from core.agentscope.tools.utils.tool_response_builder import (
build_error_output,
build_tool_response,
)
from models.memories import MemoryType
from schemas.enums import MemoryType
from schemas.agent.runtime_models import ErrorInfo, ToolAgentOutput, ToolStatus
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
class MemoryWriteArgs(BaseModel):
@@ -46,6 +46,9 @@ def create_schedule_service(
def schedule_event_to_dict(event: object) -> dict[str, Any]:
event_id = str(getattr(event, "id"))
metadata = getattr(event, "metadata", None)
status_value = getattr(event, "status", None)
if hasattr(status_value, "value"):
status_value = getattr(status_value, "value")
location_value = getattr(metadata, "location", None)
color_value = getattr(metadata, "color", None) or "#4F46E5"
reminder_minutes_value = getattr(metadata, "reminder_minutes", None)
@@ -58,6 +61,7 @@ def schedule_event_to_dict(event: object) -> dict[str, Any]:
if getattr(event, "end_at") is not None
else None,
"timezone": getattr(event, "timezone"),
"status": status_value,
"location": location_value,
"color": color_value,
"reminderMinutes": reminder_minutes_value,
+1 -1
View File
@@ -5,7 +5,7 @@ from uuid import UUID
from core.config.settings import config
from core.logging import get_logger
from schemas.automation import RuntimeConfig
from schemas.domain.automation import RuntimeConfig
logger = get_logger("core.automation.scheduler")
+2 -1
View File
@@ -6,7 +6,7 @@ from models.automation_jobs import AutomationJob
from models.group_members import GroupMember
from models.groups import Group
from models.inbox_messages import InboxMessage
from models.invite_code import InviteCode, InviteCodeStatus
from models.invite_code import InviteCode
from models.llm import Llm
from models.llm_factory import LlmFactory
from models.memories import Memory
@@ -16,6 +16,7 @@ from models.schedule_subscriptions import ScheduleSubscription
from models.system_agents import SystemAgents
from models.todos import Todo
from models.todo_sources import TodoSource
from schemas.enums import InviteCodeStatus
__all__ = [
"AgentChatMessage",
+2 -7
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
from decimal import Decimal
import uuid
from enum import Enum
from sqlalchemy import (
BigInteger,
@@ -19,13 +18,9 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AgentChatMessageRole
class AgentChatMessageRole(str, Enum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
TOOL = "tool"
__all__ = ["AgentChatMessage", "AgentChatMessageRole"]
class AgentChatMessage(TimestampMixin, SoftDeleteMixin, Base):
+2 -12
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import datetime
from decimal import Decimal
import uuid
from enum import Enum
from sqlalchemy import (
DateTime,
@@ -19,18 +18,9 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AgentChatSessionStatus, SessionType
class AgentChatSessionStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class SessionType(str, Enum):
CHAT = "chat"
AUTOMATION = "automation"
__all__ = ["AgentChatSession", "AgentChatSessionStatus", "SessionType"]
class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base):
+2 -10
View File
@@ -2,23 +2,15 @@ from __future__ import annotations
import uuid
from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, JSON, String
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AutomationJobStatus, ScheduleType
class AutomationJobStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class ScheduleType(str, Enum):
DAILY = "daily"
WEEKLY = "weekly"
__all__ = ["AutomationJob", "AutomationJobStatus", "ScheduleType"]
class AutomationJob(TimestampMixin, SoftDeleteMixin, Base):
+2 -8
View File
@@ -2,21 +2,15 @@ from __future__ import annotations
import uuid
from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import FriendshipStatus
class FriendshipStatus(str, Enum):
PENDING = "pending"
ACCEPTED = "accepted"
BLOCKED = "blocked"
DECLINED = "declined"
CANCELED = "canceled"
__all__ = ["Friendship", "FriendshipStatus"]
class Friendship(TimestampMixin, SoftDeleteMixin, Base):
+7 -17
View File
@@ -1,30 +1,20 @@
from __future__ import annotations
import uuid
from enum import Enum
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import GroupMemberRole, GroupMemberSource, GroupMemberStatus
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"
__all__ = [
"GroupMember",
"GroupMemberRole",
"GroupMemberSource",
"GroupMemberStatus",
]
class GroupMember(TimestampMixin, SoftDeleteMixin, Base):
+2 -5
View File
@@ -1,18 +1,15 @@
from __future__ import annotations
import uuid
from enum import Enum
from sqlalchemy import String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import GroupStatus
class GroupStatus(str, Enum):
ACTIVE = "active"
ARCHIVED = "archived"
__all__ = ["Group", "GroupStatus"]
class Group(TimestampMixin, SoftDeleteMixin, Base):
+2 -14
View File
@@ -1,27 +1,15 @@
from __future__ import annotations
import uuid
from enum import Enum
from sqlalchemy import Boolean, String
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
from schemas.enums import InboxMessageStatus, InboxMessageType
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"
__all__ = ["InboxMessage", "InboxMessageType", "InboxMessageStatus"]
class InboxMessage(TimestampMixin, Base):
+2 -6
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import uuid
from datetime import datetime
from enum import Enum
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import UUID
@@ -10,12 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
from core.db.types import json_jsonb
from schemas.enums import InviteCodeStatus
class InviteCodeStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
EXPIRED = "expired"
__all__ = ["InviteCode", "InviteCodeStatus"]
class InviteCode(TimestampMixin, Base):
+2 -10
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
import uuid
from enum import Enum
from sqlalchemy import String
from sqlalchemy.dialects.postgresql import UUID
@@ -9,16 +8,9 @@ from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
from core.db.types import json_jsonb
from schemas.enums import MemoryStatus, MemoryType
class MemoryType(str, Enum):
USER = "user"
WORK = "work"
class MemoryStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
__all__ = ["Memory", "MemoryType", "MemoryStatus"]
class Memory(TimestampMixin, Base):
+2 -13
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import uuid
from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, String, Text
from sqlalchemy.dialects.postgresql import UUID
@@ -10,19 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from core.db.types import json_jsonb
from schemas.enums import ScheduleItemSourceType, ScheduleItemStatus
class ScheduleItemStatus(str, Enum):
ACTIVE = "active"
COMPLETED = "completed"
CANCELED = "canceled"
ARCHIVED = "archived"
class ScheduleItemSourceType(str, Enum):
MANUAL = "manual"
IMPORTED = "imported"
AGENT_GENERATED = "agent_generated"
__all__ = ["ScheduleItem", "ScheduleItemStatus", "ScheduleItemSourceType"]
class ScheduleItem(TimestampMixin, SoftDeleteMixin, Base):
+7 -20
View File
@@ -1,33 +1,20 @@
from __future__ import annotations
import uuid
from enum import Enum
from sqlalchemy import Integer, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
from schemas.enums import NotifyLevel, SubscriptionPermission, SubscriptionStatus
class SubscriptionStatus(str, Enum):
ACTIVE = "active"
PENDING = "pending"
PAUSED = "paused"
UNSUBSCRIBED = "unsubscribed"
class NotifyLevel(str, Enum):
ALL = "all"
MENTIONS = "mentions"
NONE = "none"
class SubscriptionPermission(int, Enum):
VIEW = 1 # 001 - 可查看
INVITE = 2 # 010 - 可邀请
EDIT = 4 # 100 - 可编辑
OWNER = 7 # 111 - 所有者(VIEW + INVITE + EDIT
__all__ = [
"ScheduleSubscription",
"SubscriptionStatus",
"NotifyLevel",
"SubscriptionPermission",
]
class ScheduleSubscription(TimestampMixin, Base):
+2 -13
View File
@@ -2,26 +2,15 @@ from __future__ import annotations
import uuid
from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, Integer, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import TodoPriority, TodoStatus
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
__all__ = ["Todo", "TodoStatus", "TodoPriority"]
class Todo(TimestampMixin, SoftDeleteMixin, Base):
+1 -45
View File
@@ -1,45 +1 @@
"""Centralized shared schemas for cross-module contracts."""
from schemas.inbox.messages import (
CalendarContent,
CalendarDeleteContent,
CalendarInviteContent,
CalendarUpdateContent,
FriendshipContent,
InboxMessageContent,
InboxMessageStatus,
InboxMessageType,
parse_calendar_content,
)
from schemas.invite_codes import InviteCodeRewardConfig
from schemas.messages import AgentChatMessageMetadata
from schemas.schedule.items import (
AttachmentType,
ScheduleItemMetadata,
ScheduleItemMetadataAttachment,
ScheduleItemSourceType,
ScheduleItemStatus,
)
from schemas.sessions import SessionStateSnapshot
from schemas.user.context import UserContext
__all__ = [
"AgentChatMessageMetadata",
"AttachmentType",
"CalendarContent",
"CalendarDeleteContent",
"CalendarInviteContent",
"CalendarUpdateContent",
"FriendshipContent",
"InboxMessageContent",
"InboxMessageStatus",
"InboxMessageType",
"InviteCodeRewardConfig",
"ScheduleItemMetadata",
"ScheduleItemMetadataAttachment",
"ScheduleItemSourceType",
"ScheduleItemStatus",
"SessionStateSnapshot",
"UserContext",
"parse_calendar_content",
]
"""Backend reusable schemas package."""
+1 -1
View File
@@ -12,7 +12,7 @@ Version: 2.0
from __future__ import annotations
from enum import Enum
from typing import Any, Literal, NotRequired, TypedDict, Union
from typing import Any, Literal, TypedDict, Union
# ============================================================
# Enums
+1
View File
@@ -0,0 +1 @@
"""Reusable domain schemas shared across backend modules."""
@@ -2,13 +2,27 @@ 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
from models.automation_jobs import AutomationJob as OrmAutomationJob
from models.automation_jobs 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):
@@ -50,8 +64,7 @@ class ScheduleConfig(BaseModel):
invalid = [day for day in self.weekdays if day < 1 or day > 7]
if invalid:
raise ValueError("weekdays must be within 1-7")
deduped = sorted(set(self.weekdays))
self.weekdays = deduped
self.weekdays = sorted(set(self.weekdays))
else:
self.weekdays = None
return self
@@ -90,20 +103,20 @@ class AutomationJob(BaseModel):
updated_at: datetime
@classmethod
def from_orm(cls, obj: OrmAutomationJob) -> "AutomationJob":
def from_orm(cls, obj: object) -> "AutomationJob":
return cls(
id=obj.id,
owner_id=obj.owner_id,
bootstrap_key=obj.bootstrap_key,
title=obj.title,
config=AutomationJobConfig.model_validate(obj.config or {}),
next_run_at=obj.next_run_at,
timezone=obj.timezone,
last_run_at=obj.last_run_at,
status=obj.status,
created_by=obj.created_by,
created_at=obj.created_at,
updated_at=obj.updated_at,
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
@@ -1,24 +1,22 @@
from __future__ import annotations
import json
from enum import Enum
from typing import ClassVar, Literal, Union
from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import InboxMessageStatus, InboxMessageType
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"
__all__ = [
"InboxMessageType",
"InboxMessageStatus",
"CalendarInviteContent",
"CalendarUpdateContent",
"CalendarDeleteContent",
"FriendshipContent",
"CalendarContent",
"InboxMessageContent",
"parse_calendar_content",
]
class CalendarInviteContent(BaseModel):
@@ -1,13 +1,11 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import ClassVar, Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from schemas.memories.memory_content import (
from schemas.domain.memory_content import (
TeamMember,
UserMemoryContent,
UserPreferences,
@@ -15,16 +13,7 @@ from schemas.memories.memory_content import (
WorkProfileContent,
WorkProject,
)
class MemoryType(str, Enum):
USER = "user"
WORK = "work"
class MemoryStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
from schemas.enums import MemoryStatus, MemoryType
class MemoryModel(BaseModel):
@@ -5,6 +5,15 @@ 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):
@@ -32,16 +41,3 @@ class ScheduleItemMetadata(BaseModel):
attachments: list[ScheduleItemMetadataAttachment] = Field(default_factory=list)
reminder_minutes: int | None = Field(default=None, ge=0, le=10080)
version: Literal[1] = 1
class ScheduleItemStatus(str, Enum):
ACTIVE = "active"
COMPLETED = "completed"
CANCELED = "canceled"
ARCHIVED = "archived"
class ScheduleItemSourceType(str, Enum):
MANUAL = "manual"
IMPORTED = "imported"
AGENT_GENERATED = "agent_generated"
+136
View File
@@ -0,0 +1,136 @@
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 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"
PAUSED = "paused"
UNSUBSCRIBED = "unsubscribed"
class NotifyLevel(str, Enum):
ALL = "all"
MENTIONS = "mentions"
NONE = "none"
class SubscriptionPermission(int, Enum):
VIEW = 1
INVITE = 2
EDIT = 4
OWNER = 7
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"
-3
View File
@@ -1,3 +0,0 @@
from schemas.inbox.messages import InboxMessageStatus, InboxMessageType
__all__ = ["InboxMessageStatus", "InboxMessageType"]
-3
View File
@@ -1,3 +0,0 @@
from schemas.messages.chat_message import AgentChatMessage, AgentChatMessageMetadata
__all__ = ["AgentChatMessage", "AgentChatMessageMetadata"]
-27
View File
@@ -1,27 +0,0 @@
from schemas.inbox.messages import (
CalendarContent,
CalendarDeleteContent,
CalendarInviteContent,
CalendarUpdateContent,
parse_calendar_content,
)
from schemas.schedule.items import (
AttachmentType,
ScheduleItemMetadata,
ScheduleItemMetadataAttachment,
ScheduleItemSourceType,
ScheduleItemStatus,
)
__all__ = [
"AttachmentType",
"CalendarContent",
"CalendarDeleteContent",
"CalendarInviteContent",
"CalendarUpdateContent",
"ScheduleItemMetadata",
"ScheduleItemMetadataAttachment",
"ScheduleItemSourceType",
"ScheduleItemStatus",
"parse_calendar_content",
]
-3
View File
@@ -1,3 +0,0 @@
from schemas.sessions.chat_session import SessionStateSnapshot
__all__ = ["SessionStateSnapshot"]
+1
View File
@@ -0,0 +1 @@
"""Shared schemas used across multiple domain modules."""
-3
View File
@@ -1,3 +0,0 @@
from .contracts import TodoOrder
__all__ = ["TodoOrder"]
-17
View File
@@ -1,17 +0,0 @@
from schemas.user.context import (
PreferenceSettings,
ProfileSettingsUnion,
ProfileSettingsV1,
UserContext,
parse_profile_settings,
upgrade_to_latest,
)
__all__ = [
"PreferenceSettings",
"ProfileSettingsUnion",
"ProfileSettingsV1",
"UserContext",
"parse_profile_settings",
"upgrade_to_latest",
]
+3 -2
View File
@@ -9,10 +9,11 @@ from fastapi import HTTPException
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
from models.agent_chat_message import AgentChatMessage
from models.agent_chat_session import AgentChatSession
from models.system_agents import SystemAgents
from schemas.messages.chat_message import (
from schemas.enums import AgentChatMessageRole
from schemas.domain.chat_message import (
AgentChatMessage as AgentChatMessageSchema,
AgentChatMessageMetadata,
)
+3 -3
View File
@@ -18,8 +18,8 @@ from schemas.agent.forwarded_props import (
RuntimeMode,
)
from schemas.agent.visibility import SystemVisibilityBit, bit_mask
from schemas.automation import RuntimeConfig
from schemas.messages.chat_message import (
from schemas.domain.automation import RuntimeConfig
from schemas.domain.chat_message import (
AgentChatMessageMetadata,
UserMessageAttachment,
extract_user_message_attachments,
@@ -362,7 +362,7 @@ class AgentService:
before: date | None,
current_user: CurrentUser,
) -> HistorySnapshotResponse:
from schemas.messages.chat_message import AgentChatMessage
from schemas.domain.chat_message import AgentChatMessage
from v1.agent.utils import convert_message_to_history
from v1.agent.schemas import HistoryMessage
+1 -1
View File
@@ -11,7 +11,7 @@ from pydantic import ValidationError
from core.agentscope.tools.tool_config import AgentTool
from schemas.agent.system_agent import SystemAgentLLMConfig
from schemas.automation import (
from schemas.domain.automation import (
ContextSource,
ContextWindowMode,
MessageContextConfig,
+1 -1
View File
@@ -8,7 +8,7 @@ from collections.abc import Callable
from typing import Any
from core.agentscope.runtime.ui_compiler import compile as compile_ui_hints
from schemas.messages.chat_message import (
from schemas.domain.chat_message import (
AgentChatMessage,
AgentChatMessageMetadata,
extract_user_message_attachments,
@@ -7,7 +7,7 @@ from typing import Any
import yaml
from schemas.automation import AutomationJobConfig
from schemas.domain.automation import AutomationJobConfig
_CONFIG_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
@@ -10,12 +10,12 @@ from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.ext.asyncio import AsyncSession
from core.logging import get_logger
from models.automation_jobs import AutomationJob, AutomationJobStatus, ScheduleType
from models.memories import MemoryType
from models.automation_jobs import AutomationJob
from schemas.enums import AutomationJobStatus, MemoryType, ScheduleType
from models.profile import Profile
from schemas.automation import AutomationJobConfig, ScheduleConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user.context import parse_profile_settings
from schemas.domain.automation import AutomationJobConfig, ScheduleConfig
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.shared.user import parse_profile_settings
from v1.auth.automation_static_config import load_static_automation_job_config
from v1.auth.schemas import RegistrationBootstrapRequest
from v1.memories.repository import SQLAlchemyMemoriesRepository
+4 -3
View File
@@ -9,9 +9,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from core.db.base_repository import BaseRepository
from models.agent_chat_session import AgentChatSession, SessionType
from models.automation_jobs import AutomationJob, AutomationJobStatus, ScheduleType
from schemas.automation import AutomationJobConfig, ScheduleConfig
from models.agent_chat_session import AgentChatSession
from models.automation_jobs import AutomationJob
from schemas.enums import AutomationJobStatus, ScheduleType, SessionType
from schemas.domain.automation import AutomationJobConfig, ScheduleConfig
if TYPE_CHECKING:
from v1.automation_jobs.schemas import (
+15 -16
View File
@@ -7,9 +7,8 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from models.automation_jobs import AutomationJob as OrmAutomationJob
from models.automation_jobs import AutomationJobStatus
from schemas.automation import AutomationJobConfig
from schemas.domain.automation import AutomationJobConfig
from schemas.enums import AutomationJobStatus
class AutomationJobResponse(BaseModel):
@@ -29,20 +28,20 @@ class AutomationJobResponse(BaseModel):
updated_at: datetime
@classmethod
def from_orm(cls, obj: OrmAutomationJob) -> Self:
def from_orm(cls, obj: object) -> Self:
return cls(
id=obj.id,
owner_id=obj.owner_id,
bootstrap_key=obj.bootstrap_key,
title=obj.title,
timezone=obj.timezone,
status=obj.status,
is_system=obj.bootstrap_key is not None,
config=AutomationJobConfig.model_validate(obj.config or {}),
next_run_at=obj.next_run_at,
last_run_at=obj.last_run_at,
created_at=obj.created_at,
updated_at=obj.updated_at,
id=getattr(obj, "id"),
owner_id=getattr(obj, "owner_id"),
bootstrap_key=getattr(obj, "bootstrap_key"),
title=getattr(obj, "title"),
timezone=getattr(obj, "timezone"),
status=getattr(obj, "status"),
is_system=getattr(obj, "bootstrap_key") is not None,
config=AutomationJobConfig.model_validate(getattr(obj, "config", {}) or {}),
next_run_at=getattr(obj, "next_run_at"),
last_run_at=getattr(obj, "last_run_at"),
created_at=getattr(obj, "created_at"),
updated_at=getattr(obj, "updated_at"),
)
+2 -2
View File
@@ -7,8 +7,8 @@ from uuid import UUID
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from fastapi import HTTPException, status
from models.automation_jobs import ScheduleType
from schemas.automation import (
from schemas.enums import ScheduleType
from schemas.domain.automation import (
AutomationJob as AutomationJobSchema,
MessageContextConfig,
RuntimeConfig,
+4 -3
View File
@@ -9,9 +9,10 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.friendships import Friendship, FriendshipStatus
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType
from schemas.inbox.messages import FriendshipContent
from models.friendships import Friendship
from models.inbox_messages import InboxMessage
from schemas.enums import FriendshipStatus, InboxMessageStatus, InboxMessageType
from schemas.domain.inbox import FriendshipContent
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
+1 -1
View File
@@ -6,7 +6,7 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
class FriendRequestCreate(BaseModel):
+5 -4
View File
@@ -10,8 +10,9 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.friendships import Friendship, FriendshipStatus
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType
from models.friendships import Friendship
from models.inbox_messages import InboxMessage
from schemas.enums import FriendshipStatus, InboxMessageStatus, InboxMessageType
from v1.friendships.repository import FriendshipRepository
from v1.friendships.schemas import (
FriendRequestCreate,
@@ -22,7 +23,7 @@ from v1.users.repository import UserRepository
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
logger = get_logger("v1.friendships.service")
@@ -593,7 +594,7 @@ class FriendshipService(BaseService):
)
def _build_user_basic_info(self, profile: Any) -> "UserContext":
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
if profile is None:
return UserContext(id="", username="")
+2 -1
View File
@@ -7,7 +7,8 @@ from sqlalchemy import select, update
from sqlalchemy.exc import SQLAlchemyError
from core.logging import get_logger
from models.inbox_messages import InboxMessage, InboxMessageType, InboxMessageStatus
from models.inbox_messages import InboxMessage
from schemas.enums import InboxMessageStatus, InboxMessageType
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
+1 -1
View File
@@ -6,7 +6,7 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict
from schemas.inbox.messages import InboxMessageStatus, InboxMessageType
from schemas.domain.inbox import InboxMessageStatus, InboxMessageType
class InboxMessageResponse(BaseModel):
+2 -1
View File
@@ -9,7 +9,8 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.memories import Memory, MemoryStatus, MemoryType
from models.memories import Memory
from schemas.enums import MemoryStatus, MemoryType
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
+1 -1
View File
@@ -4,7 +4,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends, status
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from v1.memories.dependencies import get_memories_service
from v1.memories.schemas import (
MemoryListResponse,
+1 -1
View File
@@ -4,7 +4,7 @@ from typing import ClassVar
from pydantic import BaseModel, ConfigDict
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
class UserMemoryUpdate(BaseModel):
+3 -2
View File
@@ -8,8 +8,9 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.memories import Memory, MemoryType
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent
from models.memories import Memory
from schemas.enums import MemoryType
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from v1.memories.repository import MemoriesRepositoryLike
if TYPE_CHECKING:
+3 -2
View File
@@ -9,8 +9,9 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.schedule_items import ScheduleItem, ScheduleItemStatus
from models.schedule_subscriptions import ScheduleSubscription, SubscriptionStatus
from models.schedule_items import ScheduleItem
from models.schedule_subscriptions import ScheduleSubscription
from schemas.enums import ScheduleItemStatus, SubscriptionStatus
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
+2 -2
View File
@@ -7,14 +7,14 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from pydantic import BaseModel, ConfigDict, Field, field_validator
from schemas.inbox.messages import (
from schemas.domain.inbox import (
CalendarContent,
CalendarDeleteContent,
CalendarInviteContent,
CalendarUpdateContent,
parse_calendar_content,
)
from schemas.schedule.items import (
from schemas.domain.schedule import (
AttachmentType,
ScheduleItemMetadata,
ScheduleItemMetadataAttachment,
+12 -3
View File
@@ -10,9 +10,14 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.inbox_messages import InboxMessage, InboxMessageType, InboxMessageStatus
from models.inbox_messages import InboxMessage
from models.schedule_items import ScheduleItem
from models.schedule_subscriptions import SubscriptionPermission, SubscriptionStatus
from schemas.enums import (
InboxMessageStatus,
InboxMessageType,
SubscriptionPermission,
SubscriptionStatus,
)
from v1.auth.gateway import SupabaseAuthGateway
from v1.inbox_messages.repository import InboxMessageRepository
from v1.schedule_items.repository import ScheduleItemRepository
@@ -35,6 +40,8 @@ if TYPE_CHECKING:
logger = get_logger("v1.schedule_items.service")
_LEGACY_ARCHIVED_STATUSES = {"completed", "canceled"}
class AuthByPhoneGateway(Protocol):
async def get_user_by_phone(self, phone: str) -> "UserByPhoneResponse": ...
@@ -417,8 +424,10 @@ class ScheduleItemService(BaseService):
permission: int = 1,
) -> ScheduleItemResponse:
status_value = (
item.status.value if hasattr(item.status, "value") else item.status
item.status.value if hasattr(item.status, "value") else str(item.status)
)
if status_value in _LEGACY_ARCHIVED_STATUSES:
status_value = ScheduleItemStatus.ARCHIVED.value
source_type_value = (
item.source_type.value
if hasattr(item.source_type, "value")
+2 -1
View File
@@ -10,7 +10,8 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository
from core.logging import get_logger
from models.todo_sources import TodoSource
from models.todos import Todo, TodoPriority, TodoStatus
from models.todos import Todo
from schemas.enums import TodoPriority, TodoStatus
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
+1 -1
View File
@@ -5,7 +5,7 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from schemas.todo import TodoOrder
from schemas.domain.todo import TodoOrder
class TodoCreate(BaseModel):
+2 -1
View File
@@ -10,7 +10,8 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser
from core.db.base_service import BaseService
from core.logging import get_logger
from models.todos import Todo, TodoStatus
from models.todos import Todo
from schemas.enums import TodoStatus
from v1.schedule_items.repository import SQLAlchemyScheduleItemRepository
from v1.todo.repository import TodoRepository
from v1.todo.schemas import (
+1 -1
View File
@@ -5,7 +5,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
from v1.users.dependencies import get_user_service
from v1.users.schemas import UserSearchRequest, UserUpdateRequest
from v1.users.service import UserService
+3 -3
View File
@@ -13,14 +13,14 @@ from core.agentscope.persistence.user_context_cache import (
)
from core.db.base_service import BaseService
from core.logging import get_logger
from schemas.user.context import UserContext, parse_profile_settings
from schemas.shared.user import UserContext, parse_profile_settings
from v1.users.repository import UserRepository
from v1.users.schemas import UserSearchRequest, UserUpdateRequest
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
logger = get_logger("v1.users.service")
@@ -105,7 +105,7 @@ class UserService(BaseService):
)
async def get_user_by_id(self, user_id: UUID) -> "UserContext":
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
try:
profile = await self._repository.get_by_user_id(user_id)
+1 -1
View File
@@ -11,7 +11,7 @@ import uvicorn
from app import app
from core.auth.models import CurrentUser
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
from v1.users.dependencies import get_current_user, get_user_service
from v1.users.schemas import UserUpdateRequest
@@ -9,7 +9,7 @@ from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
from v1.friendships.dependencies import get_friendship_service
from v1.friendships.schemas import (
FriendRequestCreate,
@@ -8,7 +8,7 @@ from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from schemas.user.context import UserContext
from schemas.shared.user import UserContext
from v1.users.dependencies import get_current_user, get_user_service
from v1.users.schemas import UserSearchRequest, UserUpdateRequest
from v1.users.service import UserService
@@ -6,7 +6,7 @@ from uuid import uuid4
import pytest
from core.agentscope.persistence.user_context_cache import UserContextCache
from schemas.user.context import (
from schemas.shared.user import (
UserContext,
parse_profile_settings,
)
@@ -1,28 +0,0 @@
from __future__ import annotations
import pytest
from core.agentscope.runtime.registry_builder import build_consumer_registry
def test_build_consumer_registry_from_system_agent_configs() -> None:
registry = build_consumer_registry(
system_agent_configs={
"router": {"config": {"visibility_consumer_bit": 16}},
"worker": {"config": {"visibility_consumer_bit": 17}},
"memory": {"config": {"visibility_consumer_bit": 18}},
}
)
assert registry.resolve_agent_bit(agent_type="router") == 16
assert registry.resolve_agent_bit(agent_type="worker") == 17
def test_build_consumer_registry_rejects_duplicate_bit() -> None:
with pytest.raises(ValueError, match="duplicate visibility bit"):
build_consumer_registry(
system_agent_configs={
"router": {"config": {"visibility_consumer_bit": 16}},
"worker": {"config": {"visibility_consumer_bit": 16}},
}
)
@@ -6,8 +6,8 @@ import pytest
from ag_ui.core import RunAgentInput
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
from schemas.automation import MessageContextConfig, RuntimeConfig
from schemas.user import UserContext, parse_profile_settings
from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.shared.user import UserContext, parse_profile_settings
class _FakePipeline:
@@ -1,24 +0,0 @@
from __future__ import annotations
import pytest
from core.agentscope.runtime.pipeline_registry import build_default_pipeline_spec
def test_build_default_pipeline_spec_worker_has_two_stages() -> None:
spec = build_default_pipeline_spec(mode="worker")
assert spec.mode == "worker"
assert [item.stage_name for item in spec.stages] == ["router", "worker"]
def test_build_default_pipeline_spec_memory_has_single_stage() -> None:
spec = build_default_pipeline_spec(mode="memory")
assert spec.mode == "memory"
assert [item.stage_name for item in spec.stages] == ["memory"]
def test_build_default_pipeline_spec_rejects_unknown_mode() -> None:
with pytest.raises(ValueError, match="unsupported pipeline mode"):
build_default_pipeline_spec(mode="planner")
@@ -18,8 +18,8 @@ from schemas.agent.runtime_models import (
WorkerAgentOutputLite,
)
from schemas.agent.system_agent import AgentType
from schemas.automation import MessageContextConfig, RuntimeConfig
from schemas.user import UserContext, parse_profile_settings
from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.shared.user import UserContext, parse_profile_settings
def _run_input() -> RunAgentInput:
@@ -7,8 +7,8 @@ import pytest
import core.agentscope.runtime.tasks as tasks_module
from schemas.agent import ToolStatus
from schemas.automation import ContextWindowMode, MessageContextConfig
from schemas.user import UserContext, parse_profile_settings
from schemas.domain.automation import ContextWindowMode, MessageContextConfig
from schemas.shared.user import UserContext, parse_profile_settings
def _run_input_payload() -> dict[str, Any]:
@@ -1,250 +0,0 @@
from __future__ import annotations
import pytest
from core.agentscope.schemas.agui_input import (
MAX_MESSAGES,
MAX_RUN_ID_LENGTH,
MAX_RUN_INPUT_BYTES,
MAX_TEXT_CHARS,
parse_run_input,
validate_run_request_messages_contract,
)
def _base_payload() -> dict[str, object]:
return {
"threadId": "00000000-0000-0000-0000-000000000001",
"runId": "run-1",
"state": {},
"messages": [{"id": "u1", "role": "user", "content": "hello"}],
"tools": [],
"context": [],
"forwardedProps": {"agent_type": "worker"},
}
def test_parse_run_input_rejects_invalid_uuid() -> None:
payload = _base_payload()
payload["threadId"] = "bad-uuid"
with pytest.raises(ValueError, match="threadId must be a valid UUID"):
parse_run_input(payload)
def test_parse_run_input_rejects_message_count_over_limit() -> None:
payload = _base_payload()
payload["messages"] = [
{"id": f"u{i}", "role": "user", "content": "x"} for i in range(MAX_MESSAGES + 1)
]
with pytest.raises(ValueError, match="RunAgentInput.messages exceeds limit"):
parse_run_input(payload)
def test_parse_run_input_rejects_user_text_over_limit() -> None:
payload = _base_payload()
payload["messages"] = [
{"id": "u1", "role": "user", "content": "x" * (MAX_TEXT_CHARS + 1)}
]
with pytest.raises(
ValueError, match="RunAgentInput user message text exceeds limit"
):
parse_run_input(payload)
def test_parse_run_input_rejects_payload_over_limit() -> None:
payload = _base_payload()
payload["forwardedProps"] = {"blob": "x" * MAX_RUN_INPUT_BYTES}
with pytest.raises(ValueError, match="RunAgentInput payload exceeds size limit"):
parse_run_input(payload)
def test_parse_run_input_rejects_run_id_over_limit() -> None:
payload = _base_payload()
payload["runId"] = "r" * (MAX_RUN_ID_LENGTH + 1)
with pytest.raises(ValueError, match="runId exceeds length limit"):
parse_run_input(payload)
def test_validate_run_request_messages_contract_requires_single_user_message() -> None:
payload = _base_payload()
payload["messages"] = [
{"id": "u1", "role": "user", "content": "hello"},
{"id": "u2", "role": "user", "content": "again"},
]
run_input = parse_run_input(payload)
with pytest.raises(
ValueError,
match="RunAgentInput.messages must contain exactly one user message",
):
validate_run_request_messages_contract(run_input)
def test_validate_run_request_messages_contract_accepts_binary_url_blocks() -> None:
payload = _base_payload()
payload["messages"] = [
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "请分析"},
{
"type": "binary",
"mimeType": "image/png",
"url": "https://signed.example/a.png",
},
],
}
]
run_input = parse_run_input(payload)
validate_run_request_messages_contract(run_input)
def test_validate_run_request_messages_contract_rejects_binary_data_block() -> None:
payload = _base_payload()
payload["messages"] = [
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "请分析"},
{
"type": "binary",
"mimeType": "image/png",
"data": "aGVsbG8=",
},
],
}
]
run_input = parse_run_input(payload)
with pytest.raises(ValueError, match="binary content requires url"):
validate_run_request_messages_contract(run_input)
def test_parse_run_input_accepts_snake_case_aliases() -> None:
payload = {
"thread_id": "00000000-0000-0000-0000-000000000001",
"run_id": "run-1",
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": [
{"type": "text", "text": "hello"},
{
"type": "binary",
"mime_type": "image/png",
"url": "https://signed.example/a.png",
},
],
}
],
"tools": [],
"context": [],
"forwarded_props": {"agent_type": "worker"},
}
run_input = parse_run_input(payload)
assert run_input.thread_id == "00000000-0000-0000-0000-000000000001"
assert run_input.run_id == "run-1"
validate_run_request_messages_contract(run_input)
def test_parse_run_input_accepts_client_time_forwarded_props() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
},
}
run_input = parse_run_input(payload)
assert run_input.forwarded_props is not None
def test_parse_run_input_rejects_invalid_client_time_timezone() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "Mars/OlympusMons",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_invalid_client_time_now_iso() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16 09:12:33",
"client_epoch_ms": 1773658353000,
},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_invalid_client_time_epoch_type() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": "1773658353000",
},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_unknown_forwarded_props_key() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"agent_type": "worker",
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
},
"unexpected": {"foo": "bar"},
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
def test_parse_run_input_rejects_missing_forwarded_props_agent_type() -> None:
payload = _base_payload()
payload["forwardedProps"] = {
"client_time": {
"device_timezone": "America/Los_Angeles",
"client_now_iso": "2026-03-16T09:12:33-07:00",
"client_epoch_ms": 1773658353000,
}
}
with pytest.raises(ValueError, match="invalid RunAgentInput.forwardedProps"):
parse_run_input(payload)
@@ -40,6 +40,7 @@ class _FakeService:
start_at=datetime(2026, 3, 17, 9, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 3, 17, 9, 30, tzinfo=timezone.utc),
timezone="Asia/Shanghai",
status="active",
metadata=SimpleNamespace(
location=None, color="#4F46E5", reminder_minutes=15
),
@@ -247,7 +248,7 @@ async def test_calendar_read_returns_structured_result_with_ids(
assert "total=1" in payload["result"]
assert "timezone=Asia/Shanghai" in payload["result"]
assert "description=今天下午五点的会议" in payload["result"]
assert "status=" in payload["result"]
assert "status=active" in payload["result"]
assert fake_service.created_id in payload["result"]
assert fake_service.list_calls == [{"page": 1, "page_size": 20, "query": "会议"}]
@@ -1,132 +0,0 @@
from __future__ import annotations
import json
from typing import Any, AsyncGenerator
import pytest
from core.agentscope.tools.tool_config import ToolApprovalConfig, ToolConfig, ToolGroup
from core.agentscope.tools.tool_middleware import create_approval_middleware
async def _next_handler(**kwargs: Any) -> AsyncGenerator[dict[str, object], None]:
async def _generator() -> AsyncGenerator[dict[str, object], None]:
yield {"ok": True, "tool_call": kwargs.get("tool_call")}
return _generator()
def _extract_error_payload(chunk: object) -> dict[str, Any]:
content = getattr(chunk, "content", None)
if not isinstance(content, list) or not content:
return {}
first_block = content[0]
text = getattr(first_block, "text", None)
if not isinstance(text, str) and isinstance(first_block, dict):
raw_text = first_block.get("text")
text = raw_text if isinstance(raw_text, str) else None
if not isinstance(text, str):
return {}
return json.loads(text)
@pytest.mark.asyncio
async def test_hitl_middleware_default_write_does_not_require_approval() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=False),
)
}
)
responses = []
async for chunk in middleware(
{"tool_call": {"name": "calendar.write", "input": {"operation": "create"}}},
_next_handler,
):
responses.append(chunk)
assert responses[0]["ok"] is True
@pytest.mark.asyncio
async def test_hitl_middleware_pending_when_tool_requires_approval() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=True),
)
}
)
responses = []
async for chunk in middleware(
{"tool_call": {"name": "calendar_write", "input": {"operation": "create"}}},
_next_handler,
):
responses.append(chunk)
payload = _extract_error_payload(responses[0])
assert payload["error"]["code"] == "TOOL_PENDING_APPROVAL"
@pytest.mark.asyncio
async def test_hitl_middleware_passes_when_write_approved() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=True),
)
},
approval_resolver=lambda _name, _args, _config: "approved",
)
responses = []
async for chunk in middleware(
{
"tool_call": {
"name": "calendar.write",
"input": {
"operation": "create",
"_hitl": {"approval": "required"},
},
}
},
_next_handler,
):
responses.append(chunk)
assert responses[0]["ok"] is True
sanitized_input = responses[0]["tool_call"]["input"]
assert "_hitl" not in sanitized_input
@pytest.mark.asyncio
async def test_hitl_middleware_rejected_short_circuits() -> None:
middleware = create_approval_middleware(
config_by_name={
"calendar_write": ToolConfig(
name="calendar_write",
group=ToolGroup.EXECUTE,
approval=ToolApprovalConfig(required=True),
)
},
approval_resolver=lambda _name, _args, _config: "rejected",
)
responses = []
async for chunk in middleware(
{"tool_call": {"name": "calendar_write", "input": {"operation": "create"}}},
_next_handler,
):
responses.append(chunk)
payload = _extract_error_payload(responses[0])
assert payload["error"]["code"] == "TOOL_REJECTED"
@@ -9,7 +9,7 @@ from agentscope.tool import ToolResponse
from core.agentscope.tools.custom import memory as memory_module
from models.memories import MemoryType
from schemas.memories.memory_content import UserMemoryContent
from schemas.domain.memory_content import UserMemoryContent
def _decode_tool_response(response: ToolResponse) -> dict[str, object]:
@@ -9,7 +9,7 @@ from core.agentscope.prompts.system_prompt import (
)
from schemas.agent.forwarded_props import ClientTimeContext
from schemas.agent.system_agent import AgentType
from schemas.user.context import UserContext, parse_profile_settings
from schemas.shared.user import UserContext, parse_profile_settings
def _build_user_context(*, timezone_name: str = "Asia/Shanghai") -> UserContext:
@@ -159,7 +159,7 @@ def test_build_system_prompt_keeps_sections_focused_without_language_duplication
def test_build_system_prompt_includes_user_memory_section_for_router() -> None:
from schemas.memories.memory_content import UserMemoryContent
from schemas.domain.memory_content import UserMemoryContent
user_memory = UserMemoryContent()
@@ -175,7 +175,7 @@ def test_build_system_prompt_includes_user_memory_section_for_router() -> None:
def test_build_system_prompt_includes_work_memory_section_for_worker() -> None:
from schemas.memories.memory_content import WorkProfileContent
from schemas.domain.memory_content import WorkProfileContent
work_memory = WorkProfileContent()
@@ -6,7 +6,7 @@ from uuid import UUID, uuid4
import pytest
from models.automation_jobs import AutomationJob as OrmAutomationJob, ScheduleType
from schemas.automation import (
from schemas.domain.automation import (
RuntimeConfig,
ScheduleConfig,
ScheduleRunAt,
@@ -1,87 +0,0 @@
from __future__ import annotations
from datetime import datetime, timezone
from uuid import UUID, uuid4
import pytest
from sqlalchemy import Column, String, Table
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin
from core.db.base_repository import BaseRepository
class Widget(SoftDeleteMixin, Base):
__tablename__ = "widgets"
id: Mapped[UUID] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), nullable=False)
@pytest.fixture
async def db_engine():
auth_users = Table(
"users",
Base.metadata,
Column("id", String, primary_key=True),
schema="auth",
extend_existing=True,
)
engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
async with engine.begin() as conn:
await conn.exec_driver_sql("ATTACH DATABASE ':memory:' AS auth")
await conn.run_sync(Base.metadata.create_all)
yield engine
Base.metadata.remove(auth_users)
await engine.dispose()
@pytest.fixture
async def db_session(db_engine):
async_session = async_sessionmaker(
bind=db_engine,
class_=AsyncSession,
expire_on_commit=False,
)
async with async_session() as session:
yield session
await session.rollback()
@pytest.mark.asyncio
async def test_get_by_id_filters_soft_deleted(db_session: AsyncSession) -> None:
repository = BaseRepository(db_session, Widget)
widget_id = uuid4()
widget = Widget(id=widget_id, name="widget")
db_session.add(widget)
await db_session.commit()
found = await repository.get_by_id(widget_id)
assert found is not None
deleted = await repository.soft_delete_by_id(widget_id)
assert deleted is not None
assert deleted.deleted_at is not None
missing = await repository.get_by_id(widget_id)
assert missing is None
@pytest.mark.asyncio
async def test_soft_delete_sets_timestamp(db_session: AsyncSession) -> None:
repository = BaseRepository(db_session, Widget)
widget_id = uuid4()
widget = Widget(id=widget_id, name="widget")
db_session.add(widget)
await db_session.commit()
deleted = await repository.soft_delete_by_id(widget_id)
assert deleted is not None
assert isinstance(deleted.deleted_at, datetime)
deleted_at = deleted.deleted_at
if deleted_at.tzinfo is None:
deleted_at = deleted_at.replace(tzinfo=timezone.utc)
assert deleted_at <= datetime.now(timezone.utc)
@@ -1,134 +0,0 @@
from __future__ import annotations
from uuid import uuid4
import pytest
from sqlalchemy import Column, String, Table, select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from core.db.base import Base
from models.profile import Profile
@pytest.fixture
async def db_engine():
"""Create in-memory SQLite engine for testing."""
users_table = Table(
"users",
Base.metadata,
Column("id", String, primary_key=True),
schema="auth",
extend_existing=True,
)
engine = create_async_engine(
"sqlite+aiosqlite:///:memory:",
echo=False,
)
async with engine.begin() as conn:
await conn.exec_driver_sql("ATTACH DATABASE ':memory:' AS auth")
await conn.run_sync(Base.metadata.create_all)
yield engine
Base.metadata.remove(users_table)
await engine.dispose()
@pytest.fixture
async def db_session(db_engine):
"""Create a database session for testing."""
async_session = async_sessionmaker(
bind=db_engine,
class_=AsyncSession,
expire_on_commit=False,
)
async with async_session() as session:
yield session
await session.rollback()
@pytest.mark.asyncio
async def test_profile_model_create(db_session: AsyncSession) -> None:
"""Test creating a Profile model."""
profile_id = uuid4()
profile = Profile(
id=profile_id,
username="testuser",
)
db_session.add(profile)
await db_session.commit()
await db_session.refresh(profile)
assert profile.id == profile_id
assert profile.username == "testuser"
assert profile.created_at is not None
assert profile.updated_at is not None
assert profile.deleted_at is None
@pytest.mark.asyncio
async def test_profile_model_get_by_id(db_session: AsyncSession) -> None:
"""Test retrieving a Profile by ID."""
profile_id = uuid4()
profile = Profile(
id=profile_id,
username="testuser",
)
db_session.add(profile)
await db_session.commit()
result = await db_session.get(Profile, profile_id)
assert result is not None
assert result.username == "testuser"
@pytest.mark.asyncio
async def test_profile_model_get_by_username(db_session: AsyncSession) -> None:
"""Test retrieving a Profile by username."""
profile = Profile(
id=uuid4(),
username="testuser",
)
db_session.add(profile)
await db_session.commit()
result = await db_session.execute(
select(Profile).where(Profile.username == "testuser")
)
found = result.scalar_one()
assert found is not None
assert found.username == "testuser"
@pytest.mark.asyncio
async def test_profile_model_update(db_session: AsyncSession) -> None:
"""Test updating a Profile."""
profile = Profile(
id=uuid4(),
username="testuser",
bio="Old bio",
)
db_session.add(profile)
await db_session.commit()
profile.bio = "New bio"
await db_session.commit()
await db_session.refresh(profile)
assert profile.bio == "New bio"
@pytest.mark.asyncio
async def test_profile_model_allows_duplicate_usernames(
db_session: AsyncSession,
) -> None:
first = Profile(id=uuid4(), username="same_name")
second = Profile(id=uuid4(), username="same_name")
db_session.add(first)
db_session.add(second)
await db_session.commit()
result = await db_session.execute(
select(Profile).where(Profile.username == "same_name")
)
found = result.scalars().all()
assert len(found) == 2
@@ -1,32 +0,0 @@
from __future__ import annotations
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[4]
APP_SCRIPT = ROOT_DIR / "infra" / "scripts" / "app.sh"
def test_worker_commands_use_taskiq() -> None:
content = APP_SCRIPT.read_text(encoding="utf-8")
removed_runner = "uv run celery"
assert "uv run taskiq worker" in content
assert "core.taskiq.app:critical_broker" in content
assert "core.taskiq.app:default_broker" in content
assert "core.taskiq.app:bulk_broker" in content
assert 'pgrep -f "uv run taskiq worker core.taskiq.app:"' in content
assert 'kill_pids_gracefully "taskiq workers"' in content
assert "gunicorn" not in content
assert removed_runner not in content
def test_web_command_uses_uvicorn_only() -> None:
content = APP_SCRIPT.read_text(encoding="utf-8")
assert "uv run uvicorn app:app" in content
assert 'WEB_PORT="${SOCIAL_WEB__PORT:-5775}"' in content
assert "SOCIAL_WEB__WORKERS" in content
assert 'UVICORN_LOG_LEVEL="${UVICORN_LOG_LEVEL,,}"' in content
assert "SOCIAL_WEB__GUNICORN__" not in content
assert "uv run gunicorn" not in content

Some files were not shown because too many files have changed in this diff Show More