Add local timezone datetime support in agent prompts

- Add timezone support to replaceSpecialVars function with dayjs timezone plugin
- Add new special variables: {{local_datetime}} and {{local_date}}
- Update specialVariables config to include new variables
- Pass timezone from client via Intl.DateTimeFormat API
- Add timezone field to TPayload type
- Update all client-side replaceSpecialVars calls to include timezone
- Add comprehensive tests for timezone-aware special variables
- Update web_search tool context to include local datetime when timezone available

Co-authored-by: berry-13 <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-10-21 21:26:26 +00:00
parent c1dcabae20
commit 9fe4554a70
11 changed files with 127 additions and 6 deletions

View file

@ -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).

View file

@ -185,6 +185,7 @@ const initializeAgent = async ({
agent.instructions = replaceSpecialVars({
text: agent.instructions,
user: req.user,
timezone: req.body?.timezone,
});
}

View file

@ -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(

View file

@ -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) {

View file

@ -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,
});
}

View file

@ -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;

View file

@ -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)');
});
});

View file

@ -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}`;

View file

@ -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 };

View file

@ -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);
}

View file

@ -110,6 +110,7 @@ export type TPayload = Partial<TMessage> &
isTemporary: boolean;
ephemeralAgent?: TEphemeralAgent | null;
editedContent?: TEditedContent | null;
timezone?: string;
};
export type TEditedContent =