Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/assistant-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions web-app/src/containers/dialogs/AddEditAssistant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,9 @@ export default function AddEditAssistant({
className="resize-none"
rows={4}
/>
<div className="text-xs text-main-view-fg/60">
{t('assistants:instructionsDateHint')}
</div>
</div>

<div className="space-y-2 my-4">
Expand Down
138 changes: 138 additions & 0 deletions web-app/src/hooks/__tests__/useChat.instructions.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
2 changes: 1 addition & 1 deletion web-app/src/hooks/useAssistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssistantState>()((set, get) => ({
Expand Down
3 changes: 2 additions & 1 deletion web-app/src/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -229,7 +230,7 @@ export const useChat = () => {

const builder = new CompletionMessagesBuilder(
messages,
currentAssistant?.instructions
renderInstructions(currentAssistant?.instructions)
)
if (troubleshooting) builder.addUserMessage(message)

Expand Down
27 changes: 27 additions & 0 deletions web-app/src/lib/__tests__/instructionTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions web-app/src/lib/instructionTemplate.ts
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 2 additions & 1 deletion web-app/src/locales/de-DE/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
3 changes: 2 additions & 1 deletion web-app/src/locales/en/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
3 changes: 2 additions & 1 deletion web-app/src/locales/id/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
3 changes: 2 additions & 1 deletion web-app/src/locales/vn/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
3 changes: 2 additions & 1 deletion web-app/src/locales/zh-CN/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
"save": "保存",
"createNew": "创建新助手",
"personality": "个性",
"capabilities": "能力"
"capabilities": "能力",
"instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。"
}
3 changes: 2 additions & 1 deletion web-app/src/locales/zh-TW/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
"save": "儲存",
"createNew": "建立新助理",
"personality": "個性",
"capabilities": "能力"
"capabilities": "能力",
"instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。"
}
21 changes: 20 additions & 1 deletion web-app/src/utils/__tests__/formatDate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
})
36 changes: 30 additions & 6 deletions web-app/src/utils/formatDate.ts
Original file line number Diff line number Diff line change
@@ -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',
})
}
Loading