Skip to content

Commit 2dac53e

Browse files
authored
feat: Jan supports multiple assistants (#5024)
* feat: Jan supports multiple assistants * chore: persists current assistant to threads.json * chore: update assistant persistence * chore: simplify persistence objects
1 parent f643354 commit 2dac53e

File tree

11 files changed

+160
-45
lines changed

11 files changed

+160
-45
lines changed

core/src/types/thread/threadEntity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export type Thread = {
2727
* @stored
2828
*/
2929
export type ThreadAssistantInfo = {
30-
assistant_id: string
31-
assistant_name: string
30+
id: string
31+
name: string
3232
model: ModelInfo
3333
instructions?: string
3434
tools?: AssistantTool[]

src-tauri/src/core/threads.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ pub struct ImageContentValue {
9797

9898
#[derive(Debug, Serialize, Deserialize, Clone)]
9999
pub struct ThreadAssistantInfo {
100-
pub assistant_id: String,
101-
pub assistant_name: String,
100+
pub id: String,
101+
pub name: String,
102102
pub model: ModelInfo,
103103
pub instructions: Option<String>,
104104
pub tools: Option<Vec<AssistantTool>>,
@@ -456,16 +456,16 @@ pub async fn modify_thread_assistant<R: Runtime>(
456456
serde_json::from_str(&data).map_err(|e| e.to_string())?
457457
};
458458
let assistant_id = assistant
459-
.get("assistant_id")
459+
.get("id")
460460
.and_then(|v| v.as_str())
461-
.ok_or("Missing assistant_id")?;
461+
.ok_or("Missing id")?;
462462
if let Some(assistants) = thread
463463
.get_mut("assistants")
464464
.and_then(|a: &mut serde_json::Value| a.as_array_mut())
465465
{
466466
if let Some(index) = assistants
467467
.iter()
468-
.position(|a| a.get("assistant_id").and_then(|v| v.as_str()) == Some(assistant_id))
468+
.position(|a| a.get("id").and_then(|v| v.as_str()) == Some(assistant_id))
469469
{
470470
assistants[index] = assistant.clone();
471471
let data = serde_json::to_string_pretty(&thread).map_err(|e| e.to_string())?;

web-app/src/containers/DropdownAssistant.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,25 @@ import {
99
import { useAssistant } from '@/hooks/useAssistant'
1010
import AddEditAssistant from './dialogs/AddEditAssistant'
1111
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
12+
import { useThreads } from '@/hooks/useThreads'
1213

1314
const DropdownAssistant = () => {
14-
const { assistants, addAssistant, updateAssistant } = useAssistant()
15+
const {
16+
assistants,
17+
currentAssistant,
18+
addAssistant,
19+
updateAssistant,
20+
setCurrentAssistant,
21+
} = useAssistant()
22+
const { updateCurrentThreadAssistant } = useThreads()
1523
const [dropdownOpen, setDropdownOpen] = useState(false)
1624
const [dialogOpen, setDialogOpen] = useState(false)
1725
const [editingAssistantId, setEditingAssistantId] = useState<string | null>(
1826
null
1927
)
20-
const [selectedAssistantId, setSelectedAssistantId] = useState<string | null>(
21-
assistants[0]?.id || null
22-
)
2328

2429
const selectedAssistant =
25-
assistants.find((a) => a.id === selectedAssistantId) || assistants[0]
30+
assistants.find((a) => a.id === currentAssistant.id) || assistants[0]
2631

2732
return (
2833
<>
@@ -63,7 +68,10 @@ const DropdownAssistant = () => {
6368
<DropdownMenuItem className="flex justify-between items-center">
6469
<span
6570
className="truncate text-main-view-fg/70 flex-1 cursor-pointer"
66-
onClick={() => setSelectedAssistantId(assistant.id)}
71+
onClick={() => {
72+
setCurrentAssistant(assistant)
73+
updateCurrentThreadAssistant(assistant)
74+
}}
6775
>
6876
{assistant.name}
6977
</span>

web-app/src/hooks/useAssistant.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,17 @@ import { localStoregeKey } from '@/constants/localStorage'
22
import { create } from 'zustand'
33
import { persist } from 'zustand/middleware'
44

5-
export type Assistant = {
6-
avatar?: string
7-
id: string
8-
name: string
9-
created_at: number
10-
description?: string
11-
instructions: string
12-
parameters: Record<string, unknown>
13-
}
145

156
interface AssistantState {
167
assistants: Assistant[]
8+
currentAssistant: Assistant
179
addAssistant: (assistant: Assistant) => void
1810
updateAssistant: (assistant: Assistant) => void
1911
deleteAssistant: (id: string) => void
12+
setCurrentAssistant: (assistant: Assistant) => void
2013
}
2114

22-
const defaultAssistant: Assistant = {
15+
export const defaultAssistant: Assistant = {
2316
avatar: '',
2417
id: 'jan',
2518
name: 'Jan',
@@ -33,6 +26,7 @@ export const useAssistant = create<AssistantState>()(
3326
persist(
3427
(set, get) => ({
3528
assistants: [defaultAssistant],
29+
currentAssistant: defaultAssistant,
3630
addAssistant: (assistant) =>
3731
set({ assistants: [...get().assistants, assistant] }),
3832
updateAssistant: (assistant) =>
@@ -43,6 +37,9 @@ export const useAssistant = create<AssistantState>()(
4337
}),
4438
deleteAssistant: (id) =>
4539
set({ assistants: get().assistants.filter((a) => a.id !== id) }),
40+
setCurrentAssistant: (assistant) => {
41+
set({ currentAssistant: assistant })
42+
},
4643
}),
4744
{
4845
name: localStoregeKey.assistant,

web-app/src/hooks/useChat.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import {
1818
} from '@/lib/completion'
1919
import { CompletionMessagesBuilder } from '@/lib/messages'
2020
import { ChatCompletionMessageToolCall } from 'openai/resources'
21+
import { useAssistant } from './useAssistant'
2122

2223
export const useChat = () => {
2324
const { prompt, setPrompt } = usePrompt()
2425
const { tools } = useAppState()
26+
const { currentAssistant } = useAssistant()
2527

2628
const { getProviderByName, selectedModel, selectedProvider } =
2729
useModelProvider()
@@ -43,7 +45,8 @@ export const useChat = () => {
4345
id: selectedModel?.id ?? defaultModel(selectedProvider),
4446
provider: selectedProvider,
4547
},
46-
prompt
48+
prompt,
49+
currentAssistant
4750
)
4851
router.navigate({
4952
to: route.threadsDetail,
@@ -58,6 +61,7 @@ export const useChat = () => {
5861
router,
5962
selectedModel?.id,
6063
selectedProvider,
64+
currentAssistant,
6165
])
6266

6367
const sendMessage = useCallback(
@@ -79,6 +83,8 @@ export const useChat = () => {
7983
}
8084

8185
const builder = new CompletionMessagesBuilder()
86+
if (currentAssistant?.instructions?.length > 0)
87+
builder.addSystemMessage(currentAssistant?.instructions || '')
8288
// REMARK: Would it possible to not attach the entire message history to the request?
8389
// TODO: If not amend messages history here
8490
builder.addUserMessage(message)
@@ -143,9 +149,10 @@ export const useChat = () => {
143149
addMessage,
144150
setPrompt,
145151
selectedModel,
146-
tools,
152+
currentAssistant?.instructions,
147153
setAbortController,
148154
updateLoadingModel,
155+
tools,
149156
]
150157
)
151158

web-app/src/hooks/useThreads.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ type ThreadState = {
1717
deleteAllThreads: () => void
1818
unstarAllThreads: () => void
1919
setCurrentThreadId: (threadId?: string) => void
20-
createThread: (model: ThreadModel, title?: string) => Promise<Thread>
20+
createThread: (
21+
model: ThreadModel,
22+
title?: string,
23+
assistant?: Assistant
24+
) => Promise<Thread>
2125
updateCurrentThreadModel: (model: ThreadModel) => void
2226
getFilteredThreads: (searchTerm: string) => Thread[]
27+
updateCurrentThreadAssistant: (assistant: Assistant) => void
2328
searchIndex: Fuse<Thread> | null
2429
}
2530

@@ -152,18 +157,18 @@ export const useThreads = create<ThreadState>()(
152157
setCurrentThreadId: (threadId) => {
153158
set({ currentThreadId: threadId })
154159
},
155-
createThread: async (model, title) => {
160+
createThread: async (model, title, assistant) => {
156161
const newThread: Thread = {
157162
id: ulid(),
158163
title: title ?? 'New Thread',
159164
model,
160165
order: 1,
161166
updated: Date.now() / 1000,
167+
assistants: assistant ? [assistant] : [],
162168
}
163169
set((state) => ({
164170
searchIndex: new Fuse(Object.values(state.threads), fuseOptions),
165171
}))
166-
console.log('newThread', newThread)
167172
return await createThread(newThread).then((createdThread) => {
168173
set((state) => ({
169174
threads: {
@@ -175,6 +180,26 @@ export const useThreads = create<ThreadState>()(
175180
return createdThread
176181
})
177182
},
183+
updateCurrentThreadAssistant: (assistant) => {
184+
set((state) => {
185+
if (!state.currentThreadId) return { ...state }
186+
const currentThread = state.getCurrentThread()
187+
if (currentThread)
188+
updateThread({
189+
...currentThread,
190+
assistants: [{ ...assistant, model: currentThread.model }],
191+
})
192+
return {
193+
threads: {
194+
...state.threads,
195+
[state.currentThreadId as string]: {
196+
...state.threads[state.currentThreadId as string],
197+
assistants: [assistant],
198+
},
199+
},
200+
}
201+
})
202+
},
178203
updateCurrentThreadModel: (model) => {
179204
set((state) => {
180205
if (!state.currentThreadId) return { ...state }

web-app/src/lib/messages.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,61 @@
11
import { ChatCompletionMessageParam } from 'token.js'
22
import { ChatCompletionMessageToolCall } from 'openai/resources'
33

4+
/**
5+
* @fileoverview Helper functions for creating chat completion request.
6+
* These functions are used to create chat completion request objects
7+
*/
48
export class CompletionMessagesBuilder {
59
private messages: ChatCompletionMessageParam[] = []
610

711
constructor() {}
812

13+
/**
14+
* Add a system message to the messages array.
15+
* @param content - The content of the system message.
16+
*/
17+
addSystemMessage(content: string) {
18+
this.messages.push({
19+
role: 'system',
20+
content: content,
21+
})
22+
}
23+
24+
/**
25+
* Add a user message to the messages array.
26+
* @param content - The content of the user message.
27+
*/
928
addUserMessage(content: string) {
1029
this.messages.push({
1130
role: 'user',
1231
content: content,
1332
})
1433
}
1534

16-
addAssistantMessage(content: string, refusal?: string, calls?: ChatCompletionMessageToolCall[]) {
35+
/**
36+
* Add an assistant message to the messages array.
37+
* @param content - The content of the assistant message.
38+
* @param refusal - Optional refusal message.
39+
* @param calls - Optional tool calls associated with the message.
40+
*/
41+
addAssistantMessage(
42+
content: string,
43+
refusal?: string,
44+
calls?: ChatCompletionMessageToolCall[]
45+
) {
1746
this.messages.push({
1847
role: 'assistant',
1948
content: content,
2049
refusal: refusal,
21-
tool_calls: calls
50+
tool_calls: calls,
2251
})
2352
}
2453

54+
/**
55+
* Add a tool message to the messages array.
56+
* @param content - The content of the tool message.
57+
* @param toolCallId - The ID of the tool call associated with the message.
58+
*/
2559
addToolMessage(content: string, toolCallId: string) {
2660
this.messages.push({
2761
role: 'tool',
@@ -30,6 +64,10 @@ export class CompletionMessagesBuilder {
3064
})
3165
}
3266

67+
/**
68+
* Return the messages array.
69+
* @returns The array of chat completion messages.
70+
*/
3371
getMessages(): ChatCompletionMessageParam[] {
3472
return this.messages
3573
}

web-app/src/routes/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ type SearchParams = {
1515
}
1616
}
1717
import DropdownAssistant from '@/containers/DropdownAssistant'
18+
import { useEffect } from 'react'
19+
import { useThreads } from '@/hooks/useThreads'
1820

1921
export const Route = createFileRoute(route.home as any)({
2022
component: Index,
@@ -28,6 +30,7 @@ function Index() {
2830
const { providers } = useModelProvider()
2931
const search = useSearch({ from: route.home as any })
3032
const selectedModel = search.model
33+
const { setCurrentThreadId } = useThreads()
3134

3235
// Conditional to check if there are any valid providers
3336
// required min 1 api_key or 1 model in llama.cpp
@@ -37,6 +40,10 @@ function Index() {
3740
(provider.provider === 'llama.cpp' && provider.models.length)
3841
)
3942

43+
useEffect(() => {
44+
setCurrentThreadId(undefined)
45+
}, [setCurrentThreadId])
46+
4047
if (!hasValidProviders) {
4148
return <SetupScreen />
4249
}

web-app/src/routes/threads/$threadId.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useMessages } from '@/hooks/useMessages'
1616
import { fetchMessages } from '@/services/messages'
1717
import { useAppState } from '@/hooks/useAppState'
1818
import DropdownAssistant from '@/containers/DropdownAssistant'
19+
import { useAssistant } from '@/hooks/useAssistant'
1920

2021
// as route.threadsDetail
2122
export const Route = createFileRoute('/threads/$threadId')({
@@ -28,6 +29,7 @@ function ThreadDetail() {
2829
const [isAtBottom, setIsAtBottom] = useState(true)
2930
const lastScrollTopRef = useRef(0)
3031
const { currentThreadId, getThreadById, setCurrentThreadId } = useThreads()
32+
const { setCurrentAssistant, assistants } = useAssistant()
3133
const { setMessages } = useMessages()
3234
const { streamingContent, loadingModel } = useAppState()
3335

@@ -45,9 +47,16 @@ function ThreadDetail() {
4547
const isFirstRender = useRef(true)
4648

4749
useEffect(() => {
48-
if (currentThreadId !== threadId) setCurrentThreadId(threadId)
50+
if (currentThreadId !== threadId) {
51+
setCurrentThreadId(threadId)
52+
const assistant = assistants.find(
53+
(assistant) => assistant.id === thread?.assistants?.[0]?.id
54+
)
55+
if (assistant) setCurrentAssistant(assistant)
56+
}
57+
4958
// eslint-disable-next-line react-hooks/exhaustive-deps
50-
}, [threadId, currentThreadId])
59+
}, [threadId, currentThreadId, assistants])
5160

5261
useEffect(() => {
5362
fetchMessages(threadId).then((fetchedMessages) => {

0 commit comments

Comments
 (0)