diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index 698014cbe0..73918964d2 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -327,7 +327,7 @@ const loadTools = async ({ const { onSearchResults, onGetHighlights } = options?.[Tools.web_search] ?? {}; requestedTools[tool] = async () => { toolContextMap[tool] = `# \`${tool}\`: -Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })} +Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}${options.req?.body?.timezone ? `\nLocal Date & Time: ${replaceSpecialVars({ text: '{{local_datetime}}', timezone: options.req.body.timezone })}` : ''} 1. **Execute immediately without preface** when using \`${tool}\`. 2. **After the search, begin with a brief summary** that directly addresses the query without headers or explaining your process. 3. **Structure your response clearly** using Markdown formatting (Level 2 headers for sections, lists for multiple points, tables for comparisons). diff --git a/api/server/services/Endpoints/agents/agent.js b/api/server/services/Endpoints/agents/agent.js index ec9d56d026..e305bc0857 100644 --- a/api/server/services/Endpoints/agents/agent.js +++ b/api/server/services/Endpoints/agents/agent.js @@ -185,6 +185,7 @@ const initializeAgent = async ({ agent.instructions = replaceSpecialVars({ text: agent.instructions, user: req.user, + timezone: req.body?.timezone, }); } diff --git a/client/src/components/Prompts/Groups/VariableForm.tsx b/client/src/components/Prompts/Groups/VariableForm.tsx index d11bc041c6..9f2fb6b792 100644 --- a/client/src/components/Prompts/Groups/VariableForm.tsx +++ b/client/src/components/Prompts/Groups/VariableForm.tsx @@ -73,7 +73,8 @@ export default function VariableForm({ const mainText = useMemo(() => { const initialText = group.productionPrompt?.prompt ?? ''; - return replaceSpecialVars({ text: initialText, user }); + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + return replaceSpecialVars({ text: initialText, user, timezone }); }, [group.productionPrompt?.prompt, user]); const { allVariables, uniqueVariables, variableIndexMap } = useMemo( diff --git a/client/src/components/Prompts/PromptDetails.tsx b/client/src/components/Prompts/PromptDetails.tsx index f759079d18..bd38c90bbb 100644 --- a/client/src/components/Prompts/PromptDetails.tsx +++ b/client/src/components/Prompts/PromptDetails.tsx @@ -22,7 +22,8 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => { const mainText = useMemo(() => { const initialText = group?.productionPrompt?.prompt ?? ''; - return replaceSpecialVars({ text: initialText, user }); + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + return replaceSpecialVars({ text: initialText, user, timezone }); }, [group?.productionPrompt?.prompt, user]); if (!group) { diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 2dcfe37702..cc487aca9e 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -121,9 +121,11 @@ export default function useChatFunctions({ let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? []; if (conversation?.promptPrefix) { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; conversation.promptPrefix = replaceSpecialVars({ text: conversation.promptPrefix, user, + timezone, }); } diff --git a/client/src/hooks/Messages/useSubmitMessage.ts b/client/src/hooks/Messages/useSubmitMessage.ts index e5aa4d315e..553f400682 100644 --- a/client/src/hooks/Messages/useSubmitMessage.ts +++ b/client/src/hooks/Messages/useSubmitMessage.ts @@ -81,7 +81,8 @@ export default function useSubmitMessage() { const submitPrompt = useCallback( (text: string) => { - const parsedText = replaceSpecialVars({ text, user }); + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const parsedText = replaceSpecialVars({ text, user, timezone }); if (autoSendPrompts) { submitMessage({ text: parsedText }); return; diff --git a/packages/data-provider/specs/parsers.spec.ts b/packages/data-provider/specs/parsers.spec.ts index 8bcbfac7ec..d8ed3ed399 100644 --- a/packages/data-provider/specs/parsers.spec.ts +++ b/packages/data-provider/specs/parsers.spec.ts @@ -5,7 +5,7 @@ import type { TUser } from '../src/types'; // Mock dayjs module with consistent date/time values regardless of environment jest.mock('dayjs', () => { // Create a mock implementation that returns fixed values - const mockDayjs = () => ({ + const mockDayjs = (input?: any) => ({ format: (format: string) => { if (format === 'YYYY-MM-DD') { return '2024-04-29'; @@ -17,6 +17,33 @@ jest.mock('dayjs', () => { }, day: () => 1, // 1 = Monday toISOString: () => '2024-04-29T16:34:56.000Z', + tz: (timezone: string) => ({ + format: (format: string) => { + // Mock timezone-specific formatting for America/New_York (UTC-4 during DST) + if (timezone === 'America/New_York') { + if (format === 'YYYY-MM-DD') { + return '2024-04-29'; + } + if (format === 'YYYY-MM-DD HH:mm:ss') { + return '2024-04-29 08:34:56'; // 4 hours behind UTC 12:34:56 + } + } + // Mock timezone-specific formatting for Asia/Tokyo (UTC+9) + if (timezone === 'Asia/Tokyo') { + if (format === 'YYYY-MM-DD') { + return '2024-04-29'; + } + if (format === 'YYYY-MM-DD HH:mm:ss') { + return '2024-04-29 21:34:56'; // 9 hours ahead of UTC 12:34:56 + } + } + return format; + }, + day: () => { + // Return same day number for simplicity in tests + return 1; + }, + }), }); // Add any static methods needed @@ -121,5 +148,55 @@ describe('replaceSpecialVars', () => { expect(result).toContain('2024-04-29 12:34:56 (1)'); // current_datetime expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime expect(result).toContain('Test User'); // current_user + // local_date and local_datetime should fall back to UTC when no timezone provided + expect(result).toContain('2024-04-29 (1)'); // local_date (fallback to UTC) + expect(result).toContain('2024-04-29 12:34:56 (1)'); // local_datetime (fallback to UTC) + }); + + test('should replace {{local_date}} with the timezone-aware date when timezone is provided', () => { + const result = replaceSpecialVars({ + text: 'Today in NY is {{local_date}}', + timezone: 'America/New_York', + }); + expect(result).toBe('Today in NY is 2024-04-29 (1)'); + }); + + test('should replace {{local_datetime}} with the timezone-aware datetime when timezone is provided', () => { + const result = replaceSpecialVars({ + text: 'Now in NY is {{local_datetime}}', + timezone: 'America/New_York', + }); + expect(result).toBe('Now in NY is 2024-04-29 08:34:56 (1)'); + }); + + test('should replace {{local_datetime}} with Tokyo timezone', () => { + const result = replaceSpecialVars({ + text: 'Now in Tokyo is {{local_datetime}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('Now in Tokyo is 2024-04-29 21:34:56 (1)'); + }); + + test('should fall back to UTC for local variables when no timezone is provided', () => { + const result = replaceSpecialVars({ + text: 'Date: {{local_date}}, Time: {{local_datetime}}', + }); + expect(result).toBe('Date: 2024-04-29 (1), Time: 2024-04-29 12:34:56 (1)'); + }); + + test('should handle both UTC and local timezone variables in the same text', () => { + const result = replaceSpecialVars({ + text: 'UTC: {{current_datetime}}, Local: {{local_datetime}}', + timezone: 'America/New_York', + }); + expect(result).toBe('UTC: 2024-04-29 12:34:56 (1), Local: 2024-04-29 08:34:56 (1)'); + }); + + test('should be case-insensitive for local timezone variables', () => { + const result = replaceSpecialVars({ + text: 'Date: {{LOCAL_DATE}}, Time: {{Local_DateTime}}', + timezone: 'America/New_York', + }); + expect(result).toBe('Date: 2024-04-29 (1), Time: 2024-04-29 08:34:56 (1)'); }); }); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index c3f872eaec..182b023387 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1723,6 +1723,8 @@ export const specialVariables = { current_user: true, iso_datetime: true, current_datetime: true, + local_date: true, + local_datetime: true, }; export type TSpecialVarLabel = `com_ui_special_var_${keyof typeof specialVariables}`; diff --git a/packages/data-provider/src/createPayload.ts b/packages/data-provider/src/createPayload.ts index a0eacb244d..205d664498 100644 --- a/packages/data-provider/src/createPayload.ts +++ b/packages/data-provider/src/createPayload.ts @@ -38,6 +38,7 @@ export default function createPayload(submission: t.TSubmission) { conversationId, isContinued: !!(isEdited && isContinued), ephemeralAgent: s.isAssistantsEndpoint(endpoint) ? undefined : ephemeralAgent, + timezone: typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : undefined, }; return { server, payload }; diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 61616a57a8..1bc5c5a68a 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -1,9 +1,14 @@ import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; import type { ZodIssue } from 'zod'; import type * as a from './types/assistants'; import type * as s from './schemas'; import type * as t from './types'; import { ContentTypes } from './types/runs'; + +dayjs.extend(utc); +dayjs.extend(timezone); import { openAISchema, googleSchema, @@ -410,7 +415,15 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n return lastIndex; } -export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) { +export function replaceSpecialVars({ + text, + user, + timezone, +}: { + text: string; + user?: t.TUser | null; + timezone?: string; +}) { let result = text; if (!result) { return result; @@ -428,6 +441,27 @@ export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUse const isoDatetime = dayjs().toISOString(); result = result.replace(/{{iso_datetime}}/gi, isoDatetime); + // Local timezone support + if (timezone) { + try { + const localDate = dayjs().tz(timezone).format('YYYY-MM-DD'); + const localDayNumber = dayjs().tz(timezone).day(); + const localCombinedDate = `${localDate} (${localDayNumber})`; + result = result.replace(/{{local_date}}/gi, localCombinedDate); + + const localDatetime = dayjs().tz(timezone).format('YYYY-MM-DD HH:mm:ss'); + result = result.replace(/{{local_datetime}}/gi, `${localDatetime} (${localDayNumber})`); + } catch (error) { + // If timezone is invalid, fall back to UTC values for local_* variables + result = result.replace(/{{local_date}}/gi, combinedDate); + result = result.replace(/{{local_datetime}}/gi, `${currentDatetime} (${dayNumber})`); + } + } else { + // If no timezone is provided, replace local_* variables with UTC values + result = result.replace(/{{local_date}}/gi, combinedDate); + result = result.replace(/{{local_datetime}}/gi, `${currentDatetime} (${dayNumber})`); + } + if (user && user.name) { result = result.replace(/{{current_user}}/gi, user.name); } diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index c01db59c17..3992a88793 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -110,6 +110,7 @@ export type TPayload = Partial & isTemporary: boolean; ephemeralAgent?: TEphemeralAgent | null; editedContent?: TEditedContent | null; + timezone?: string; }; export type TEditedContent =