Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c31309a1db Add comprehensive documentation for special variables feature
- Add detailed JSDoc comments to replaceSpecialVars function
- Create SPECIAL_VARIABLES.md with complete usage guide
- Document all available special variables including new local timezone ones
- Provide examples and technical implementation details
- Include migration guide for existing users

Co-authored-by: berry-13 <81851188+berry-13@users.noreply.github.com>
2025-10-21 21:34:15 +00:00
copilot-swe-agent[bot]
3c4c4d53dd Fix linting issues in timezone implementation
Co-authored-by: berry-13 <81851188+berry-13@users.noreply.github.com>
2025-10-21 21:32:01 +00:00
copilot-swe-agent[bot]
9fe4554a70 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>
2025-10-21 21:26:26 +00:00
copilot-swe-agent[bot]
c1dcabae20 Initial plan 2025-10-21 21:04:56 +00:00
12 changed files with 273 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

@@ -0,0 +1,114 @@
# Special Variables in LibreChat
LibreChat supports special variables that can be used in agent prompts, conversation prompts, and other text fields. These variables are automatically replaced with their corresponding values at runtime.
## Available Special Variables
### Date and Time Variables
#### UTC-based Variables
- **`{{current_date}}`**: Current date in UTC timezone
- Format: `YYYY-MM-DD (D)` where D is the day of week (0=Sunday, 1=Monday, etc.)
- Example: `2024-04-29 (1)` (Monday, April 29, 2024)
- **`{{current_datetime}}`**: Current date and time in UTC timezone
- Format: `YYYY-MM-DD HH:mm:ss (D)` where D is the day of week
- Example: `2024-04-29 12:34:56 (1)` (Monday, April 29, 2024 at 12:34:56 PM UTC)
- **`{{iso_datetime}}`**: Current date and time in ISO 8601 format (UTC)
- Format: ISO 8601 standard
- Example: `2024-04-29T16:34:56.000Z`
#### Local Timezone Variables
- **`{{local_date}}`**: Current date in the user's local timezone
- Format: `YYYY-MM-DD (D)` where D is the day of week
- Example: `2024-04-29 (1)` (Monday, April 29, 2024 in user's timezone)
- Note: Falls back to UTC if timezone is not available
- **`{{local_datetime}}`**: Current date and time in the user's local timezone
- Format: `YYYY-MM-DD HH:mm:ss (D)` where D is the day of week
- Example: `2024-04-29 08:34:56 (1)` (Monday, April 29, 2024 at 8:34:56 AM in America/New_York)
- Note: Falls back to UTC if timezone is not available
### User Variables
- **`{{current_user}}`**: Name of the current user
- Example: `John Doe`
- Note: Only replaced if user information is available
## Day of Week Reference
- `0` = Sunday
- `1` = Monday
- `2` = Tuesday
- `3` = Wednesday
- `4` = Thursday
- `5` = Friday
- `6` = Saturday
## Usage Examples
### Agent Instructions
```
You are an AI assistant helping {{current_user}}.
Today's date is {{local_date}}.
Current time in user's timezone: {{local_datetime}}
UTC time: {{current_datetime}}
```
### System Prompts
```
System time (UTC): {{current_datetime}}
User's local time: {{local_datetime}}
Remember to consider the user's timezone when scheduling or discussing times.
```
### Conversation Starters
```
Good morning {{current_user}}! Today is {{local_date}}.
How can I help you today?
```
## Technical Details
### Timezone Detection
The user's timezone is automatically detected from their browser using the JavaScript `Intl.DateTimeFormat().resolvedOptions().timeZone` API. This provides an IANA timezone identifier (e.g., `America/New_York`, `Europe/London`, `Asia/Tokyo`).
### Fallback Behavior
If the timezone cannot be detected or is invalid:
- `{{local_date}}` falls back to `{{current_date}}` (UTC)
- `{{local_datetime}}` falls back to `{{current_datetime}}` (UTC)
### Case Insensitivity
All special variables are case-insensitive. The following are all equivalent:
- `{{current_date}}` = `{{Current_Date}}` = `{{CURRENT_DATE}}`
- `{{local_datetime}}` = `{{Local_DateTime}}` = `{{LOCAL_DATETIME}}`
## Implementation Notes
For developers working with special variables:
1. **Server-side processing**: The `replaceSpecialVars` function in `packages/data-provider/src/parsers.ts` handles all replacements.
2. **Timezone propagation**: The timezone is automatically included in API requests from the client.
3. **Testing**: Comprehensive tests are available in `packages/data-provider/specs/parsers.spec.ts`.
4. **Dependencies**: The implementation uses `dayjs` with `timezone` and `utc` plugins for timezone conversions.
## Migration Guide
If you were using the old UTC-only variables in your prompts, they continue to work exactly as before. The new local timezone variables are additive and don't break existing functionality.
### Upgrading Existing Prompts
```diff
- Current time (UTC): {{current_datetime}}
+ Current time (UTC): {{current_datetime}}
+ Current time (Your timezone): {{local_datetime}}
```
## Future Enhancements
Potential future additions:
- Custom date/time formats
- Relative time expressions (e.g., "tomorrow", "next week")
- User-specified timezone overrides
- Additional locale-aware formatting options

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,8 @@ 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,46 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n
return lastIndex;
}
export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) {
/**
* Replaces special variables in text with their corresponding values.
*
* Available special variables:
* - {{current_date}}: Current date in UTC (YYYY-MM-DD with day of week)
* - {{current_datetime}}: Current datetime in UTC (YYYY-MM-DD HH:mm:ss with day of week)
* - {{iso_datetime}}: Current datetime in ISO 8601 format (UTC)
* - {{local_date}}: Current date in user's local timezone (YYYY-MM-DD with day of week)
* - {{local_datetime}}: Current datetime in user's local timezone (YYYY-MM-DD HH:mm:ss with day of week)
* - {{current_user}}: Name of the current user (if available)
*
* Day of week values: 0=Sunday, 1=Monday, 2=Tuesday, 3=Wednesday, 4=Thursday, 5=Friday, 6=Saturday
*
* Note: {{local_date}} and {{local_datetime}} require the timezone parameter.
* If timezone is not provided, they will fall back to UTC values.
*
* @example
* ```typescript
* replaceSpecialVars({
* text: 'Today is {{local_date}} and it\'s {{current_user}}\'s session',
* user: { name: 'John' },
* timezone: 'America/New_York'
* });
* // Result: "Today is 2024-04-29 (1) and it's John's session"
* ```
*
* @param text - The text containing special variables to replace
* @param user - Optional user object containing user information
* @param timezone - Optional IANA timezone string (e.g., 'America/New_York', 'Europe/London', 'Asia/Tokyo')
* @returns The text with all special variables replaced with their values
*/
export function replaceSpecialVars({
text,
user,
timezone,
}: {
text: string;
user?: t.TUser | null;
timezone?: string;
}) {
let result = text;
if (!result) {
return result;
@@ -428,6 +472,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 {
// 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 =