diff --git a/autogpt_platform/backend/backend/executor/scheduler.py b/autogpt_platform/backend/backend/executor/scheduler.py index e476b3ceed..77ed652886 100644 --- a/autogpt_platform/backend/backend/executor/scheduler.py +++ b/autogpt_platform/backend/backend/executor/scheduler.py @@ -191,15 +191,22 @@ class GraphExecutionJobInfo(GraphExecutionJobArgs): id: str name: str next_run_time: str + timezone: str = Field(default="UTC", description="Timezone used for scheduling") @staticmethod def from_db( job_args: GraphExecutionJobArgs, job_obj: JobObj ) -> "GraphExecutionJobInfo": + # Extract timezone from the trigger if it's a CronTrigger + timezone_str = "UTC" + if hasattr(job_obj.trigger, "timezone"): + timezone_str = str(job_obj.trigger.timezone) + return GraphExecutionJobInfo( id=job_obj.id, name=job_obj.name, next_run_time=job_obj.next_run_time.isoformat(), + timezone=timezone_str, **job_args.model_dump(), ) @@ -395,6 +402,7 @@ class Scheduler(AppService): input_data: BlockInput, input_credentials: dict[str, CredentialsMetaInput], name: Optional[str] = None, + user_timezone: str | None = None, ) -> GraphExecutionJobInfo: # Validate the graph before scheduling to prevent runtime failures # We don't need the return value, just want the validation to run @@ -408,7 +416,18 @@ class Scheduler(AppService): ) ) - logger.info(f"Scheduling job for user {user_id} in UTC (cron: {cron})") + # Use provided timezone or default to UTC + # Note: Timezone should be passed from the client to avoid database lookups + if not user_timezone: + user_timezone = "UTC" + logger.warning( + f"No timezone provided for user {user_id}, using UTC for scheduling. " + f"Client should pass user's timezone for correct scheduling." + ) + + logger.info( + f"Scheduling job for user {user_id} with timezone {user_timezone} (cron: {cron})" + ) job_args = GraphExecutionJobArgs( user_id=user_id, @@ -422,12 +441,12 @@ class Scheduler(AppService): execute_graph, kwargs=job_args.model_dump(), name=name, - trigger=CronTrigger.from_crontab(cron, timezone="UTC"), + trigger=CronTrigger.from_crontab(cron, timezone=user_timezone), jobstore=Jobstores.EXECUTION.value, replace_existing=True, ) logger.info( - f"Added job {job.id} with cron schedule '{cron}' in UTC, input data: {input_data}" + f"Added job {job.id} with cron schedule '{cron}' in timezone {user_timezone}, input data: {input_data}" ) return GraphExecutionJobInfo.from_db(job_args, job) diff --git a/autogpt_platform/backend/backend/server/routers/v1.py b/autogpt_platform/backend/backend/server/routers/v1.py index c5c7d818e3..084b0f3cb2 100644 --- a/autogpt_platform/backend/backend/server/routers/v1.py +++ b/autogpt_platform/backend/backend/server/routers/v1.py @@ -90,7 +90,6 @@ from backend.util.cloud_storage import get_cloud_storage_handler from backend.util.exceptions import GraphValidationError, NotFoundError from backend.util.settings import Settings from backend.util.timezone_utils import ( - convert_cron_to_utc, convert_utc_time_to_user_timezone, get_user_timezone_or_utc, ) @@ -945,6 +944,10 @@ class ScheduleCreationRequest(pydantic.BaseModel): cron: str inputs: dict[str, Any] credentials: dict[str, CredentialsMetaInput] = pydantic.Field(default_factory=dict) + timezone: Optional[str] = pydantic.Field( + default=None, + description="User's timezone for scheduling (e.g., 'America/New_York'). If not provided, will use user's saved timezone or UTC.", + ) @v1_router.post( @@ -969,26 +972,22 @@ async def create_graph_execution_schedule( detail=f"Graph #{graph_id} v{schedule_params.graph_version} not found.", ) - user = await get_user_by_id(user_id) - user_timezone = get_user_timezone_or_utc(user.timezone if user else None) - - # Convert cron expression from user timezone to UTC - try: - utc_cron = convert_cron_to_utc(schedule_params.cron, user_timezone) - except ValueError as e: - raise HTTPException( - status_code=400, - detail=f"Invalid cron expression for timezone {user_timezone}: {e}", - ) + # Use timezone from request if provided, otherwise fetch from user profile + if schedule_params.timezone: + user_timezone = schedule_params.timezone + else: + user = await get_user_by_id(user_id) + user_timezone = get_user_timezone_or_utc(user.timezone if user else None) result = await get_scheduler_client().add_execution_schedule( user_id=user_id, graph_id=graph_id, graph_version=graph.version, name=schedule_params.name, - cron=utc_cron, # Send UTC cron to scheduler + cron=schedule_params.cron, input_data=schedule_params.inputs, input_credentials=schedule_params.credentials, + user_timezone=user_timezone, ) # Convert the next_run_time back to user timezone for display @@ -1010,24 +1009,11 @@ async def list_graph_execution_schedules( user_id: Annotated[str, Security(get_user_id)], graph_id: str = Path(), ) -> list[scheduler.GraphExecutionJobInfo]: - schedules = await get_scheduler_client().get_execution_schedules( + return await get_scheduler_client().get_execution_schedules( user_id=user_id, graph_id=graph_id, ) - # Get user timezone for conversion - user = await get_user_by_id(user_id) - user_timezone = get_user_timezone_or_utc(user.timezone if user else None) - - # Convert next_run_time to user timezone for display - for schedule in schedules: - if schedule.next_run_time: - schedule.next_run_time = convert_utc_time_to_user_timezone( - schedule.next_run_time, user_timezone - ) - - return schedules - @v1_router.get( path="/schedules", @@ -1038,20 +1024,7 @@ async def list_graph_execution_schedules( async def list_all_graphs_execution_schedules( user_id: Annotated[str, Security(get_user_id)], ) -> list[scheduler.GraphExecutionJobInfo]: - schedules = await get_scheduler_client().get_execution_schedules(user_id=user_id) - - # Get user timezone for conversion - user = await get_user_by_id(user_id) - user_timezone = get_user_timezone_or_utc(user.timezone if user else None) - - # Convert UTC next_run_time to user timezone for display - for schedule in schedules: - if schedule.next_run_time: - schedule.next_run_time = convert_utc_time_to_user_timezone( - schedule.next_run_time, user_timezone - ) - - return schedules + return await get_scheduler_client().get_execution_schedules(user_id=user_id) @v1_router.delete( diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts index 4fc6e1c926..daaa920814 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentModal/useAgentRunModal.ts @@ -15,6 +15,7 @@ import { usePostV2SetupTrigger } from "@/app/api/__generated__/endpoints/presets import { GraphExecutionMeta } from "@/app/api/__generated__/models/graphExecutionMeta"; import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset"; +import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth"; export type RunVariant = | "manual" @@ -48,6 +49,13 @@ export function useAgentRunModal( agent.recommended_schedule_cron || "0 9 * * 1", ); + // Get user timezone for scheduling + const { data: userTimezone } = useGetV1GetUserTimezone({ + query: { + select: (res) => (res.status === 200 ? res.data.timezone : undefined), + }, + }); + // Determine the default run type based on agent capabilities const defaultRunType: RunVariant = agent.has_external_trigger ? "automatic-trigger" @@ -307,6 +315,8 @@ export function useAgentRunModal( inputs: inputValues, graph_version: agent.graph_version, credentials: inputCredentials, + timezone: + userTimezone && userTimezone !== "not-set" ? userTimezone : undefined, }, }); }, [ @@ -319,6 +329,7 @@ export function useAgentRunModal( notifyMissingRequirements, createScheduleMutation, toast, + userTimezone, ]); function handleShowSchedule() { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx index 9cd20aa6c2..7040b2f958 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/AgentRunsView/components/ScheduleDetails/ScheduleDetails.tsx @@ -98,7 +98,7 @@ export function ScheduleDetails({ run={undefined} scheduleRecurrence={ schedule - ? `${humanizeCronExpression(schedule.cron || "", userTzRes)} · ${getTimezoneDisplayName(userTzRes || "UTC")}` + ? `${humanizeCronExpression(schedule.cron || "")} · ${getTimezoneDisplayName(schedule.timezone || userTzRes || "UTC")}` : undefined } /> @@ -161,10 +161,12 @@ export function ScheduleDetails({ Recurrence
- {humanizeCronExpression(schedule.cron, userTzRes)} + {humanizeCronExpression(schedule.cron)} {" • "} - {getTimezoneDisplayName(userTzRes || "UTC")} + {getTimezoneDisplayName( + schedule.timezone || userTzRes || "UTC", + )}
@@ -187,7 +189,9 @@ export function ScheduleDetails({ )}{" "} •{" "} - {getTimezoneDisplayName(userTzRes || "UTC")} + {getTimezoneDisplayName( + schedule.timezone || userTzRes || "UTC", + )} diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx index fbf057f42f..dddc870823 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/OldAgentLibraryView/components/agent-schedule-details-view.tsx @@ -59,7 +59,7 @@ export function AgentScheduleDetailsView({ }, { label: "Schedule", - value: humanizeCronExpression(schedule.cron, userTimezone), + value: humanizeCronExpression(schedule.cron), }, { label: "Next run", diff --git a/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx b/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx index 434103bf34..f02d146c5a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/monitoring/page.tsx @@ -1,12 +1,12 @@ "use client"; import React, { useCallback, useEffect, useState } from "react"; +import { GraphExecutionMeta, LibraryAgent } from "@/lib/autogpt-server-api"; +import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { - GraphExecutionMeta, - Schedule, - LibraryAgent, - ScheduleID, -} from "@/lib/autogpt-server-api"; + useGetV1ListExecutionSchedulesForAUser, + useDeleteV1DeleteExecutionSchedule, +} from "@/app/api/__generated__/endpoints/schedules/schedules"; import { Card } from "@/components/ui/card"; import { @@ -22,26 +22,29 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context"; const Monitor = () => { const [flows, setFlows] = useState