chore: persist assistants settings (#5127)

* chore: assistant settings

* chore: fix model sources issue after deleted models

* chore: assistants as files

* chore: clean up
This commit is contained in:
Louis 2025-05-28 19:33:13 +07:00 committed by GitHub
parent ab3f027d02
commit 4672754b81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 169 additions and 50 deletions

View File

@ -4,7 +4,7 @@ import { FileStat } from '../types'
* Writes data to a file at the specified path.
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
*/
const writeFileSync = (...args: any[]) => globalThis.core.api?.writeFileSync(...args)
const writeFileSync = (...args: any[]) => globalThis.core.api?.writeFileSync({ args })
/**
* Writes blob data to a file at the specified path.

View File

@ -1,7 +1,14 @@
import { Assistant, AssistantExtension } from '@janhq/core'
import { Assistant, AssistantExtension, fs, joinPath } from '@janhq/core'
export default class JanAssistantExtension extends AssistantExtension {
async onLoad() {}
async onLoad() {
if (!(await fs.existsSync('file://assistants'))) {
await fs.mkdir('file://assistants')
}
const assistants = await this.getAssistants()
if (assistants.length === 0) {
await this.createAssistant(this.defaultAssistant)
}
}
/**
* Called when the extension is unloaded.
@ -9,23 +16,66 @@ export default class JanAssistantExtension extends AssistantExtension {
onUnload(): void {}
async getAssistants(): Promise<Assistant[]> {
if (!(await fs.existsSync('file://assistants')))
return [this.defaultAssistant]
const assistants = await fs.readdirSync('file://assistants')
const assistantsData: Assistant[] = []
for (const assistant of assistants) {
const assistantPath = await joinPath([
'file://assistants',
assistant,
'assistant.json',
])
if (!(await fs.existsSync(assistantPath))) {
console.warn(`Assistant file not found: ${assistantPath}`)
continue
}
try {
const assistantData = JSON.parse(await fs.readFileSync(assistantPath))
assistantsData.push(assistantData as Assistant)
} catch (error) {
console.error(`Failed to read assistant ${assistant}:`, error)
}
}
return assistantsData
}
/** DEPRECATED */
async createAssistant(assistant: Assistant): Promise<void> {}
async deleteAssistant(assistant: Assistant): Promise<void> {}
async createAssistant(assistant: Assistant): Promise<void> {
const assistantPath = await joinPath([
'file://assistants',
assistant.id,
'assistant.json',
])
const assistantFolder = await joinPath(['file://assistants', assistant.id])
if (!(await fs.existsSync(assistantFolder))) {
await fs.mkdir(assistantFolder)
}
await fs.writeFileSync(assistantPath, JSON.stringify(assistant, null, 2))
}
async deleteAssistant(assistant: Assistant): Promise<void> {
const assistantPath = await joinPath([
'file://assistants',
assistant.id,
'assistant.json',
])
if (await fs.existsSync(assistantPath)) {
await fs.unlinkSync(assistantPath)
}
}
private defaultAssistant: Assistant = {
avatar: '',
avatar: '👋',
thread_location: undefined,
id: 'jan',
object: 'assistant',
created_at: Date.now() / 1000,
name: 'Jan',
description: 'A default assistant that can use all downloaded models',
description:
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the users behalf.',
model: '*',
instructions: '',
instructions:
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the users behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.',
tools: [
{
type: 'retrieval',

View File

@ -52,7 +52,6 @@
"value": true
}
},
{
"key": "caching_enabled",
"title": "Caching",

View File

@ -156,7 +156,6 @@ pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>)
});
let package_name = env!("CARGO_PKG_NAME");
log::debug!("Package name: {}", package_name);
#[cfg(target_os = "linux")]
let old_data_dir = {
if let Some(config_path) = dirs::config_dir() {

View File

@ -97,6 +97,20 @@ pub fn read_file_sync<R: Runtime>(
fs::read_to_string(&path).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn write_file_sync<R: Runtime>(
app_handle: tauri::AppHandle<R>,
args: Vec<String>,
) -> Result<(), String> {
if args.len() < 2 || args[0].is_empty() || args[1].is_empty() {
return Err("write_file_sync error: Invalid argument".to_string());
}
let path = resolve_path(app_handle, &args[0]);
let content = &args[1];
fs::write(&path, content).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn readdir_sync<R: Runtime>(
app_handle: tauri::AppHandle<R>,

View File

@ -30,6 +30,7 @@ pub fn run() {
core::fs::read_file_sync,
core::fs::rm,
core::fs::file_stat,
core::fs::write_file_sync,
// App commands
core::cmd::get_themes,
core::cmd::get_app_configurations,

View File

@ -1,6 +1,6 @@
import { localStorageKey } from '@/constants/localStorage'
import { createAssistant, deleteAssistant } from '@/services/assistants'
import { Assistant as CoreAssistant } from '@janhq/core'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface AssistantState {
assistants: Assistant[]
@ -9,25 +9,30 @@ interface AssistantState {
updateAssistant: (assistant: Assistant) => void
deleteAssistant: (id: string) => void
setCurrentAssistant: (assistant: Assistant) => void
setAssistants: (assistants: Assistant[]) => void
}
export const defaultAssistant: Assistant = {
avatar: '👋',
id: 'jan',
name: 'Jan',
created_at: 1747029866.542,
description: 'A default assistant that can use all downloaded models.',
instructions: '',
parameters: {},
avatar: '👋',
description:
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the users behalf.',
instructions:
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the users behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.',
}
export const useAssistant = create<AssistantState>()(
persist(
(set, get) => ({
export const useAssistant = create<AssistantState>()((set, get) => ({
assistants: [defaultAssistant],
currentAssistant: defaultAssistant,
addAssistant: (assistant) =>
set({ assistants: [...get().assistants, assistant] }),
addAssistant: (assistant) => {
set({ assistants: [...get().assistants, assistant] })
createAssistant(assistant as unknown as CoreAssistant).catch((error) => {
console.error('Failed to create assistant:', error)
})
},
updateAssistant: (assistant) => {
const state = get()
set({
@ -40,15 +45,23 @@ export const useAssistant = create<AssistantState>()(
? assistant
: state.currentAssistant,
})
// Create assistant already cover update logic
createAssistant(assistant as unknown as CoreAssistant).catch((error) => {
console.error('Failed to update assistant:', error)
})
},
deleteAssistant: (id) => {
deleteAssistant(
get().assistants.find((e) => e.id === id) as unknown as CoreAssistant
).catch((error) => {
console.error('Failed to delete assistant:', error)
})
set({ assistants: get().assistants.filter((a) => a.id !== id) })
},
deleteAssistant: (id) =>
set({ assistants: get().assistants.filter((a) => a.id !== id) }),
setCurrentAssistant: (assistant) => {
set({ currentAssistant: assistant })
},
}),
{
name: localStorageKey.assistant,
}
)
)
setAssistants: (assistants) => {
set({ assistants })
},
}))

View File

@ -10,6 +10,8 @@ import { ModelManager } from '@janhq/core'
import { useEffect } from 'react'
import { useMCPServers } from '@/hooks/useMCPServers'
import { getMCPConfig } from '@/services/mcp'
import { useAssistant } from '@/hooks/useAssistant'
import { getAssistants } from '@/services/assistants'
export function DataProvider() {
const { setProviders } = useModelProvider()
@ -17,6 +19,7 @@ export function DataProvider() {
const { setMessages } = useMessages()
const { checkForUpdate } = useAppUpdater()
const { setServers } = useMCPServers()
const { setAssistants } = useAssistant()
useEffect(() => {
fetchModels().then((models) => {
@ -24,6 +27,9 @@ export function DataProvider() {
getProviders().then(setProviders)
})
getMCPConfig().then((data) => setServers(data.mcpServers ?? []))
getAssistants().then((data) =>
setAssistants((data as unknown as Assistant[]) ?? [])
)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

View File

@ -0,0 +1,32 @@
import { ExtensionManager } from '@/lib/extension'
import { Assistant, AssistantExtension, ExtensionTypeEnum } from '@janhq/core'
/**
* Fetches all available assistants.
* @returns A promise that resolves to the assistants.
*/
export const getAssistants = async () => {
return ExtensionManager.getInstance()
.get<AssistantExtension>(ExtensionTypeEnum.Assistant)
?.getAssistants()
}
/**
* Creates a new assistant.
* @param assistant The assistant to create.
*/
export const createAssistant = async (assistant: Assistant) => {
return ExtensionManager.getInstance()
.get<AssistantExtension>(ExtensionTypeEnum.Assistant)
?.createAssistant(assistant)
}
/**
* Deletes an existing assistant.
* @param assistant The assistant to delete.
* @return A promise that resolves when the assistant is deleted.
*/
export const deleteAssistant = async (assistant: Assistant) => {
return ExtensionManager.getInstance()
.get<AssistantExtension>(ExtensionTypeEnum.Assistant)
?.deleteAssistant(assistant)
}

View File

@ -161,7 +161,12 @@ export const deleteModel = async (id: string) => {
if (!extension) throw new Error('Model extension not found')
try {
return await extension.deleteModel(id)
return await extension.deleteModel(id).then(() => {
// TODO: This should be removed when we integrate new llama.cpp extension
if (id.includes(':')) {
extension.addSource(`cortexso/${id.split(':')[0]}`)
}
})
} catch (error) {
console.error('Failed to delete model:', error)
throw error