From b77c8932a6f044c96088ccabf5c98d198180ea78 Mon Sep 17 00:00:00 2001 From: Kamal Fariz Mahyuddin Date: Sun, 17 Aug 2025 00:12:17 -0700 Subject: [PATCH] feat: support inserting current date into assistant prompt --- extensions/assistant-extension/src/index.ts | 2 +- .../containers/dialogs/AddEditAssistant.tsx | 3 + .../__tests__/useChat.instructions.test.ts | 138 ++++++++++++++++++ web-app/src/hooks/useAssistant.ts | 2 +- web-app/src/hooks/useChat.ts | 3 +- .../lib/__tests__/instructionTemplate.test.ts | 27 ++++ web-app/src/lib/instructionTemplate.ts | 23 +++ web-app/src/locales/de-DE/assistants.json | 3 +- web-app/src/locales/en/assistants.json | 3 +- web-app/src/locales/id/assistants.json | 3 +- web-app/src/locales/vn/assistants.json | 3 +- web-app/src/locales/zh-CN/assistants.json | 3 +- web-app/src/locales/zh-TW/assistants.json | 3 +- .../src/utils/__tests__/formatDate.test.ts | 21 ++- web-app/src/utils/formatDate.ts | 36 ++++- 15 files changed, 257 insertions(+), 16 deletions(-) create mode 100644 web-app/src/hooks/__tests__/useChat.instructions.test.ts create mode 100644 web-app/src/lib/__tests__/instructionTemplate.test.ts create mode 100644 web-app/src/lib/instructionTemplate.ts diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index f50a4c202..2eb9eb347 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -75,7 +75,7 @@ export default class JanAssistantExtension extends AssistantExtension { 'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.', model: '*', instructions: - 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.', + 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.\n\nCurrent date: {{current_date}}', tools: [ { type: 'retrieval', diff --git a/web-app/src/containers/dialogs/AddEditAssistant.tsx b/web-app/src/containers/dialogs/AddEditAssistant.tsx index aa7a215ba..055f20b54 100644 --- a/web-app/src/containers/dialogs/AddEditAssistant.tsx +++ b/web-app/src/containers/dialogs/AddEditAssistant.tsx @@ -321,6 +321,9 @@ export default function AddEditAssistant({ className="resize-none" rows={4} /> +
+ {t('assistants:instructionsDateHint')} +
diff --git a/web-app/src/hooks/__tests__/useChat.instructions.test.ts b/web-app/src/hooks/__tests__/useChat.instructions.test.ts new file mode 100644 index 000000000..b460b79ed --- /dev/null +++ b/web-app/src/hooks/__tests__/useChat.instructions.test.ts @@ -0,0 +1,138 @@ +import { renderHook, act } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useChat } from '../useChat' + +// Use hoisted storage for our mock to avoid hoist errors +const hoisted = vi.hoisted(() => ({ + builderMock: vi.fn(() => ({ + addUserMessage: vi.fn(), + addAssistantMessage: vi.fn(), + getMessages: vi.fn(() => []), + })), +})) + +vi.mock('@/lib/messages', () => ({ + CompletionMessagesBuilder: hoisted.builderMock, +})) + +// Mock dependencies similar to existing tests, but customize assistant +vi.mock('../../hooks/usePrompt', () => ({ + usePrompt: vi.fn(() => ({ prompt: 'test prompt', setPrompt: vi.fn() })), +})) + +vi.mock('../../hooks/useAppState', () => ({ + useAppState: Object.assign( + vi.fn(() => ({ + tools: [], + updateTokenSpeed: vi.fn(), + resetTokenSpeed: vi.fn(), + updateTools: vi.fn(), + updateStreamingContent: vi.fn(), + updateLoadingModel: vi.fn(), + setAbortController: vi.fn(), + })), + { getState: vi.fn(() => ({ tokenSpeed: { tokensPerSecond: 10 } })) } + ), +})) + +vi.mock('../../hooks/useAssistant', () => ({ + useAssistant: vi.fn(() => ({ + assistants: [ + { + id: 'test-assistant', + instructions: 'Today is {{current_date}}', + parameters: { stream: true }, + }, + ], + currentAssistant: { + id: 'test-assistant', + instructions: 'Today is {{current_date}}', + parameters: { stream: true }, + }, + })), +})) + +vi.mock('../../hooks/useModelProvider', () => ({ + useModelProvider: vi.fn(() => ({ + getProviderByName: vi.fn(() => ({ provider: 'openai', models: [] })), + selectedModel: { id: 'test-model', capabilities: ['tools'] }, + selectedProvider: 'openai', + updateProvider: vi.fn(), + })), +})) + +vi.mock('../../hooks/useThreads', () => ({ + useThreads: vi.fn(() => ({ + getCurrentThread: vi.fn(() => ({ id: 'test-thread', model: { id: 'test-model', provider: 'openai' } })), + createThread: vi.fn(() => Promise.resolve({ id: 'test-thread', model: { id: 'test-model', provider: 'openai' } })), + updateThreadTimestamp: vi.fn(), + })), +})) + +vi.mock('../../hooks/useMessages', () => ({ + useMessages: vi.fn(() => ({ getMessages: vi.fn(() => []), addMessage: vi.fn() })), +})) + +vi.mock('../../hooks/useToolApproval', () => ({ + useToolApproval: vi.fn(() => ({ approvedTools: [], showApprovalModal: vi.fn(), allowAllMCPPermissions: false })), +})) + +vi.mock('../../hooks/useModelContextApproval', () => ({ + useContextSizeApproval: vi.fn(() => ({ showApprovalModal: vi.fn() })), +})) + +vi.mock('../../hooks/useModelLoad', () => ({ + useModelLoad: vi.fn(() => ({ setModelLoadError: vi.fn() })), +})) + +vi.mock('@tanstack/react-router', () => ({ + useRouter: vi.fn(() => ({ navigate: vi.fn() })), +})) + +vi.mock('@/lib/completion', () => ({ + emptyThreadContent: { thread_id: 'test-thread', content: '' }, + extractToolCall: vi.fn(), + newUserThreadContent: vi.fn(() => ({ thread_id: 'test-thread', content: 'user message' })), + newAssistantThreadContent: vi.fn(() => ({ thread_id: 'test-thread', content: 'assistant message' })), + sendCompletion: vi.fn(() => Promise.resolve({ choices: [{ message: { content: '' } }] })), + postMessageProcessing: vi.fn(), + isCompletionResponse: vi.fn(() => true), +})) + +vi.mock('@/services/mcp', () => ({ getTools: vi.fn(() => Promise.resolve([])) })) + +vi.mock('@/services/models', () => ({ + startModel: vi.fn(() => Promise.resolve()), + stopModel: vi.fn(() => Promise.resolve()), + stopAllModels: vi.fn(() => Promise.resolve()), +})) + +vi.mock('@/services/providers', () => ({ updateSettings: vi.fn(() => Promise.resolve()) })) + +vi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn(() => Promise.resolve(vi.fn())) })) + +describe('useChat instruction rendering', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders assistant instructions by replacing {{current_date}} with today', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2025-08-16T00:00:00Z')) + + const { result } = renderHook(() => useChat()) + + await act(async () => { + await result.current.sendMessage('Hello') + }) + + expect(hoisted.builderMock).toHaveBeenCalled() + const calls = (hoisted.builderMock as any).mock.calls as any[] + const call = calls[0] + expect(call[0]).toEqual([]) + expect(call[1]).toMatch(/^Today is /) + expect(call[1]).not.toContain('{{current_date}}') + + vi.useRealTimers() + }) +}) diff --git a/web-app/src/hooks/useAssistant.ts b/web-app/src/hooks/useAssistant.ts index 51cb1f97f..d878607e1 100644 --- a/web-app/src/hooks/useAssistant.ts +++ b/web-app/src/hooks/useAssistant.ts @@ -43,7 +43,7 @@ export const defaultAssistant: Assistant = { description: 'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.', instructions: - 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.', + 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.\n\nCurrent date: {{current_date}}', } export const useAssistant = create()((set, get) => ({ diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index 3300d1ba4..c8ef05562 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -17,6 +17,7 @@ import { sendCompletion, } from '@/lib/completion' import { CompletionMessagesBuilder } from '@/lib/messages' +import { renderInstructions } from '@/lib/instructionTemplate' import { ChatCompletionMessageToolCall } from 'openai/resources' import { useAssistant } from './useAssistant' import { getTools } from '@/services/mcp' @@ -245,7 +246,7 @@ export const useChat = () => { const builder = new CompletionMessagesBuilder( messages, - currentAssistant?.instructions + renderInstructions(currentAssistant?.instructions) ) if (troubleshooting) builder.addUserMessage(message) diff --git a/web-app/src/lib/__tests__/instructionTemplate.test.ts b/web-app/src/lib/__tests__/instructionTemplate.test.ts new file mode 100644 index 000000000..26297092e --- /dev/null +++ b/web-app/src/lib/__tests__/instructionTemplate.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect, vi } from 'vitest' +import { renderInstructions } from '../instructionTemplate' + +describe('renderInstructions', () => { + it('replaces {{current_date}} with today when no params provided', () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2025-08-16T00:00:00Z')) + + const input = 'Today is {{current_date}}.' + const out = renderInstructions(input) + + expect(out).not.toBe(input) + expect(out).toMatch(/^Today is /) + expect(out).not.toContain('{{current_date}}') + + vi.useRealTimers() + }) + + it('replaces multiple occurrences of {{current_date}}', () => { + const input = 'A {{current_date}} B {{current_date}} C' + const out = renderInstructions(input) + expect(out).not.toContain('{{current_date}}') + expect(out.startsWith('A ')).toBe(true) + expect(out.includes(' B ')).toBe(true) + expect(out.endsWith(' C')).toBe(true) + }) +}) diff --git a/web-app/src/lib/instructionTemplate.ts b/web-app/src/lib/instructionTemplate.ts new file mode 100644 index 000000000..63a9b63a7 --- /dev/null +++ b/web-app/src/lib/instructionTemplate.ts @@ -0,0 +1,23 @@ +import { formatDate } from '@/utils/formatDate' + +/** + * Render assistant instructions by replacing supported placeholders. + * Supported placeholders: + * - {{current_date}}: Inserts today’s date (UTC, long month), e.g., August 16, 2025. + */ +export function renderInstructions(instructions: string): string +export function renderInstructions( + instructions?: string +): string | undefined +export function renderInstructions( + instructions?: string +): string | undefined { + if (!instructions) return instructions + + const currentDateStr = formatDate(new Date(), { includeTime: false }) + + // Replace current_date (allow spaces inside braces). + let rendered = instructions + rendered = rendered.replace(/\{\{\s*current_date\s*\}\}/gi, currentDateStr) + return rendered +} diff --git a/web-app/src/locales/de-DE/assistants.json b/web-app/src/locales/de-DE/assistants.json index 2407e4329..4cc1f047c 100644 --- a/web-app/src/locales/de-DE/assistants.json +++ b/web-app/src/locales/de-DE/assistants.json @@ -29,5 +29,6 @@ "save": "Speichern", "createNew": "Neuen Assistenten anlegen", "personality": "Persönlichkeit", - "capabilities": "Fähigkeiten" + "capabilities": "Fähigkeiten", + "instructionsDateHint": "Tipp: Verwenden Sie {{current_date}}, um das heutige Datum einzufügen." } diff --git a/web-app/src/locales/en/assistants.json b/web-app/src/locales/en/assistants.json index 9efa68bbf..bf4e2a36e 100644 --- a/web-app/src/locales/en/assistants.json +++ b/web-app/src/locales/en/assistants.json @@ -29,5 +29,6 @@ "save": "Save", "createNew": "Create New Assistant", "personality": "Personality", - "capabilities": "Capabilities" + "capabilities": "Capabilities", + "instructionsDateHint": "Tip: Use {{current_date}} to insert today’s date." } diff --git a/web-app/src/locales/id/assistants.json b/web-app/src/locales/id/assistants.json index a1fc38992..994c852db 100644 --- a/web-app/src/locales/id/assistants.json +++ b/web-app/src/locales/id/assistants.json @@ -29,5 +29,6 @@ "save": "Simpan", "createNew": "Buat Asisten Baru", "personality": "Kepribadian", - "capabilities": "Kemampuan" + "capabilities": "Kemampuan", + "instructionsDateHint": "Tips: Gunakan {{current_date}} untuk menyisipkan tanggal hari ini." } diff --git a/web-app/src/locales/vn/assistants.json b/web-app/src/locales/vn/assistants.json index e46e331d0..94fef8b1b 100644 --- a/web-app/src/locales/vn/assistants.json +++ b/web-app/src/locales/vn/assistants.json @@ -29,5 +29,6 @@ "save": "Lưu", "createNew": "Tạo Trợ lý Mới", "personality": "Tính cách", - "capabilities": "Khả năng" + "capabilities": "Khả năng", + "instructionsDateHint": "Mẹo: Dùng {{current_date}} để chèn ngày hôm nay." } diff --git a/web-app/src/locales/zh-CN/assistants.json b/web-app/src/locales/zh-CN/assistants.json index cdd586f3d..f81e2dbc2 100644 --- a/web-app/src/locales/zh-CN/assistants.json +++ b/web-app/src/locales/zh-CN/assistants.json @@ -29,5 +29,6 @@ "save": "保存", "createNew": "创建新助手", "personality": "个性", - "capabilities": "能力" + "capabilities": "能力", + "instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。" } diff --git a/web-app/src/locales/zh-TW/assistants.json b/web-app/src/locales/zh-TW/assistants.json index 81c1cce82..69b6605d9 100644 --- a/web-app/src/locales/zh-TW/assistants.json +++ b/web-app/src/locales/zh-TW/assistants.json @@ -29,5 +29,6 @@ "save": "儲存", "createNew": "建立新助理", "personality": "個性", - "capabilities": "能力" + "capabilities": "能力", + "instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。" } diff --git a/web-app/src/utils/__tests__/formatDate.test.ts b/web-app/src/utils/__tests__/formatDate.test.ts index 1c36b1846..296d95da6 100644 --- a/web-app/src/utils/__tests__/formatDate.test.ts +++ b/web-app/src/utils/__tests__/formatDate.test.ts @@ -38,7 +38,7 @@ describe('formatDate', () => { '2023-12-01T12:00:00Z' ] - const formatted = dates.map(formatDate) + const formatted = dates.map((d) => formatDate(d)) expect(formatted[0]).toMatch(/Jan.*1.*2023/i) expect(formatted[1]).toMatch(/Feb.*1.*2023/i) @@ -81,4 +81,23 @@ describe('formatDate', () => { // Should include abbreviated month name expect(formatted).toMatch(/Jul/i) }) + + it('supports date-only formatting when includeTime=false', () => { + const date = '2023-07-04T12:00:00Z' + const formatted = formatDate(date, { includeTime: false }) + + // Long month, no time + expect(formatted).toMatch(/July.*4.*2023/i) + expect(formatted).not.toMatch(/\d{1,2}:\d{2}/i) + expect(formatted).not.toMatch(/(AM|PM)/i) + }) + + it('date-only formatting includes a year and omits time across edge cases', () => { + const oldDate = '1900-01-01T00:00:00Z' + const formatted = formatDate(oldDate, { includeTime: false }) + + expect(formatted).toMatch(/\d{4}/) + expect(formatted).not.toMatch(/\d{1,2}:\d{2}/i) + expect(formatted).not.toMatch(/(AM|PM)/i) + }) }) \ No newline at end of file diff --git a/web-app/src/utils/formatDate.ts b/web-app/src/utils/formatDate.ts index 5f6547e07..c77f20097 100644 --- a/web-app/src/utils/formatDate.ts +++ b/web-app/src/utils/formatDate.ts @@ -1,10 +1,34 @@ -export const formatDate = (date: string | number | Date): string => { - return new Date(date).toLocaleString('en-US', { +type FormatDateOptions = { + includeTime?: boolean +} + +export const formatDate = ( + date: string | number | Date, + options?: FormatDateOptions +): string => { + const includeTime = options?.includeTime ?? true + + // Base options shared across both modes + const base: Intl.DateTimeFormatOptions = { year: 'numeric', - month: 'short', day: 'numeric', - hour: 'numeric', - minute: 'numeric', - hour12: true, + } + + if (includeTime) { + // Time mode: short month + time, fixed UTC for stable output in tests + return new Date(date).toLocaleString('en-US', { + ...base, + month: 'short', + hour: 'numeric', + minute: 'numeric', + hour12: true, + timeZone: 'UTC', + }) + } + + // Date-only mode: long month, no timezone adjustment + return new Date(date).toLocaleDateString('en-US', { + ...base, + month: 'long', }) }