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"?> <?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"> <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 --> <!-- You can insert your own image assets here -->
<!-- <item> <!-- <item>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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"> <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 --> <!-- You can insert your own image assets here -->
<!-- <item> <!-- <item>
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#FFFFFF</color> <color name="ic_launcher_background">#FFFFFF</color>
<color name="launchBackground">#EFF8FF</color>
</resources> </resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -1,23 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
class AuthBootScreen extends StatelessWidget { class AuthBootScreen extends StatelessWidget {
const AuthBootScreen({super.key}); const AuthBootScreen({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Scaffold( return Scaffold(
backgroundColor: AppColors.authBackgroundTop, backgroundColor: const Color(0xFFEFF8FF),
body: SafeArea( body: SafeArea(
child: Center( child: Center(
child: AppLoadingIndicator( child: Image.asset(
variant: AppLoadingVariant.surface, 'assets/branding/assistant_octopus_foreground.png',
size: 28, width: 260,
strokeWidth: 2.5,
color: AppColors.authPrimaryButton,
trackColor: AppColors.authPrimaryButtonDisabled,
), ),
), ),
), ),
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
enum ScheduleSourceType { manual, imported, agentGenerated } enum ScheduleSourceType { manual, imported, agentGenerated }
enum ScheduleStatus { active, completed, canceled, archived } enum ScheduleStatus { active, archived }
class ScheduleItemModel { class ScheduleItemModel {
final String id; final String id;
@@ -279,9 +279,7 @@ ScheduleSourceType _sourceTypeFromApi(String? raw) {
ScheduleStatus _statusFromApi(String? raw) { ScheduleStatus _statusFromApi(String? raw) {
switch (raw) { switch (raw) {
case 'completed': case 'completed':
return ScheduleStatus.completed;
case 'canceled': case 'canceled':
return ScheduleStatus.canceled;
case 'archived': case 'archived':
return ScheduleStatus.archived; return ScheduleStatus.archived;
case 'active': case 'active':
@@ -294,10 +292,6 @@ String _statusToApi(ScheduleStatus status) {
switch (status) { switch (status) {
case ScheduleStatus.active: case ScheduleStatus.active:
return 'active'; return 'active';
case ScheduleStatus.completed:
return 'completed';
case ScheduleStatus.canceled:
return 'canceled';
case ScheduleStatus.archived: case ScheduleStatus.archived:
return 'archived'; return 'archived';
} }
+1
View File
@@ -41,6 +41,7 @@ flutter:
uses-material-design: true uses-material-design: true
assets: assets:
- assets/images/ - assets/images/
- assets/branding/
flutter_launcher_icons: flutter_launcher_icons:
android: true 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. - Use service-role DB access only in backend.
- Soft delete uses `deleted_at`; reads must exclude deleted records by default. - Soft delete uses `deleted_at`; reads must exclude deleted records by default.
- Alembic is the only schema migration source of truth. - 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 ## 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 import func, select
from sqlalchemy.ext.asyncio import AsyncSession 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, AgentChatSessionStatus from models.agent_chat_session import AgentChatSession
from schemas.enums import AgentChatMessageRole, AgentChatSessionStatus
class MessageRepository: class MessageRepository:
+2 -3
View File
@@ -6,12 +6,11 @@ from uuid import UUID
from core.agentscope.events.persistence import MessageRepository, SessionRepository from core.agentscope.events.persistence import MessageRepository, SessionRepository
from core.logging import get_logger from core.logging import get_logger
from models.agent_chat_message import AgentChatMessageRole from schemas.enums import AgentChatMessageRole, AgentChatSessionStatus
from models.agent_chat_session import AgentChatSessionStatus
from schemas.agent.system_agent import AgentType from schemas.agent.system_agent import AgentType
from schemas.agent.runtime_models import AgentOutput, RouterAgentOutput, ToolAgentOutput from schemas.agent.runtime_models import AgentOutput, RouterAgentOutput, ToolAgentOutput
from schemas.agent.visibility import SystemVisibilityBit, bit_mask 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): class EventStore(Protocol):
@@ -9,7 +9,7 @@ from uuid import UUID
import redis.asyncio as redis import redis.asyncio as redis
from core.config.settings import config from core.config.settings import config
from core.logging import get_logger from core.logging import get_logger
from schemas.user import ( from schemas.shared.user import (
UserContext, UserContext,
parse_profile_settings, parse_profile_settings,
) )
@@ -2,7 +2,7 @@ from __future__ import annotations
import json 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: 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 core.agentscope.prompts.tool_prompt import build_tools_prompt
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
from schemas.agent.forwarded_props import ClientTimeContext from schemas.agent.forwarded_props import ClientTimeContext
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user.context import UserContext from schemas.shared.user import UserContext
def _wrap_section(section: str, content: str) -> str: 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 agentscope.message import Msg
from core.agentscope.runtime.runner import AgentScopeRunner from core.agentscope.runtime.runner import AgentScopeRunner
from core.logging import get_logger from core.logging import get_logger
from schemas.automation import RuntimeConfig from schemas.domain.automation import RuntimeConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user import UserContext from schemas.shared.user import UserContext
logger = get_logger("core.agentscope.runtime.orchestrator") logger = get_logger("core.agentscope.runtime.orchestrator")
@@ -40,9 +40,9 @@ from schemas.agent.system_agent import (
AgentType, AgentType,
SystemAgentLLMConfig, SystemAgentLLMConfig,
) )
from schemas.automation import RuntimeConfig from schemas.domain.automation import RuntimeConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user import UserContext from schemas.shared.user import UserContext
from services.litellm.service import LiteLLMService from services.litellm.service import LiteLLMService
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession 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.db.session import AsyncSessionLocal
from core.logging import get_logger from core.logging import get_logger
from core.taskiq.app import worker_agent_broker, worker_automation_broker from core.taskiq.app import worker_agent_broker, worker_automation_broker
from schemas.automation import MessageContextConfig, RuntimeConfig from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.messages.chat_message import ( from schemas.domain.chat_message import (
AgentChatMessageMetadata, AgentChatMessageMetadata,
extract_user_message_attachments, 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.redis import get_or_init_redis_client
from services.base.supabase import supabase_service from services.base.supabase import supabase_service
from v1.agent.repository import AgentRepository 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.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 _DEFAULT_CONTEXT_WINDOW_USER_MESSAGES = 20
@@ -84,7 +84,7 @@ class CalendarWriteOperation(BaseModel):
le=10080, le=10080,
description="Reminder minutes before event start.", description="Reminder minutes before event start.",
) )
status: Literal["active", "completed", "canceled", "archived"] | None = Field( status: Literal["active", "archived"] | None = Field(
default=None, default=None,
description="Optional status for update action.", description="Optional status for update action.",
) )
@@ -163,6 +163,10 @@ async def calendar_read(
) -> ToolResponse: ) -> ToolResponse:
"""Read calendar events with optional keyword filtering and pagination. """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: Args:
query: Optional keyword used to filter events by text fields. query: Optional keyword used to filter events by text fields.
page: Page number starting from 1. page: Page number starting from 1.
@@ -15,9 +15,9 @@ from core.agentscope.tools.utils.tool_response_builder import (
build_error_output, build_error_output,
build_tool_response, build_tool_response,
) )
from models.memories import MemoryType from schemas.enums import MemoryType
from schemas.agent.runtime_models import ErrorInfo, ToolAgentOutput, ToolStatus 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): class MemoryWriteArgs(BaseModel):
@@ -46,6 +46,9 @@ def create_schedule_service(
def schedule_event_to_dict(event: object) -> dict[str, Any]: def schedule_event_to_dict(event: object) -> dict[str, Any]:
event_id = str(getattr(event, "id")) event_id = str(getattr(event, "id"))
metadata = getattr(event, "metadata", None) 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) location_value = getattr(metadata, "location", None)
color_value = getattr(metadata, "color", None) or "#4F46E5" color_value = getattr(metadata, "color", None) or "#4F46E5"
reminder_minutes_value = getattr(metadata, "reminder_minutes", None) 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 if getattr(event, "end_at") is not None
else None, else None,
"timezone": getattr(event, "timezone"), "timezone": getattr(event, "timezone"),
"status": status_value,
"location": location_value, "location": location_value,
"color": color_value, "color": color_value,
"reminderMinutes": reminder_minutes_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.config.settings import config
from core.logging import get_logger from core.logging import get_logger
from schemas.automation import RuntimeConfig from schemas.domain.automation import RuntimeConfig
logger = get_logger("core.automation.scheduler") 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.group_members import GroupMember
from models.groups import Group from models.groups import Group
from models.inbox_messages import InboxMessage 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 import Llm
from models.llm_factory import LlmFactory from models.llm_factory import LlmFactory
from models.memories import Memory from models.memories import Memory
@@ -16,6 +16,7 @@ from models.schedule_subscriptions import ScheduleSubscription
from models.system_agents import SystemAgents from models.system_agents import SystemAgents
from models.todos import Todo from models.todos import Todo
from models.todo_sources import TodoSource from models.todo_sources import TodoSource
from schemas.enums import InviteCodeStatus
__all__ = [ __all__ = [
"AgentChatMessage", "AgentChatMessage",
+2 -7
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
from decimal import Decimal from decimal import Decimal
import uuid import uuid
from enum import Enum
from sqlalchemy import ( from sqlalchemy import (
BigInteger, BigInteger,
@@ -19,13 +18,9 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AgentChatMessageRole
__all__ = ["AgentChatMessage", "AgentChatMessageRole"]
class AgentChatMessageRole(str, Enum):
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
TOOL = "tool"
class AgentChatMessage(TimestampMixin, SoftDeleteMixin, Base): class AgentChatMessage(TimestampMixin, SoftDeleteMixin, Base):
+2 -12
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
import uuid import uuid
from enum import Enum
from sqlalchemy import ( from sqlalchemy import (
DateTime, DateTime,
@@ -19,18 +18,9 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AgentChatSessionStatus, SessionType
__all__ = ["AgentChatSession", "AgentChatSessionStatus", "SessionType"]
class AgentChatSessionStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class SessionType(str, Enum):
CHAT = "chat"
AUTOMATION = "automation"
class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base): class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base):
+2 -10
View File
@@ -2,23 +2,15 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, JSON, String from sqlalchemy import DateTime, JSON, String
from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AutomationJobStatus, ScheduleType
__all__ = ["AutomationJob", "AutomationJobStatus", "ScheduleType"]
class AutomationJobStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class ScheduleType(str, Enum):
DAILY = "daily"
WEEKLY = "weekly"
class AutomationJob(TimestampMixin, SoftDeleteMixin, Base): class AutomationJob(TimestampMixin, SoftDeleteMixin, Base):
+2 -8
View File
@@ -2,21 +2,15 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, String from sqlalchemy import DateTime, String
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import FriendshipStatus
__all__ = ["Friendship", "FriendshipStatus"]
class FriendshipStatus(str, Enum):
PENDING = "pending"
ACCEPTED = "accepted"
BLOCKED = "blocked"
DECLINED = "declined"
CANCELED = "canceled"
class Friendship(TimestampMixin, SoftDeleteMixin, Base): class Friendship(TimestampMixin, SoftDeleteMixin, Base):
+7 -17
View File
@@ -1,30 +1,20 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from enum import Enum
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import GroupMemberRole, GroupMemberSource, GroupMemberStatus
__all__ = [
class GroupMemberRole(str, Enum): "GroupMember",
OWNER = "owner" "GroupMemberRole",
ADMIN = "admin" "GroupMemberSource",
MEMBER = "member" "GroupMemberStatus",
]
class GroupMemberSource(str, Enum):
INVITED = "invited"
JOINED = "joined"
class GroupMemberStatus(str, Enum):
ACTIVE = "active"
MUTED = "muted"
REMOVED = "removed"
class GroupMember(TimestampMixin, SoftDeleteMixin, Base): class GroupMember(TimestampMixin, SoftDeleteMixin, Base):
+2 -5
View File
@@ -1,18 +1,15 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from enum import Enum
from sqlalchemy import String, Text from sqlalchemy import String, Text
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import GroupStatus
__all__ = ["Group", "GroupStatus"]
class GroupStatus(str, Enum):
ACTIVE = "active"
ARCHIVED = "archived"
class Group(TimestampMixin, SoftDeleteMixin, Base): class Group(TimestampMixin, SoftDeleteMixin, Base):
+2 -14
View File
@@ -1,27 +1,15 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from enum import Enum
from sqlalchemy import Boolean, String from sqlalchemy import Boolean, String
from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin from core.db.base import Base, TimestampMixin
from schemas.enums import InboxMessageStatus, InboxMessageType
__all__ = ["InboxMessage", "InboxMessageType", "InboxMessageStatus"]
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 InboxMessage(TimestampMixin, Base): class InboxMessage(TimestampMixin, Base):
+2 -6
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Integer, String from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import UUID 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.base import Base, TimestampMixin
from core.db.types import json_jsonb from core.db.types import json_jsonb
from schemas.enums import InviteCodeStatus
__all__ = ["InviteCode", "InviteCodeStatus"]
class InviteCodeStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
EXPIRED = "expired"
class InviteCode(TimestampMixin, Base): class InviteCode(TimestampMixin, Base):
+2 -10
View File
@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from enum import Enum
from sqlalchemy import String from sqlalchemy import String
from sqlalchemy.dialects.postgresql import UUID 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.base import Base, TimestampMixin
from core.db.types import json_jsonb from core.db.types import json_jsonb
from schemas.enums import MemoryStatus, MemoryType
__all__ = ["Memory", "MemoryType", "MemoryStatus"]
class MemoryType(str, Enum):
USER = "user"
WORK = "work"
class MemoryStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class Memory(TimestampMixin, Base): class Memory(TimestampMixin, Base):
+2 -13
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, String, Text from sqlalchemy import DateTime, String, Text
from sqlalchemy.dialects.postgresql import UUID 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.base import Base, SoftDeleteMixin, TimestampMixin
from core.db.types import json_jsonb from core.db.types import json_jsonb
from schemas.enums import ScheduleItemSourceType, ScheduleItemStatus
__all__ = ["ScheduleItem", "ScheduleItemStatus", "ScheduleItemSourceType"]
class ScheduleItemStatus(str, Enum):
ACTIVE = "active"
COMPLETED = "completed"
CANCELED = "canceled"
ARCHIVED = "archived"
class ScheduleItemSourceType(str, Enum):
MANUAL = "manual"
IMPORTED = "imported"
AGENT_GENERATED = "agent_generated"
class ScheduleItem(TimestampMixin, SoftDeleteMixin, Base): class ScheduleItem(TimestampMixin, SoftDeleteMixin, Base):
+7 -20
View File
@@ -1,33 +1,20 @@
from __future__ import annotations from __future__ import annotations
import uuid import uuid
from enum import Enum
from sqlalchemy import Integer, String from sqlalchemy import Integer, String
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin from core.db.base import Base, TimestampMixin
from schemas.enums import NotifyLevel, SubscriptionPermission, SubscriptionStatus
__all__ = [
class SubscriptionStatus(str, Enum): "ScheduleSubscription",
ACTIVE = "active" "SubscriptionStatus",
PENDING = "pending" "NotifyLevel",
PAUSED = "paused" "SubscriptionPermission",
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
class ScheduleSubscription(TimestampMixin, Base): class ScheduleSubscription(TimestampMixin, Base):
+2 -13
View File
@@ -2,26 +2,15 @@ from __future__ import annotations
import uuid import uuid
from datetime import datetime from datetime import datetime
from enum import Enum
from sqlalchemy import DateTime, Integer, String from sqlalchemy import DateTime, Integer, String
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import TodoPriority, TodoStatus
__all__ = ["Todo", "TodoStatus", "TodoPriority"]
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 Todo(TimestampMixin, SoftDeleteMixin, Base): class Todo(TimestampMixin, SoftDeleteMixin, Base):
+1 -45
View File
@@ -1,45 +1 @@
"""Centralized shared schemas for cross-module contracts.""" """Backend reusable schemas package."""
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",
]
+1 -1
View File
@@ -12,7 +12,7 @@ Version: 2.0
from __future__ import annotations from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Any, Literal, NotRequired, TypedDict, Union from typing import Any, Literal, TypedDict, Union
# ============================================================ # ============================================================
# Enums # 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 datetime import datetime
from enum import Enum from enum import Enum
from typing import Protocol
from uuid import UUID from uuid import UUID
from core.agentscope.tools.tool_config import AgentTool from core.agentscope.tools.tool_config import AgentTool
from pydantic import BaseModel, ConfigDict, Field, model_validator 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): class ContextSource(str, Enum):
@@ -50,8 +64,7 @@ class ScheduleConfig(BaseModel):
invalid = [day for day in self.weekdays if day < 1 or day > 7] invalid = [day for day in self.weekdays if day < 1 or day > 7]
if invalid: if invalid:
raise ValueError("weekdays must be within 1-7") raise ValueError("weekdays must be within 1-7")
deduped = sorted(set(self.weekdays)) self.weekdays = sorted(set(self.weekdays))
self.weekdays = deduped
else: else:
self.weekdays = None self.weekdays = None
return self return self
@@ -90,20 +103,20 @@ class AutomationJob(BaseModel):
updated_at: datetime updated_at: datetime
@classmethod @classmethod
def from_orm(cls, obj: OrmAutomationJob) -> "AutomationJob": def from_orm(cls, obj: object) -> "AutomationJob":
return cls( return cls(
id=obj.id, id=getattr(obj, "id"),
owner_id=obj.owner_id, owner_id=getattr(obj, "owner_id"),
bootstrap_key=obj.bootstrap_key, bootstrap_key=getattr(obj, "bootstrap_key"),
title=obj.title, title=getattr(obj, "title"),
config=AutomationJobConfig.model_validate(obj.config or {}), config=AutomationJobConfig.model_validate(getattr(obj, "config", {}) or {}),
next_run_at=obj.next_run_at, next_run_at=getattr(obj, "next_run_at"),
timezone=obj.timezone, timezone=getattr(obj, "timezone"),
last_run_at=obj.last_run_at, last_run_at=getattr(obj, "last_run_at"),
status=obj.status, status=getattr(obj, "status"),
created_by=obj.created_by, created_by=getattr(obj, "created_by"),
created_at=obj.created_at, created_at=getattr(obj, "created_at"),
updated_at=obj.updated_at, updated_at=getattr(obj, "updated_at"),
) )
@property @property
@@ -1,24 +1,22 @@
from __future__ import annotations from __future__ import annotations
import json import json
from enum import Enum
from typing import ClassVar, Literal, Union from typing import ClassVar, Literal, Union
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import InboxMessageStatus, InboxMessageType
__all__ = [
class InboxMessageType(str, Enum): "InboxMessageType",
FRIEND_REQUEST = "friend_request" "InboxMessageStatus",
CALENDAR = "calendar" "CalendarInviteContent",
SYSTEM = "system" "CalendarUpdateContent",
GROUP = "group" "CalendarDeleteContent",
"FriendshipContent",
"CalendarContent",
class InboxMessageStatus(str, Enum): "InboxMessageContent",
PENDING = "pending" "parse_calendar_content",
ACCEPTED = "accepted" ]
REJECTED = "rejected"
DISMISSED = "dismissed"
class CalendarInviteContent(BaseModel): class CalendarInviteContent(BaseModel):
@@ -1,13 +1,11 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from enum import Enum
from typing import ClassVar, Literal from typing import ClassVar, Literal
from uuid import UUID from uuid import UUID
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from schemas.domain.memory_content import (
from schemas.memories.memory_content import (
TeamMember, TeamMember,
UserMemoryContent, UserMemoryContent,
UserPreferences, UserPreferences,
@@ -15,16 +13,7 @@ from schemas.memories.memory_content import (
WorkProfileContent, WorkProfileContent,
WorkProject, WorkProject,
) )
from schemas.enums import MemoryStatus, MemoryType
class MemoryType(str, Enum):
USER = "user"
WORK = "work"
class MemoryStatus(str, Enum):
ACTIVE = "active"
DISABLED = "disabled"
class MemoryModel(BaseModel): class MemoryModel(BaseModel):
@@ -5,6 +5,15 @@ from typing import ClassVar, Literal
from uuid import UUID from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import ScheduleItemSourceType, ScheduleItemStatus
__all__ = [
"AttachmentType",
"ScheduleItemMetadataAttachment",
"ScheduleItemMetadata",
"ScheduleItemSourceType",
"ScheduleItemStatus",
]
class AttachmentType(str, Enum): class AttachmentType(str, Enum):
@@ -32,16 +41,3 @@ class ScheduleItemMetadata(BaseModel):
attachments: list[ScheduleItemMetadataAttachment] = Field(default_factory=list) attachments: list[ScheduleItemMetadataAttachment] = Field(default_factory=list)
reminder_minutes: int | None = Field(default=None, ge=0, le=10080) reminder_minutes: int | None = Field(default=None, ge=0, le=10080)
version: Literal[1] = 1 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 import Select, select
from sqlalchemy.ext.asyncio import AsyncSession 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.agent_chat_session import AgentChatSession
from models.system_agents import SystemAgents 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, AgentChatMessage as AgentChatMessageSchema,
AgentChatMessageMetadata, AgentChatMessageMetadata,
) )
+3 -3
View File
@@ -18,8 +18,8 @@ from schemas.agent.forwarded_props import (
RuntimeMode, RuntimeMode,
) )
from schemas.agent.visibility import SystemVisibilityBit, bit_mask from schemas.agent.visibility import SystemVisibilityBit, bit_mask
from schemas.automation import RuntimeConfig from schemas.domain.automation import RuntimeConfig
from schemas.messages.chat_message import ( from schemas.domain.chat_message import (
AgentChatMessageMetadata, AgentChatMessageMetadata,
UserMessageAttachment, UserMessageAttachment,
extract_user_message_attachments, extract_user_message_attachments,
@@ -362,7 +362,7 @@ class AgentService:
before: date | None, before: date | None,
current_user: CurrentUser, current_user: CurrentUser,
) -> HistorySnapshotResponse: ) -> 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.utils import convert_message_to_history
from v1.agent.schemas import HistoryMessage 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 core.agentscope.tools.tool_config import AgentTool
from schemas.agent.system_agent import SystemAgentLLMConfig from schemas.agent.system_agent import SystemAgentLLMConfig
from schemas.automation import ( from schemas.domain.automation import (
ContextSource, ContextSource,
ContextWindowMode, ContextWindowMode,
MessageContextConfig, MessageContextConfig,
+1 -1
View File
@@ -8,7 +8,7 @@ from collections.abc import Callable
from typing import Any from typing import Any
from core.agentscope.runtime.ui_compiler import compile as compile_ui_hints 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, AgentChatMessage,
AgentChatMessageMetadata, AgentChatMessageMetadata,
extract_user_message_attachments, extract_user_message_attachments,
@@ -7,7 +7,7 @@ from typing import Any
import yaml 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}$") _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 sqlalchemy.ext.asyncio import AsyncSession
from core.logging import get_logger from core.logging import get_logger
from models.automation_jobs import AutomationJob, AutomationJobStatus, ScheduleType from models.automation_jobs import AutomationJob
from models.memories import MemoryType from schemas.enums import AutomationJobStatus, MemoryType, ScheduleType
from models.profile import Profile from models.profile import Profile
from schemas.automation import AutomationJobConfig, ScheduleConfig from schemas.domain.automation import AutomationJobConfig, ScheduleConfig
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from schemas.user.context import parse_profile_settings from schemas.shared.user import parse_profile_settings
from v1.auth.automation_static_config import load_static_automation_job_config from v1.auth.automation_static_config import load_static_automation_job_config
from v1.auth.schemas import RegistrationBootstrapRequest from v1.auth.schemas import RegistrationBootstrapRequest
from v1.memories.repository import SQLAlchemyMemoriesRepository 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 zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from core.db.base_repository import BaseRepository from core.db.base_repository import BaseRepository
from models.agent_chat_session import AgentChatSession, SessionType from models.agent_chat_session import AgentChatSession
from models.automation_jobs import AutomationJob, AutomationJobStatus, ScheduleType from models.automation_jobs import AutomationJob
from schemas.automation import AutomationJobConfig, ScheduleConfig from schemas.enums import AutomationJobStatus, ScheduleType, SessionType
from schemas.domain.automation import AutomationJobConfig, ScheduleConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from v1.automation_jobs.schemas import ( 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 pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from models.automation_jobs import AutomationJob as OrmAutomationJob from schemas.domain.automation import AutomationJobConfig
from models.automation_jobs import AutomationJobStatus from schemas.enums import AutomationJobStatus
from schemas.automation import AutomationJobConfig
class AutomationJobResponse(BaseModel): class AutomationJobResponse(BaseModel):
@@ -29,20 +28,20 @@ class AutomationJobResponse(BaseModel):
updated_at: datetime updated_at: datetime
@classmethod @classmethod
def from_orm(cls, obj: OrmAutomationJob) -> Self: def from_orm(cls, obj: object) -> Self:
return cls( return cls(
id=obj.id, id=getattr(obj, "id"),
owner_id=obj.owner_id, owner_id=getattr(obj, "owner_id"),
bootstrap_key=obj.bootstrap_key, bootstrap_key=getattr(obj, "bootstrap_key"),
title=obj.title, title=getattr(obj, "title"),
timezone=obj.timezone, timezone=getattr(obj, "timezone"),
status=obj.status, status=getattr(obj, "status"),
is_system=obj.bootstrap_key is not None, is_system=getattr(obj, "bootstrap_key") is not None,
config=AutomationJobConfig.model_validate(obj.config or {}), config=AutomationJobConfig.model_validate(getattr(obj, "config", {}) or {}),
next_run_at=obj.next_run_at, next_run_at=getattr(obj, "next_run_at"),
last_run_at=obj.last_run_at, last_run_at=getattr(obj, "last_run_at"),
created_at=obj.created_at, created_at=getattr(obj, "created_at"),
updated_at=obj.updated_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 zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from fastapi import HTTPException, status from fastapi import HTTPException, status
from models.automation_jobs import ScheduleType from schemas.enums import ScheduleType
from schemas.automation import ( from schemas.domain.automation import (
AutomationJob as AutomationJobSchema, AutomationJob as AutomationJobSchema,
MessageContextConfig, MessageContextConfig,
RuntimeConfig, RuntimeConfig,
+4 -3
View File
@@ -9,9 +9,10 @@ from sqlalchemy.exc import SQLAlchemyError
from core.db.base_repository import BaseRepository from core.db.base_repository import BaseRepository
from core.logging import get_logger from core.logging import get_logger
from models.friendships import Friendship, FriendshipStatus from models.friendships import Friendship
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType from models.inbox_messages import InboxMessage
from schemas.inbox.messages import FriendshipContent from schemas.enums import FriendshipStatus, InboxMessageStatus, InboxMessageType
from schemas.domain.inbox import FriendshipContent
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession 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 pydantic import BaseModel, ConfigDict, Field
from schemas.user.context import UserContext from schemas.shared.user import UserContext
class FriendRequestCreate(BaseModel): class FriendRequestCreate(BaseModel):
+5 -4
View File
@@ -10,8 +10,9 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser from core.auth.models import CurrentUser
from core.db.base_service import BaseService from core.db.base_service import BaseService
from core.logging import get_logger from core.logging import get_logger
from models.friendships import Friendship, FriendshipStatus from models.friendships import Friendship
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType from models.inbox_messages import InboxMessage
from schemas.enums import FriendshipStatus, InboxMessageStatus, InboxMessageType
from v1.friendships.repository import FriendshipRepository from v1.friendships.repository import FriendshipRepository
from v1.friendships.schemas import ( from v1.friendships.schemas import (
FriendRequestCreate, FriendRequestCreate,
@@ -22,7 +23,7 @@ from v1.users.repository import UserRepository
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from schemas.user.context import UserContext from schemas.shared.user import UserContext
logger = get_logger("v1.friendships.service") logger = get_logger("v1.friendships.service")
@@ -593,7 +594,7 @@ class FriendshipService(BaseService):
) )
def _build_user_basic_info(self, profile: Any) -> "UserContext": 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: if profile is None:
return UserContext(id="", username="") return UserContext(id="", username="")
+2 -1
View File
@@ -7,7 +7,8 @@ from sqlalchemy import select, update
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from core.logging import get_logger 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: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
+1 -1
View File
@@ -6,7 +6,7 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from schemas.inbox.messages import InboxMessageStatus, InboxMessageType from schemas.domain.inbox import InboxMessageStatus, InboxMessageType
class InboxMessageResponse(BaseModel): 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.db.base_repository import BaseRepository
from core.logging import get_logger 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: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession 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 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.dependencies import get_memories_service
from v1.memories.schemas import ( from v1.memories.schemas import (
MemoryListResponse, MemoryListResponse,
+1 -1
View File
@@ -4,7 +4,7 @@ from typing import ClassVar
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
class UserMemoryUpdate(BaseModel): class UserMemoryUpdate(BaseModel):
+3 -2
View File
@@ -8,8 +8,9 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser from core.auth.models import CurrentUser
from core.db.base_service import BaseService from core.db.base_service import BaseService
from core.logging import get_logger from core.logging import get_logger
from models.memories import Memory, MemoryType from models.memories import Memory
from schemas.memories.memory_content import UserMemoryContent, WorkProfileContent from schemas.enums import MemoryType
from schemas.domain.memory_content import UserMemoryContent, WorkProfileContent
from v1.memories.repository import MemoriesRepositoryLike from v1.memories.repository import MemoriesRepositoryLike
if TYPE_CHECKING: 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.db.base_repository import BaseRepository
from core.logging import get_logger from core.logging import get_logger
from models.schedule_items import ScheduleItem, ScheduleItemStatus from models.schedule_items import ScheduleItem
from models.schedule_subscriptions import ScheduleSubscription, SubscriptionStatus from models.schedule_subscriptions import ScheduleSubscription
from schemas.enums import ScheduleItemStatus, SubscriptionStatus
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession 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 pydantic import BaseModel, ConfigDict, Field, field_validator
from schemas.inbox.messages import ( from schemas.domain.inbox import (
CalendarContent, CalendarContent,
CalendarDeleteContent, CalendarDeleteContent,
CalendarInviteContent, CalendarInviteContent,
CalendarUpdateContent, CalendarUpdateContent,
parse_calendar_content, parse_calendar_content,
) )
from schemas.schedule.items import ( from schemas.domain.schedule import (
AttachmentType, AttachmentType,
ScheduleItemMetadata, ScheduleItemMetadata,
ScheduleItemMetadataAttachment, ScheduleItemMetadataAttachment,
+12 -3
View File
@@ -10,9 +10,14 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser from core.auth.models import CurrentUser
from core.db.base_service import BaseService from core.db.base_service import BaseService
from core.logging import get_logger 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_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.auth.gateway import SupabaseAuthGateway
from v1.inbox_messages.repository import InboxMessageRepository from v1.inbox_messages.repository import InboxMessageRepository
from v1.schedule_items.repository import ScheduleItemRepository from v1.schedule_items.repository import ScheduleItemRepository
@@ -35,6 +40,8 @@ if TYPE_CHECKING:
logger = get_logger("v1.schedule_items.service") logger = get_logger("v1.schedule_items.service")
_LEGACY_ARCHIVED_STATUSES = {"completed", "canceled"}
class AuthByPhoneGateway(Protocol): class AuthByPhoneGateway(Protocol):
async def get_user_by_phone(self, phone: str) -> "UserByPhoneResponse": ... async def get_user_by_phone(self, phone: str) -> "UserByPhoneResponse": ...
@@ -417,8 +424,10 @@ class ScheduleItemService(BaseService):
permission: int = 1, permission: int = 1,
) -> ScheduleItemResponse: ) -> ScheduleItemResponse:
status_value = ( 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 = ( source_type_value = (
item.source_type.value item.source_type.value
if hasattr(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.db.base_repository import BaseRepository
from core.logging import get_logger from core.logging import get_logger
from models.todo_sources import TodoSource 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: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession 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 pydantic import BaseModel, ConfigDict, Field
from schemas.todo import TodoOrder from schemas.domain.todo import TodoOrder
class TodoCreate(BaseModel): class TodoCreate(BaseModel):
+2 -1
View File
@@ -10,7 +10,8 @@ from sqlalchemy.exc import SQLAlchemyError
from core.auth.models import CurrentUser from core.auth.models import CurrentUser
from core.db.base_service import BaseService from core.db.base_service import BaseService
from core.logging import get_logger 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.schedule_items.repository import SQLAlchemyScheduleItemRepository
from v1.todo.repository import TodoRepository from v1.todo.repository import TodoRepository
from v1.todo.schemas import ( from v1.todo.schemas import (
+1 -1
View File
@@ -5,7 +5,7 @@ from uuid import UUID
from fastapi import APIRouter, Depends 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.dependencies import get_user_service
from v1.users.schemas import UserSearchRequest, UserUpdateRequest from v1.users.schemas import UserSearchRequest, UserUpdateRequest
from v1.users.service import UserService 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.db.base_service import BaseService
from core.logging import get_logger 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.repository import UserRepository
from v1.users.schemas import UserSearchRequest, UserUpdateRequest from v1.users.schemas import UserSearchRequest, UserUpdateRequest
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from schemas.user.context import UserContext from schemas.shared.user import UserContext
logger = get_logger("v1.users.service") logger = get_logger("v1.users.service")
@@ -105,7 +105,7 @@ class UserService(BaseService):
) )
async def get_user_by_id(self, user_id: UUID) -> "UserContext": async def get_user_by_id(self, user_id: UUID) -> "UserContext":
from schemas.user.context import UserContext from schemas.shared.user import UserContext
try: try:
profile = await self._repository.get_by_user_id(user_id) 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 app import app
from core.auth.models import CurrentUser 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.dependencies import get_current_user, get_user_service
from v1.users.schemas import UserUpdateRequest from v1.users.schemas import UserUpdateRequest
@@ -9,7 +9,7 @@ from fastapi.testclient import TestClient
from app import app from app import app
from core.auth.models import CurrentUser 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.dependencies import get_friendship_service
from v1.friendships.schemas import ( from v1.friendships.schemas import (
FriendRequestCreate, FriendRequestCreate,
@@ -8,7 +8,7 @@ from fastapi.testclient import TestClient
from app import app from app import app
from core.auth.models import CurrentUser 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.dependencies import get_current_user, get_user_service
from v1.users.schemas import UserSearchRequest, UserUpdateRequest from v1.users.schemas import UserSearchRequest, UserUpdateRequest
from v1.users.service import UserService from v1.users.service import UserService
@@ -6,7 +6,7 @@ from uuid import uuid4
import pytest import pytest
from core.agentscope.persistence.user_context_cache import UserContextCache from core.agentscope.persistence.user_context_cache import UserContextCache
from schemas.user.context import ( from schemas.shared.user import (
UserContext, UserContext,
parse_profile_settings, 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 ag_ui.core import RunAgentInput
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
from schemas.automation import MessageContextConfig, RuntimeConfig from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.user import UserContext, parse_profile_settings from schemas.shared.user import UserContext, parse_profile_settings
class _FakePipeline: 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, WorkerAgentOutputLite,
) )
from schemas.agent.system_agent import AgentType from schemas.agent.system_agent import AgentType
from schemas.automation import MessageContextConfig, RuntimeConfig from schemas.domain.automation import MessageContextConfig, RuntimeConfig
from schemas.user import UserContext, parse_profile_settings from schemas.shared.user import UserContext, parse_profile_settings
def _run_input() -> RunAgentInput: def _run_input() -> RunAgentInput:
@@ -7,8 +7,8 @@ import pytest
import core.agentscope.runtime.tasks as tasks_module import core.agentscope.runtime.tasks as tasks_module
from schemas.agent import ToolStatus from schemas.agent import ToolStatus
from schemas.automation import ContextWindowMode, MessageContextConfig from schemas.domain.automation import ContextWindowMode, MessageContextConfig
from schemas.user import UserContext, parse_profile_settings from schemas.shared.user import UserContext, parse_profile_settings
def _run_input_payload() -> dict[str, Any]: 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), start_at=datetime(2026, 3, 17, 9, 0, tzinfo=timezone.utc),
end_at=datetime(2026, 3, 17, 9, 30, tzinfo=timezone.utc), end_at=datetime(2026, 3, 17, 9, 30, tzinfo=timezone.utc),
timezone="Asia/Shanghai", timezone="Asia/Shanghai",
status="active",
metadata=SimpleNamespace( metadata=SimpleNamespace(
location=None, color="#4F46E5", reminder_minutes=15 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 "total=1" in payload["result"]
assert "timezone=Asia/Shanghai" in payload["result"] assert "timezone=Asia/Shanghai" in payload["result"]
assert "description=今天下午五点的会议" 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.created_id in payload["result"]
assert fake_service.list_calls == [{"page": 1, "page_size": 20, "query": "会议"}] 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 core.agentscope.tools.custom import memory as memory_module
from models.memories import MemoryType 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]: 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.forwarded_props import ClientTimeContext
from schemas.agent.system_agent import AgentType 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: 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: 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() 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: 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() work_memory = WorkProfileContent()
@@ -6,7 +6,7 @@ from uuid import UUID, uuid4
import pytest import pytest
from models.automation_jobs import AutomationJob as OrmAutomationJob, ScheduleType from models.automation_jobs import AutomationJob as OrmAutomationJob, ScheduleType
from schemas.automation import ( from schemas.domain.automation import (
RuntimeConfig, RuntimeConfig,
ScheduleConfig, ScheduleConfig,
ScheduleRunAt, 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