Skip to content

feat: settings store #238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 20, 2025
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
1 change: 1 addition & 0 deletions apps/agent-tars/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"rimraf": "^6.0.1",
"autoprefixer": "10.4.20",
"electron": "34.1.1",
"electron-store": "^10.0.0",
"electron-packager-languages": "0.6.0",
"electron-vite": "^3.0.0",
"jsdom": "^26.0.0",
Expand Down
11 changes: 4 additions & 7 deletions apps/agent-tars/src/main/customTools/search.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { SearchProvider, SearchSettings, ToolCall } from '@agent-infra/shared';
import { SearchProvider, ToolCall } from '@agent-infra/shared';
import {
SearchClient,
SearchProvider as SearchProviderEnum,
} from '@agent-infra/search';
import { MCPToolResult } from '@main/type';
import { tavily as tavilyCore } from '@tavily/core';
import { SettingStore } from '@main/store/setting';

export const tavily = tavilyCore;

let currentSearchConfig: SearchSettings | null = null;

export function updateSearchConfig(config: SearchSettings) {
currentSearchConfig = config;
}

const searchByTavily = async (options: { count: number; query: string }) => {
const currentSearchConfig = SettingStore.get('search');
const client = tavily({
apiKey: process.env.TAVILY_API_KEY || currentSearchConfig?.apiKey,
});
Expand All @@ -33,6 +29,7 @@ const searchByTavily = async (options: { count: number; query: string }) => {
};

export async function search(toolCall: ToolCall): Promise<MCPToolResult> {
const currentSearchConfig = SettingStore.get('search');
const args = JSON.parse(toolCall.function.arguments);

try {
Expand Down
1 change: 1 addition & 0 deletions apps/agent-tars/src/main/ipcRoutes/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const actionRoute = t.router({
listTools: t.procedure.handle(async () => {
const mcpClient = await createMcpClient();
const tools = mcpToolsToAzureTools(await mcpClient.listTools());
console.log('toolstools', tools);
const customTools = listCustomTools();
return [
...tools.map((tool) => tool.function),
Expand Down
3 changes: 2 additions & 1 deletion apps/agent-tars/src/main/ipcRoutes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { actionRoute } from './action';
import { browserRoute } from './browser';
import { fileSystemRoute } from './filesystem';
import { searchRoute } from './search';

import { settingsRoute } from './settings';
const t = initIpc.create();

export const ipcRoutes = t.router({
Expand All @@ -15,6 +15,7 @@ export const ipcRoutes = t.router({
...browserRoute,
...fileSystemRoute,
...searchRoute,
...settingsRoute,
});
export type Router = typeof ipcRoutes;

Expand Down
50 changes: 39 additions & 11 deletions apps/agent-tars/src/main/ipcRoutes/llm.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
import { MCPServerName, Message, MessageData } from '@agent-infra/shared';
import {
MCPServerName,
Message,
MessageData,
ModelSettings,
} from '@agent-infra/shared';
import { initIpc } from '@ui-tars/electron-ipc/main';
import { ChatCompletionTool } from 'openai/resources/index.mjs';
import { BrowserWindow } from 'electron';
import { createLLM, LLMConfig } from '@main/llmProvider';
import { ProviderFactory } from '@main/llmProvider/ProviderFactory';
import { SettingStore } from '@main/store/setting';

const t = initIpc.create();

/**
* Get the current provider configuration based on settings
*/
function getLLMProviderConfig(settings: ModelSettings): LLMConfig {
const { provider, model, apiKey, apiVersion, endpoint } = settings;
return {
configName: provider,
model,
apiKey,
apiVersion,
// TODO: baseURL || endpoint
baseURL: endpoint,
};
}

const currentLLMConfigRef: {
current: LLMConfig;
} = {
current: {},
current: getLLMProviderConfig(SettingStore.get('model') || {}),
};

export const llmRoute = t.router({
Expand Down Expand Up @@ -41,6 +62,7 @@ export const llmRoute = t.router({
const messages = input.messages.map((msg) => new Message(msg));
const llm = createLLM(currentLLMConfigRef.current);
console.log('current llm config', currentLLMConfigRef.current);
console.log('input.tools', input.tools);
const response = await llm.askTool({
messages,
tools: input.tools,
Expand Down Expand Up @@ -90,17 +112,23 @@ export const llmRoute = t.router({
return requestId;
}),

updateLLMConfig: t.procedure.input<LLMConfig>().handle(async ({ input }) => {
try {
console.log('input entered', input);
currentLLMConfigRef.current = input;
return true;
} catch (error) {
console.error('Failed to update LLM configuration:', error);
return false;
}
getLLMConfig: t.procedure.input<void>().handle(async () => {
return SettingStore.get('model');
}),

updateLLMConfig: t.procedure
.input<ModelSettings>()
.handle(async ({ input }) => {
try {
SettingStore.set('model', input);
currentLLMConfigRef.current = getLLMProviderConfig(input);
return true;
} catch (error) {
console.error('Failed to update LLM configuration:', error);
return false;
}
}),

getAvailableProviders: t.procedure.input<void>().handle(async () => {
try {
return ProviderFactory.getAvailableProviders();
Expand Down
8 changes: 6 additions & 2 deletions apps/agent-tars/src/main/ipcRoutes/search.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { initIpc } from '@ui-tars/electron-ipc/main';
import { updateSearchConfig } from '../customTools/search';
import { SearchSettings } from '@agent-infra/shared';
import { SettingStore } from '@main/store/setting';

const t = initIpc.create();

Expand All @@ -9,11 +9,15 @@ export const searchRoute = t.router({
.input<SearchSettings>()
.handle(async ({ input }) => {
try {
await updateSearchConfig(input);
SettingStore.set('search', input);
return true;
} catch (error) {
console.error('Failed to update search configuration:', error);
return false;
}
}),

getSearchConfig: t.procedure.input<void>().handle(async () => {
return SettingStore.get('search');
}),
});
26 changes: 26 additions & 0 deletions apps/agent-tars/src/main/ipcRoutes/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AppSettings } from '@agent-infra/shared';
import { SettingStore } from '@main/store/setting';
import { initIpc } from '@ui-tars/electron-ipc/main';

const t = initIpc.create();

export const settingsRoute = t.router({
getSettings: t.procedure.input<void>().handle(async () => {
return SettingStore.getStore();
}),
getFileSystemSettings: t.procedure.input<void>().handle(async () => {
return SettingStore.get('fileSystem');
}),
updateAppSettings: t.procedure
.input<AppSettings>()
.handle(async ({ input }) => {
SettingStore.setStore(input);
return true;
}),
updateFileSystemSettings: t.procedure
.input<AppSettings['fileSystem']>()
.handle(async ({ input }) => {
SettingStore.set('fileSystem', input);
return true;
}),
});
92 changes: 92 additions & 0 deletions apps/agent-tars/src/main/store/setting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2025 Bytedance, Inc. and its affiliates.
* SPDX-License-Identifier: Apache-2.0
*/
import { BrowserWindow } from 'electron';
import ElectronStore from 'electron-store';
import {
ModelProvider,
ModelSettings,
SearchProvider,
SearchSettings,
FileSystemSettings,
AppSettings,
} from '@agent-infra/shared';

const DEFAULT_MODEL_SETTINGS: ModelSettings = {
provider: ModelProvider.OPENAI,
model: 'gpt-4o',
apiKey: '',
apiVersion: '',
endpoint: '',
};

const DEFAULT_FILESYSTEM_SETTINGS: FileSystemSettings = {
availableDirectories: [],
};

const DEFAULT_SEARCH_SETTINGS: SearchSettings = {
provider: SearchProvider.DUCKDUCKGO_SEARCH,
apiKey: '',
};

export const DEFAULT_SETTING: AppSettings = {
model: DEFAULT_MODEL_SETTINGS,
fileSystem: DEFAULT_FILESYSTEM_SETTINGS,
search: DEFAULT_SEARCH_SETTINGS,
};

export class SettingStore {
private static instance: ElectronStore<AppSettings>;

public static getInstance(): ElectronStore<AppSettings> {
if (!SettingStore.instance) {
SettingStore.instance = new ElectronStore<AppSettings>({
name: 'agent_tars.setting',
defaults: DEFAULT_SETTING,
});

SettingStore.instance.onDidAnyChange((newValue, oldValue) => {
console.log(
`SettingStore: ${JSON.stringify(oldValue)} changed to ${JSON.stringify(newValue)}`,
);
// Notify that value updated
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('setting-updated', newValue);
});
});
}
return SettingStore.instance;
}

public static set<K extends keyof AppSettings>(
key: K,
value: AppSettings[K],
): void {
SettingStore.getInstance().set(key, value);
}

public static setStore(state: AppSettings): void {
SettingStore.getInstance().set(state);
}

public static get<K extends keyof AppSettings>(key: K): AppSettings[K] {
return SettingStore.getInstance().get(key);
}

public static remove<K extends keyof AppSettings>(key: K): void {
SettingStore.getInstance().delete(key);
}

public static getStore(): AppSettings {
return SettingStore.getInstance().store;
}

public static clear(): void {
SettingStore.getInstance().set(DEFAULT_SETTING);
}

public static openInEditor(): void {
SettingStore.getInstance().openInEditor();
}
}
18 changes: 0 additions & 18 deletions apps/agent-tars/src/renderer/src/agent/AgentFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { Greeter } from './Greeter';
import { extractHistoryEvents } from '@renderer/utils/extractHistoryEvents';
import { EventItem, EventType } from '@renderer/type/event';
import { SNAPSHOT_BROWSER_ACTIONS } from '@renderer/constants';
import { loadLLMSettings } from '@renderer/services/llmSettings';

export interface AgentContext {
plan: PlanTask[];
Expand All @@ -40,23 +39,6 @@ export class AgentFlow {
private loadingStatusTip = '';

constructor(private appContext: AppContext) {
// Load LLM settings and update the configuration
const llmSettings = loadLLMSettings();
if (llmSettings) {
// Update LLM configuration when starting the agent flow
ipcClient
.updateLLMConfig({
configName: llmSettings.provider,
model: llmSettings.model,
apiKey: llmSettings.apiKey,
apiVersion: llmSettings.apiVersion,
baseURL: llmSettings.endpoint,
})
.catch((error) => {
console.error('Failed to update LLM configuration:', error);
});
}

const omegaHistoryEvents = this.parseHistoryEvents();
this.eventManager = new EventManager(omegaHistoryEvents);
this.abortController = new AbortController();
Expand Down
14 changes: 9 additions & 5 deletions apps/agent-tars/src/renderer/src/api/fileSystemInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,18 @@ export async function interceptToolCalls(
toolName === ToolCallType.DirectoryTree ||
toolName === ToolCallType.GetFileInfo
) {
updatedParams.path = normalizePath(params.path);
updatedParams.path = await normalizePath(params.path);
} else if (toolName === ToolCallType.ReadMultipleFiles) {
updatedParams.paths = (params.paths || []).map(normalizePath);
updatedParams.paths = await Promise.all(
(params.paths || []).map((path: string) => {
return normalizePath(path);
}),
);
} else if (toolName === ToolCallType.MoveFile) {
updatedParams.source = normalizePath(params.source);
updatedParams.destination = normalizePath(params.destination);
updatedParams.source = await normalizePath(params.source);
updatedParams.destination = await normalizePath(params.destination);
} else if (toolName === ToolCallType.SearchFiles) {
updatedParams.path = normalizePath(params.path);
updatedParams.path = await normalizePath(params.path);
}

// Update the tool call with normalized paths
Expand Down
4 changes: 1 addition & 3 deletions apps/agent-tars/src/renderer/src/api/llmConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ipcClient } from './index';
import { ModelSettings } from '@agent-infra/shared';
import { getLLMProviderConfig } from '../services/llmSettings';

/**
* Update the LLM configuration in the main process
Expand All @@ -9,8 +8,7 @@ export async function updateLLMConfig(
settings: ModelSettings,
): Promise<boolean> {
try {
const config = getLLMProviderConfig(settings);
return await ipcClient.updateLLMConfig(config);
return await ipcClient.updateLLMConfig(settings);
} catch (error) {
console.error('Failed to update LLM configuration:', error);
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import {
} from '@nextui-org/react';
import { IoWarningOutline } from 'react-icons/io5';
import { ipcClient } from '@renderer/api';
import {
loadFileSystemSettings,
saveFileSystemSettings,
} from '@renderer/services/fileSystemSettings';
import path from 'path-browserify';
import { resolvePermission } from '@renderer/services/filePermissionService';
import { useAppSettings } from '../LeftSidebar/Settings/useAppSettings';
Expand All @@ -35,12 +31,13 @@ export function FilePermissionModal({
setIsProcessing(true);
try {
// Add this directory to allowed directories
const settings = loadFileSystemSettings() || {
const settings = (await ipcClient.getFileSystemSettings()) || {
availableDirectories: [],
};

if (!settings.availableDirectories.includes(directoryPath)) {
settings.availableDirectories.push(directoryPath);
saveFileSystemSettings(settings);
await ipcClient.updateFileSystemSettings(settings);
setSettings((appSettings) => {
return {
...appSettings,
Expand Down
Loading
Loading