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:
parent
ab3f027d02
commit
4672754b81
@ -4,7 +4,7 @@ import { FileStat } from '../types'
|
|||||||
* Writes data to a file at the specified path.
|
* Writes data to a file at the specified path.
|
||||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
* @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.
|
* Writes blob data to a file at the specified path.
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { Assistant, AssistantExtension } from '@janhq/core'
|
import { Assistant, AssistantExtension, fs, joinPath } from '@janhq/core'
|
||||||
|
|
||||||
export default class JanAssistantExtension extends AssistantExtension {
|
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.
|
* Called when the extension is unloaded.
|
||||||
@ -9,23 +16,66 @@ export default class JanAssistantExtension extends AssistantExtension {
|
|||||||
onUnload(): void {}
|
onUnload(): void {}
|
||||||
|
|
||||||
async getAssistants(): Promise<Assistant[]> {
|
async getAssistants(): Promise<Assistant[]> {
|
||||||
|
if (!(await fs.existsSync('file://assistants')))
|
||||||
return [this.defaultAssistant]
|
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 createAssistant(assistant: Assistant): Promise<void> {}
|
const assistantPath = await joinPath([
|
||||||
async deleteAssistant(assistant: Assistant): Promise<void> {}
|
'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 = {
|
private defaultAssistant: Assistant = {
|
||||||
avatar: '',
|
avatar: '👋',
|
||||||
thread_location: undefined,
|
thread_location: undefined,
|
||||||
id: 'jan',
|
id: 'jan',
|
||||||
object: 'assistant',
|
object: 'assistant',
|
||||||
created_at: Date.now() / 1000,
|
created_at: Date.now() / 1000,
|
||||||
name: 'Jan',
|
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 user’s behalf.',
|
||||||
model: '*',
|
model: '*',
|
||||||
instructions: '',
|
instructions:
|
||||||
|
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.',
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
type: 'retrieval',
|
type: 'retrieval',
|
||||||
|
|||||||
@ -52,7 +52,6 @@
|
|||||||
"value": true
|
"value": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"key": "caching_enabled",
|
"key": "caching_enabled",
|
||||||
"title": "Caching",
|
"title": "Caching",
|
||||||
|
|||||||
@ -156,7 +156,6 @@ pub fn get_configuration_file_path<R: Runtime>(app_handle: tauri::AppHandle<R>)
|
|||||||
});
|
});
|
||||||
|
|
||||||
let package_name = env!("CARGO_PKG_NAME");
|
let package_name = env!("CARGO_PKG_NAME");
|
||||||
log::debug!("Package name: {}", package_name);
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
let old_data_dir = {
|
let old_data_dir = {
|
||||||
if let Some(config_path) = dirs::config_dir() {
|
if let Some(config_path) = dirs::config_dir() {
|
||||||
|
|||||||
@ -97,6 +97,20 @@ pub fn read_file_sync<R: Runtime>(
|
|||||||
fs::read_to_string(&path).map_err(|e| e.to_string())
|
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]
|
#[tauri::command]
|
||||||
pub fn readdir_sync<R: Runtime>(
|
pub fn readdir_sync<R: Runtime>(
|
||||||
app_handle: tauri::AppHandle<R>,
|
app_handle: tauri::AppHandle<R>,
|
||||||
|
|||||||
@ -30,6 +30,7 @@ pub fn run() {
|
|||||||
core::fs::read_file_sync,
|
core::fs::read_file_sync,
|
||||||
core::fs::rm,
|
core::fs::rm,
|
||||||
core::fs::file_stat,
|
core::fs::file_stat,
|
||||||
|
core::fs::write_file_sync,
|
||||||
// App commands
|
// App commands
|
||||||
core::cmd::get_themes,
|
core::cmd::get_themes,
|
||||||
core::cmd::get_app_configurations,
|
core::cmd::get_app_configurations,
|
||||||
|
|||||||
@ -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 { create } from 'zustand'
|
||||||
import { persist } from 'zustand/middleware'
|
|
||||||
|
|
||||||
interface AssistantState {
|
interface AssistantState {
|
||||||
assistants: Assistant[]
|
assistants: Assistant[]
|
||||||
@ -9,25 +9,30 @@ interface AssistantState {
|
|||||||
updateAssistant: (assistant: Assistant) => void
|
updateAssistant: (assistant: Assistant) => void
|
||||||
deleteAssistant: (id: string) => void
|
deleteAssistant: (id: string) => void
|
||||||
setCurrentAssistant: (assistant: Assistant) => void
|
setCurrentAssistant: (assistant: Assistant) => void
|
||||||
|
setAssistants: (assistants: Assistant[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultAssistant: Assistant = {
|
export const defaultAssistant: Assistant = {
|
||||||
avatar: '👋',
|
|
||||||
id: 'jan',
|
id: 'jan',
|
||||||
name: 'Jan',
|
name: 'Jan',
|
||||||
created_at: 1747029866.542,
|
created_at: 1747029866.542,
|
||||||
description: 'A default assistant that can use all downloaded models.',
|
|
||||||
instructions: '',
|
|
||||||
parameters: {},
|
parameters: {},
|
||||||
|
avatar: '👋',
|
||||||
|
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:
|
||||||
|
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf. Respond naturally and concisely, take actions when needed, and guide the user toward their goals.',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAssistant = create<AssistantState>()(
|
export const useAssistant = create<AssistantState>()((set, get) => ({
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
assistants: [defaultAssistant],
|
assistants: [defaultAssistant],
|
||||||
currentAssistant: defaultAssistant,
|
currentAssistant: defaultAssistant,
|
||||||
addAssistant: (assistant) =>
|
addAssistant: (assistant) => {
|
||||||
set({ assistants: [...get().assistants, assistant] }),
|
set({ assistants: [...get().assistants, assistant] })
|
||||||
|
createAssistant(assistant as unknown as CoreAssistant).catch((error) => {
|
||||||
|
console.error('Failed to create assistant:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
updateAssistant: (assistant) => {
|
updateAssistant: (assistant) => {
|
||||||
const state = get()
|
const state = get()
|
||||||
set({
|
set({
|
||||||
@ -40,15 +45,23 @@ export const useAssistant = create<AssistantState>()(
|
|||||||
? assistant
|
? assistant
|
||||||
: state.currentAssistant,
|
: 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) => {
|
setCurrentAssistant: (assistant) => {
|
||||||
set({ currentAssistant: assistant })
|
set({ currentAssistant: assistant })
|
||||||
},
|
},
|
||||||
}),
|
setAssistants: (assistants) => {
|
||||||
{
|
set({ assistants })
|
||||||
name: localStorageKey.assistant,
|
},
|
||||||
}
|
}))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import { ModelManager } from '@janhq/core'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useMCPServers } from '@/hooks/useMCPServers'
|
import { useMCPServers } from '@/hooks/useMCPServers'
|
||||||
import { getMCPConfig } from '@/services/mcp'
|
import { getMCPConfig } from '@/services/mcp'
|
||||||
|
import { useAssistant } from '@/hooks/useAssistant'
|
||||||
|
import { getAssistants } from '@/services/assistants'
|
||||||
|
|
||||||
export function DataProvider() {
|
export function DataProvider() {
|
||||||
const { setProviders } = useModelProvider()
|
const { setProviders } = useModelProvider()
|
||||||
@ -17,6 +19,7 @@ export function DataProvider() {
|
|||||||
const { setMessages } = useMessages()
|
const { setMessages } = useMessages()
|
||||||
const { checkForUpdate } = useAppUpdater()
|
const { checkForUpdate } = useAppUpdater()
|
||||||
const { setServers } = useMCPServers()
|
const { setServers } = useMCPServers()
|
||||||
|
const { setAssistants } = useAssistant()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchModels().then((models) => {
|
fetchModels().then((models) => {
|
||||||
@ -24,6 +27,9 @@ export function DataProvider() {
|
|||||||
getProviders().then(setProviders)
|
getProviders().then(setProviders)
|
||||||
})
|
})
|
||||||
getMCPConfig().then((data) => setServers(data.mcpServers ?? []))
|
getMCPConfig().then((data) => setServers(data.mcpServers ?? []))
|
||||||
|
getAssistants().then((data) =>
|
||||||
|
setAssistants((data as unknown as Assistant[]) ?? [])
|
||||||
|
)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
32
web-app/src/services/assistants.ts
Normal file
32
web-app/src/services/assistants.ts
Normal 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)
|
||||||
|
}
|
||||||
@ -161,7 +161,12 @@ export const deleteModel = async (id: string) => {
|
|||||||
if (!extension) throw new Error('Model extension not found')
|
if (!extension) throw new Error('Model extension not found')
|
||||||
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to delete model:', error)
|
console.error('Failed to delete model:', error)
|
||||||
throw error
|
throw error
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user