Merge pull request #6205 from kamal/current-date-instruction
feat: support inserting current date into assistant prompt
This commit is contained in:
commit
cd5b8b3e6f
@ -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.',
|
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
|
||||||
model: '*',
|
model: '*',
|
||||||
instructions:
|
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: [
|
tools: [
|
||||||
{
|
{
|
||||||
type: 'retrieval',
|
type: 'retrieval',
|
||||||
|
|||||||
@ -325,6 +325,9 @@ export default function AddEditAssistant({
|
|||||||
className="resize-none"
|
className="resize-none"
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
|
<div className="text-xs text-main-view-fg/60">
|
||||||
|
{t('assistants:instructionsDateHint')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 my-4 mt-6">
|
<div className="space-y-2 my-4 mt-6">
|
||||||
|
|||||||
138
web-app/src/hooks/__tests__/useChat.instructions.test.ts
Normal file
138
web-app/src/hooks/__tests__/useChat.instructions.test.ts
Normal file
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -43,7 +43,7 @@ export const defaultAssistant: Assistant = {
|
|||||||
description:
|
description:
|
||||||
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
|
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
|
||||||
instructions:
|
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<AssistantState>()((set, get) => ({
|
export const useAssistant = create<AssistantState>()((set, get) => ({
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
sendCompletion,
|
sendCompletion,
|
||||||
} from '@/lib/completion'
|
} from '@/lib/completion'
|
||||||
import { CompletionMessagesBuilder } from '@/lib/messages'
|
import { CompletionMessagesBuilder } from '@/lib/messages'
|
||||||
|
import { renderInstructions } from '@/lib/instructionTemplate'
|
||||||
import { ChatCompletionMessageToolCall } from 'openai/resources'
|
import { ChatCompletionMessageToolCall } from 'openai/resources'
|
||||||
import { useAssistant } from './useAssistant'
|
import { useAssistant } from './useAssistant'
|
||||||
|
|
||||||
@ -239,7 +240,7 @@ export const useChat = () => {
|
|||||||
|
|
||||||
const builder = new CompletionMessagesBuilder(
|
const builder = new CompletionMessagesBuilder(
|
||||||
messages,
|
messages,
|
||||||
currentAssistant?.instructions
|
renderInstructions(currentAssistant?.instructions)
|
||||||
)
|
)
|
||||||
if (troubleshooting) builder.addUserMessage(message, attachments)
|
if (troubleshooting) builder.addUserMessage(message, attachments)
|
||||||
|
|
||||||
|
|||||||
27
web-app/src/lib/__tests__/instructionTemplate.test.ts
Normal file
27
web-app/src/lib/__tests__/instructionTemplate.test.ts
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
23
web-app/src/lib/instructionTemplate.ts
Normal file
23
web-app/src/lib/instructionTemplate.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -30,5 +30,6 @@
|
|||||||
"createNew": "Neuen Assistenten anlegen",
|
"createNew": "Neuen Assistenten anlegen",
|
||||||
"personality": "Persönlichkeit",
|
"personality": "Persönlichkeit",
|
||||||
"capabilities": "Fähigkeiten",
|
"capabilities": "Fähigkeiten",
|
||||||
|
"instructionsDateHint": "Tipp: Verwenden Sie {{current_date}}, um das heutige Datum einzufügen.",
|
||||||
"maxToolSteps": "Maximale Werkzeugschritte"
|
"maxToolSteps": "Maximale Werkzeugschritte"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,5 +30,6 @@
|
|||||||
"createNew": "Create New Assistant",
|
"createNew": "Create New Assistant",
|
||||||
"personality": "Personality",
|
"personality": "Personality",
|
||||||
"capabilities": "Capabilities",
|
"capabilities": "Capabilities",
|
||||||
|
"instructionsDateHint": "Tip: Use {{current_date}} to insert today’s date.",
|
||||||
"maxToolSteps": "Max tool steps"
|
"maxToolSteps": "Max tool steps"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,5 +30,6 @@
|
|||||||
"createNew": "Buat Asisten Baru",
|
"createNew": "Buat Asisten Baru",
|
||||||
"personality": "Kepribadian",
|
"personality": "Kepribadian",
|
||||||
"capabilities": "Kemampuan",
|
"capabilities": "Kemampuan",
|
||||||
|
"instructionsDateHint": "Tips: Gunakan {{current_date}} untuk menyisipkan tanggal hari ini.",
|
||||||
"maxToolSteps": "Langkah alat maksimum"
|
"maxToolSteps": "Langkah alat maksimum"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,5 +30,6 @@
|
|||||||
"createNew": "Tạo Trợ lý Mới",
|
"createNew": "Tạo Trợ lý Mới",
|
||||||
"personality": "Tính cách",
|
"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.",
|
||||||
"maxToolSteps": "Bước tối đa của công cụ"
|
"maxToolSteps": "Bước tối đa của công cụ"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,5 +30,6 @@
|
|||||||
"createNew": "创建新助手",
|
"createNew": "创建新助手",
|
||||||
"personality": "个性",
|
"personality": "个性",
|
||||||
"capabilities": "能力",
|
"capabilities": "能力",
|
||||||
|
"instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。",
|
||||||
"maxToolSteps": "最大工具步骤"
|
"maxToolSteps": "最大工具步骤"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,5 +30,6 @@
|
|||||||
"createNew": "建立新助理",
|
"createNew": "建立新助理",
|
||||||
"personality": "個性",
|
"personality": "個性",
|
||||||
"capabilities": "能力",
|
"capabilities": "能力",
|
||||||
|
"instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。",
|
||||||
"maxToolSteps": "最大工具步驟"
|
"maxToolSteps": "最大工具步驟"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ describe('formatDate', () => {
|
|||||||
'2023-12-01T12:00:00Z'
|
'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[0]).toMatch(/Jan.*1.*2023/i)
|
||||||
expect(formatted[1]).toMatch(/Feb.*1.*2023/i)
|
expect(formatted[1]).toMatch(/Feb.*1.*2023/i)
|
||||||
@ -81,4 +81,23 @@ describe('formatDate', () => {
|
|||||||
// Should include abbreviated month name
|
// Should include abbreviated month name
|
||||||
expect(formatted).toMatch(/Jul/i)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@ -1,10 +1,34 @@
|
|||||||
export const formatDate = (date: string | number | Date): string => {
|
type FormatDateOptions = {
|
||||||
return new Date(date).toLocaleString('en-US', {
|
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',
|
year: 'numeric',
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
hour12: true,
|
hour12: true,
|
||||||
|
timeZone: 'UTC',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date-only mode: long month, no timezone adjustment
|
||||||
|
return new Date(date).toLocaleDateString('en-US', {
|
||||||
|
...base,
|
||||||
|
month: 'long',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user