Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -325,6 +325,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 mt-6">
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 @@ -239,7 +240,7 @@ export const useChat = () => {

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

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
}
1 change: 1 addition & 0 deletions web-app/src/locales/de-DE/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"createNew": "Neuen Assistenten anlegen",
"personality": "Persönlichkeit",
"capabilities": "Fähigkeiten",
"instructionsDateHint": "Tipp: Verwenden Sie {{current_date}}, um das heutige Datum einzufügen.",
"maxToolSteps": "Maximale Werkzeugschritte"
}
1 change: 1 addition & 0 deletions web-app/src/locales/en/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"createNew": "Create New Assistant",
"personality": "Personality",
"capabilities": "Capabilities",
"instructionsDateHint": "Tip: Use {{current_date}} to insert today’s date.",
"maxToolSteps": "Max tool steps"
}
1 change: 1 addition & 0 deletions web-app/src/locales/id/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"createNew": "Buat Asisten Baru",
"personality": "Kepribadian",
"capabilities": "Kemampuan",
"instructionsDateHint": "Tips: Gunakan {{current_date}} untuk menyisipkan tanggal hari ini.",
"maxToolSteps": "Langkah alat maksimum"
}
1 change: 1 addition & 0 deletions web-app/src/locales/vn/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"createNew": "Tạo Trợ lý Mới",
"personality": "Tính cách",
"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ụ"
}
1 change: 1 addition & 0 deletions web-app/src/locales/zh-CN/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"createNew": "创建新助手",
"personality": "个性",
"capabilities": "能力",
"instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。",
"maxToolSteps": "最大工具步骤"
}
1 change: 1 addition & 0 deletions web-app/src/locales/zh-TW/assistants.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"createNew": "建立新助理",
"personality": "個性",
"capabilities": "能力",
"instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。",
"maxToolSteps": "最大工具步驟"
}
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