diff --git a/apps/test/features/calendar/ui/calendar_time_utils_test.dart b/apps/test/features/calendar/ui/calendar_time_utils_test.dart deleted file mode 100644 index b9518b0..0000000 --- a/apps/test/features/calendar/ui/calendar_time_utils_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/calendar/ui/calendar_time_utils.dart'; - -void main() { - group('calendar_time_utils', () { - test('returns week start on sunday', () { - final date = DateTime(2026, 2, 11); - final weekStart = weekStartFor(date); - - expect(weekStart.year, 2026); - expect(weekStart.month, 2); - expect(weekStart.day, 8); - }); - - test('shows current marker only for selected today', () { - final now = DateTime(2026, 2, 11, 15, 28); - - expect(shouldShowCurrentMarker(DateTime(2026, 2, 11), now), isTrue); - expect(shouldShowCurrentMarker(DateTime(2026, 2, 10), now), isFalse); - }); - - test('formats hour minute with zero pad', () { - expect(formatHm(DateTime(2026, 2, 11, 7, 5)), '07:05'); - expect(formatHm(DateTime(2026, 2, 11, 15, 28)), '15:28'); - }); - - test('parses and formats ymd date string', () { - final parsed = parseYmd('2026-02-11'); - - expect(parsed, isNotNull); - expect(parsed!.year, 2026); - expect(parsed.month, 2); - expect(parsed.day, 11); - expect(formatYmd(parsed), '2026-02-11'); - }); - - test('returns null for invalid ymd date string', () { - expect(parseYmd('2026/02/11'), isNull); - expect(parseYmd('bad-input'), isNull); - expect(parseYmd(null), isNull); - }); - - test('builds all dates for month', () { - final dates = monthDatesFor(DateTime(2026, 2, 9)); - - expect(dates.length, 28); - expect(formatYmd(dates.first), '2026-02-01'); - expect(formatYmd(dates.last), '2026-02-28'); - }); - }); -} diff --git a/apps/test/features/calendar/ui/create_event_sheet_test.dart b/apps/test/features/calendar/ui/create_event_sheet_test.dart deleted file mode 100644 index 46bc111..0000000 --- a/apps/test/features/calendar/ui/create_event_sheet_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/calendar/data/models/schedule_item_model.dart'; -import 'package:social_app/features/calendar/ui/widgets/create_event_sheet.dart'; - -void main() { - testWidgets('编辑日程时支持非默认提醒值', (tester) async { - final event = ScheduleItemModel( - id: 'evt_1', - ownerId: 'user_1', - title: '测试日程', - startAt: DateTime(2026, 3, 18, 10, 0), - endAt: DateTime(2026, 3, 18, 11, 0), - metadata: ScheduleMetadata(reminderMinutes: 20), - ); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: CreateEventSheet(editingEvent: event)), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('进阶')); - await tester.pumpAndSettle(); - - expect(find.text('开始前20分钟'), findsOneWidget); - }); -} diff --git a/apps/test/features/calendar/ui/create_event_sheet_time_align_test.dart b/apps/test/features/calendar/ui/create_event_sheet_time_align_test.dart deleted file mode 100644 index fb37bb1..0000000 --- a/apps/test/features/calendar/ui/create_event_sheet_time_align_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('时间自动对齐逻辑', () { - test('开始时间改变后,结束时间早于开始时间应自动对齐', () { - DateTime startTime = DateTime(2026, 3, 11, 10, 0); - DateTime endTime = DateTime(2026, 3, 11, 9, 0); - - final newStartTime = DateTime(2026, 3, 11, 14, 30); - - if (endTime.isBefore(newStartTime)) { - endTime = newStartTime; - } - - expect(endTime.hour, 14); - expect(endTime.minute, 30); - }); - - test('结束时间晚于开始时间则不需要对齐', () { - DateTime startTime = DateTime(2026, 3, 11, 10, 0); - DateTime endTime = DateTime(2026, 3, 11, 12, 0); - - final newStartTime = DateTime(2026, 3, 11, 14, 30); - - if (endTime.isBefore(newStartTime)) { - endTime = newStartTime; - } - - expect(endTime.hour, 14); - expect(endTime.minute, 30); - }); - - test('结束时间等于开始时间也需要对齐', () { - DateTime startTime = DateTime(2026, 3, 11, 10, 0); - DateTime endTime = DateTime(2026, 3, 11, 10, 0); - - final newStartTime = DateTime(2026, 3, 11, 14, 30); - - if (endTime.isBefore(newStartTime)) { - endTime = newStartTime; - } - - expect(endTime.hour, 14); - expect(endTime.minute, 30); - }); - }); -} diff --git a/apps/test/features/calendar/ui/dayweek/day_event_layout_engine_test.dart b/apps/test/features/calendar/ui/dayweek/day_event_layout_engine_test.dart deleted file mode 100644 index 7fc846e..0000000 --- a/apps/test/features/calendar/ui/dayweek/day_event_layout_engine_test.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/calendar/data/models/schedule_item_model.dart'; -import 'package:social_app/features/calendar/ui/dayweek/day_event_layout_engine.dart'; -import 'package:social_app/features/calendar/ui/dayweek/day_timeline_metrics.dart'; -import 'package:social_app/features/calendar/ui/dayweek/day_view_scale.dart'; - -void main() { - group('DayEventLayoutEngine', () { - const engine = DayEventLayoutEngine(); - const scale = DayViewScale(hourHeight: 60); - - test('maps event top and height by exact minutes', () { - final event = _event( - id: 'a', - start: DateTime(2026, 3, 18, 10, 15), - end: DateTime(2026, 3, 18, 11, 45), - ); - - final layouts = engine.layout( - events: [event], - scale: scale, - eventAreaLeft: DayTimelineMetrics.eventAreaLeft(), - eventAreaWidth: 200, - ); - - expect(layouts, hasLength(1)); - expect(layouts.first.startMinutes, 615); - expect(layouts.first.endMinutes, 705); - expect(layouts.first.top, 615); - expect(layouts.first.geometryHeight, 90); - expect(layouts.first.visualHeight, 90); - }); - - test('splits overlapped events into columns', () { - final e1 = _event( - id: 'a', - start: DateTime(2026, 3, 18, 9, 0), - end: DateTime(2026, 3, 18, 10, 0), - ); - final e2 = _event( - id: 'b', - start: DateTime(2026, 3, 18, 9, 30), - end: DateTime(2026, 3, 18, 10, 30), - ); - - final layouts = engine.layout( - events: [e1, e2], - scale: scale, - eventAreaLeft: DayTimelineMetrics.eventAreaLeft(), - eventAreaWidth: 200, - ); - - expect(layouts, hasLength(2)); - expect(layouts[0].columnCount, 2); - expect(layouts[1].columnCount, 2); - expect(layouts[0].width, closeTo(98, 0.001)); - expect(layouts[1].width, closeTo(98, 0.001)); - expect(layouts[0].left, DayTimelineMetrics.eventAreaLeft()); - expect( - layouts[1].left, - closeTo(DayTimelineMetrics.eventAreaLeft() + 102, 0.001), - ); - }); - - test('uses 1 pixel minimum visual height but preserves geometry', () { - final event = _event( - id: 'a', - start: DateTime(2026, 3, 18, 9, 0), - end: DateTime(2026, 3, 18, 9, 1), - ); - - final tinyScale = const DayViewScale( - hourHeight: DayViewScale.minHourHeight, - ); - final layouts = engine.layout( - events: [event], - scale: tinyScale, - eventAreaLeft: DayTimelineMetrics.eventAreaLeft(), - eventAreaWidth: 200, - ); - - expect(layouts, hasLength(1)); - expect( - layouts.first.geometryHeight, - closeTo(tinyScale.pixelsForMinutes(1), 0.001), - ); - expect(layouts.first.visualHeight, greaterThanOrEqualTo(1)); - }); - }); -} - -ScheduleItemModel _event({ - required String id, - required DateTime start, - required DateTime end, -}) { - return ScheduleItemModel( - id: id, - ownerId: 'owner', - title: 'event-$id', - startAt: start, - endAt: end, - ); -} diff --git a/apps/test/features/calendar/ui/dayweek/day_view_scale_test.dart b/apps/test/features/calendar/ui/dayweek/day_view_scale_test.dart deleted file mode 100644 index b31d258..0000000 --- a/apps/test/features/calendar/ui/dayweek/day_view_scale_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/features/calendar/ui/dayweek/day_view_scale.dart'; - -void main() { - group('DayViewScale', () { - test('maps minutes to pixels and back', () { - const scale = DayViewScale(hourHeight: 60); - - expect(scale.pixelsForMinutes(30), 30); - expect(scale.pixelsForMinutes(75), 75); - expect(scale.minutesForPixels(90), 90); - }); - - test('clamps zoom height at boundaries', () { - const scale = DayViewScale(hourHeight: 34); - - final zoomIn = scale.zoomByFactor(20); - final zoomOut = scale.zoomByFactor(0.01); - - expect(zoomIn.hourHeight, DayViewScale.maxHourHeight); - expect(zoomOut.hourHeight, DayViewScale.minHourHeight); - }); - - test('ignores invalid zoom factor', () { - const scale = DayViewScale(hourHeight: 34); - - expect(scale.zoomByFactor(0).hourHeight, 34); - expect(scale.zoomByFactor(-1).hourHeight, 34); - }); - }); -} diff --git a/apps/test/features/calendar/ui/event_color_resolver_test.dart b/apps/test/features/calendar/ui/event_color_resolver_test.dart deleted file mode 100644 index ebce70d..0000000 --- a/apps/test/features/calendar/ui/event_color_resolver_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:social_app/core/theme/design_tokens.dart'; -import 'package:social_app/features/calendar/data/models/schedule_item_model.dart'; -import 'package:social_app/features/calendar/ui/utils/event_color_resolver.dart'; - -void main() { - test('returns gray for archived status regardless of custom color', () { - final color = resolveEventColor( - status: ScheduleStatus.archived, - colorHex: '#EF4444', - ); - - expect(color, AppColors.slate400); - }); - - test('returns parsed color for active status', () { - final color = resolveEventColor( - status: ScheduleStatus.active, - colorHex: '#3B82F6', - ); - - expect(color.value, const Color(0xFF3B82F6).value); - }); -} diff --git a/backend/src/models/profile.py b/backend/src/models/profile.py index 64b7541..5d0d9aa 100644 --- a/backend/src/models/profile.py +++ b/backend/src/models/profile.py @@ -2,7 +2,7 @@ from __future__ import annotations import uuid -from sqlalchemy import ForeignKey, String, Text +from sqlalchemy import String, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column @@ -43,9 +43,3 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base): nullable=False, server_default="{}", ) - referred_by: Mapped[uuid.UUID | None] = mapped_column( - UUID(as_uuid=True), - ForeignKey("profiles.id", ondelete="SET NULL"), - nullable=True, - index=True, - ) diff --git a/backend/src/schemas/agent/__init__.py b/backend/src/schemas/agent/__init__.py index 86de19d..1647112 100644 --- a/backend/src/schemas/agent/__init__.py +++ b/backend/src/schemas/agent/__init__.py @@ -1,15 +1,39 @@ +from schemas.agent.consumer_registry import AgentConsumerBinding, ConsumerRegistry from schemas.agent.forwarded_props import ( ClientTimeContext, + ForwardedPropsPayload, + parse_forwarded_props_agent_type, parse_forwarded_props_client_time, ) +from schemas.agent.pipeline_spec import ( + ContextPolicy, + ContextWindowMode, + ExecutorKind, + PipelineSpec, + StageSpec, +) from schemas.agent.runtime_models import ( AgentOutput, + ConstraintItem, + ExecutionMode, + KeyEntity, + NormalizedTaskInput, + ResultTyping, ResultType, + RouterAgentOutput, + RouterUiDecision, RunStatus, + TaskType, + TaskTyping, ToolAgentOutput, ToolStatus, + UiMode, + WorkerAgentOutputLite, + WorkerAgentOutputRich, + resolve_worker_output_model, ) from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig +from schemas.agent.visibility import SystemVisibilityBit, VisibilityMask, bit_mask from schemas.agent.ui_hints import ( UiHintAction, UiHintIntent, @@ -21,16 +45,41 @@ from schemas.agent.ui_hints import ( __all__ = [ "AgentType", "AgentOutput", + "AgentConsumerBinding", + "ConstraintItem", + "ConsumerRegistry", + "ContextPolicy", + "ContextWindowMode", + "ExecutionMode", + "ExecutorKind", + "ForwardedPropsPayload", + "KeyEntity", + "NormalizedTaskInput", + "PipelineSpec", + "ResultTyping", "ClientTimeContext", "ResultType", + "RouterAgentOutput", + "RouterUiDecision", "RunStatus", + "TaskType", + "TaskTyping", "SystemAgentLLMConfig", + "SystemVisibilityBit", + "StageSpec", "ToolAgentOutput", "ToolStatus", + "UiMode", "UiHintAction", "UiHintIntent", "UiHintSection", "UiHintStatus", "UiHintsPayload", + "VisibilityMask", + "WorkerAgentOutputLite", + "WorkerAgentOutputRich", + "bit_mask", + "parse_forwarded_props_agent_type", "parse_forwarded_props_client_time", + "resolve_worker_output_model", ] diff --git a/backend/src/schemas/agent/forwarded_props.py b/backend/src/schemas/agent/forwarded_props.py index 4afc8a5..dbda22a 100644 --- a/backend/src/schemas/agent/forwarded_props.py +++ b/backend/src/schemas/agent/forwarded_props.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime import re -from typing import Any from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from pydantic import ( @@ -63,16 +62,34 @@ class ClientTimeContext(BaseModel): class ForwardedPropsPayload(BaseModel): model_config = ConfigDict(extra="forbid") + agent_type: str = Field(..., min_length=1, max_length=64) client_time: ClientTimeContext | None = None + @field_validator("agent_type") + @classmethod + def validate_agent_type(cls, value: str) -> str: + normalized = value.strip().lower() + if not normalized: + raise ValueError("invalid forwarded_props.agent_type") + return normalized + + +def parse_forwarded_props(forwarded_props: object) -> ForwardedPropsPayload: + if not isinstance(forwarded_props, dict): + raise ValueError("invalid RunAgentInput.forwardedProps") + try: + return ForwardedPropsPayload.model_validate(forwarded_props) + except ValidationError as exc: + raise ValueError("invalid RunAgentInput.forwardedProps") from exc + def parse_forwarded_props_client_time( - forwarded_props: Any, + forwarded_props: object, ) -> ClientTimeContext | None: - if not isinstance(forwarded_props, dict): - return None - try: - payload = ForwardedPropsPayload.model_validate(forwarded_props) - except ValidationError as exc: - raise ValueError("invalid RunAgentInput.forwardedProps") from exc + payload = parse_forwarded_props(forwarded_props) return payload.client_time + + +def parse_forwarded_props_agent_type(forwarded_props: object) -> str: + payload = parse_forwarded_props(forwarded_props) + return payload.agent_type diff --git a/backend/src/schemas/agent/runtime_models.py b/backend/src/schemas/agent/runtime_models.py index 81d084b..982e15e 100644 --- a/backend/src/schemas/agent/runtime_models.py +++ b/backend/src/schemas/agent/runtime_models.py @@ -8,6 +8,22 @@ from pydantic import BaseModel, ConfigDict, Field from schemas.agent.ui_hints import UiHintsPayload +class TaskType(str, Enum): + KNOWLEDGE = "knowledge" + RECOMMENDATION = "recommendation" + PLANNING = "planning" + SCHEDULING = "scheduling" + REMINDER_MANAGEMENT = "reminder_management" + TODO_MANAGEMENT = "todo_management" + COMMUNICATION_DRAFTING = "communication_drafting" + INFORMATION_ORGANIZATION = "information_organization" + STATUS_TRACKING = "status_tracking" + TRANSACTION_ASSIST = "transaction_assist" + ACTION_EXECUTION = "action_execution" + TROUBLESHOOTING = "troubleshooting" + UNKNOWN = "unknown" + + class ResultType(str, Enum): DIRECT_ANSWER = "direct_answer" OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation" @@ -26,6 +42,31 @@ class ResultType(str, Enum): UNKNOWN = "unknown" +class TaskTyping(BaseModel): + model_config = ConfigDict(extra="forbid") + + primary: TaskType + secondary: list[TaskType] = Field(default_factory=list, max_length=3) + + +class ResultTyping(BaseModel): + model_config = ConfigDict(extra="forbid") + + primary: ResultType + secondary: list[ResultType] = Field(default_factory=list, max_length=3) + + +class ExecutionMode(str, Enum): + ONESTEP = "onestep" + TOOL_ASSISTED = "tool_assisted" + MULTISTEP = "multistep" + + +class UiMode(str, Enum): + NONE = "none" + RICH = "rich" + + class RunStatus(str, Enum): SUCCESS = "success" PARTIAL_SUCCESS = "partial_success" @@ -38,13 +79,55 @@ class ToolStatus(str, Enum): PARTIAL = "partial" +class KeyEntity(BaseModel): + model_config = ConfigDict(extra="forbid") + + name: str + type: str + value: str | None = None + + +class ConstraintItem(BaseModel): + model_config = ConfigDict(extra="forbid") + + key: str + value: str + required: bool = True + + +class NormalizedTaskInput(BaseModel): + model_config = ConfigDict(extra="forbid") + + user_text: str + multimodal_summary: list[str] = Field(default_factory=list) + + +class RouterUiDecision(BaseModel): + model_config = ConfigDict(extra="forbid") + + ui_mode: UiMode + ui_decision_reason: str + + +class RouterAgentOutput(BaseModel): + model_config = ConfigDict(extra="forbid") + + normalized_task_input: NormalizedTaskInput + key_entities: list[KeyEntity] = Field(default_factory=list) + constraints: list[ConstraintItem] = Field(default_factory=list) + task_typing: TaskTyping + execution_mode: ExecutionMode + result_typing: ResultTyping + ui: RouterUiDecision + + class ErrorInfo(BaseModel): model_config = ConfigDict(extra="forbid") - code: str = Field(..., description="Stable error code for programmatic handling.") - message: str = Field(..., description="Human-readable error message.") - retryable: bool = Field(default=False) - details: dict[str, Any] | None = Field(default=None) + code: str + message: str + retryable: bool = False + details: dict[str, Any] | None = None class ToolAgentOutput(BaseModel): @@ -58,13 +141,29 @@ class ToolAgentOutput(BaseModel): error: ErrorInfo | None = None -class AgentOutput(BaseModel): +class WorkerAgentOutputLite(BaseModel): model_config = ConfigDict(extra="forbid") - status: RunStatus = Field(default=RunStatus.SUCCESS) + status: RunStatus = RunStatus.SUCCESS answer: str key_points: list[str] = Field(default_factory=list) - result_type: ResultType = Field(default=ResultType.UNKNOWN) + result_type: ResultType = ResultType.UNKNOWN suggested_actions: list[str] = Field(default_factory=list) error: ErrorInfo | None = None + + +class WorkerAgentOutputRich(WorkerAgentOutputLite): ui_hints: UiHintsPayload | None = None + + +class AgentOutput(WorkerAgentOutputRich): + model_config = ConfigDict(extra="forbid") + + +WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich + + +def resolve_worker_output_model(ui_mode: UiMode) -> type[WorkerAgentOutputLite]: + if ui_mode == UiMode.RICH: + return WorkerAgentOutputRich + return WorkerAgentOutputLite diff --git a/backend/src/schemas/agent/system_agent.py b/backend/src/schemas/agent/system_agent.py index 6389391..fd20712 100644 --- a/backend/src/schemas/agent/system_agent.py +++ b/backend/src/schemas/agent/system_agent.py @@ -4,10 +4,11 @@ from enum import Enum from pydantic import BaseModel, Field, field_validator -from core.agentscope.tools.tool_config import ToolGroup +from core.agentscope.tools.tool_config import AgentTool, parse_agent_tool class AgentType(str, Enum): + ROUTER = "router" WORKER = "worker" MEMORY = "memory" @@ -29,24 +30,22 @@ class SystemAgentLLMConfig(BaseModel): context_messages: ContextMessagesConfig = Field( default_factory=ContextMessagesConfig ) - enabled_tool_groups: list[ToolGroup] = Field(default_factory=list, max_length=8) + visibility_consumer_bit: int = Field(default=16, ge=16, le=63) + enabled_tools: list[AgentTool] = Field(default_factory=list, max_length=32) - @field_validator("enabled_tool_groups", mode="before") + @field_validator("enabled_tools", mode="before") @classmethod - def _normalize_enabled_tool_groups(cls, value: object) -> list[ToolGroup]: + def _normalize_enabled_tools(cls, value: object) -> list[AgentTool]: if value is None: return [] if not isinstance(value, list): - raise ValueError("enabled_tool_groups must be a list") - normalized: list[ToolGroup] = [] + raise ValueError("enabled_tools must be a list") + normalized: list[AgentTool] = [] for item in value: - if isinstance(item, ToolGroup): - group = item - else: - raw_group = str(item or "").strip().lower() - if not raw_group: - continue - group = ToolGroup(raw_group) - if group not in normalized: - normalized.append(group) + raw_item = str(item or "").strip() + if not raw_item: + continue + tool = parse_agent_tool(raw_item) + if tool not in normalized: + normalized.append(tool) return normalized