Skip to content

Commit

Permalink
✨ feat: Support define default agent config with `DEFAULT_AGENT_CONFI…
Browse files Browse the repository at this point in the history
…G` ENV (lobehub#1291)

* ✨ feat: add parser

* ✨ feat: add parser

* ✨ feat: support DEFAULT_AGENT_CONFIG env

* ✨ feat: add parser

* 📝 docs: add docs

* 🐛 fix: fix max token config
  • Loading branch information
arvinxx authored Feb 14, 2024
1 parent 6bf1bfa commit c7c096e
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ config.rules['react/no-unknown-property'] = 0;
config.rules['unicorn/prefer-ternary'] = 0;
config.rules['unicorn/prefer-spread'] = 0;
config.rules['unicorn/catch-error-name'] = 0;
config.rules['unicorn/no-array-for-each'] = 0;
config.rules['unicorn/prefer-number-properties'] = 0;

module.exports = config;
26 changes: 26 additions & 0 deletions docs/Deployment/Environment-Variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ LobeChat provides additional configuration options during deployment, which can
- Default: `-`
- Example: `/test`

#### `DEFAULT_AGENT_CONFIG`

- Type: Optional
- Description: Used to configure the default configuration of the LobeChat default assistant. It supports various data types and structures, including key-value pairs, nested fields, array values, etc.
- Default Value: `-`
- Example: `'model=gpt-4-1106-preview;params.max_tokens=300;plugins=search-engine,lobe-image-designer`

`DEFAULT_AGENT_CONFIG` is used to configure the default configuration of the LobeChat default agent. It supports various data types and structures, including key-value pairs, nested fields, array values, etc. The table below provides detailed explanations of the configuration options, examples, and corresponding explanations for the `DEFAULT_AGENT_CONFIG` environment variable:

| Configuration Type | Example | Explanation |
| ----------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Basic Key-Value Pair | `model=gpt-4` | Set the model to `gpt-4`. |
| Nested Field | `tts.sttLocale=en-US` | Set the language region for the text-to-speech service to `en-US`. |
| Array | `plugins=search-engine,lobe-image-designer` | Enable the `search-engine` and `lobe-image-designer` plugins. |
| Chinese Comma | `plugins=search-engine,lobe-image-designer` | The same as above, demonstrating support for Chinese comma separation. |
| Multiple Configurations | `model=glm-4;provider=zhipu` | Set the model to `glm-4` and the model provider to `zhipu`. |
| Numeric Value | `params.max_tokens=300` | Set the maximum number of tokens to `300`. |
| Boolean Value | `enableAutoCreateTopic=true` | Enable automatic topic creation. |
| Special Characters | `inputTemplate="Hello; I am a bot;"` | Set the input template to `Hello; I am a bot;`. |
| Error Handling | `model=gpt-4;maxToken` | Ignore the invalid entry `maxToken` and only parse out `model=gpt-4`. |
| Value Overriding | `model=gpt-4;model=gpt-4-1106-preview` | If the key is duplicated, use the value that appears last, in this case, the value of `model` is `gpt-4-1106-preview`. |

Related discussions:

- [\[RFC\] 022 - Default Helper Parameters for Environment Variable Configuration](https://github.com/lobehub/lobe-chat/discussions/913)

## Authentication Service Providers

### Common Settings
Expand Down
26 changes: 26 additions & 0 deletions docs/Deployment/Environment-Variable.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ LobeChat 在部署时提供了一些额外的配置项,使用环境变量进
- 默认值: `-`
- 示例: `/test`

#### `DEFAULT_AGENT_CONFIG`

- 类型:可选
- 描述:用于配置 LobeChat 默认助理的默认配置。它支持多种数据类型和结构,包括键值对、嵌套字段、数组值等。
- 默认值:`-`
- 示例:`'model=gpt-4-1106-preview;params.max_tokens=300;plugins=search-engine,lobe-image-designer`

`DEFAULT_AGENT_CONFIG` 用于配置 LobeChat 默认助理的默认配置。它支持多种数据类型和结构,包括键值对、嵌套字段、数组值等。下表详细说明了 `DEFAULT_AGENT_CONFIG` 环境变量的配置项、示例以及相应解释:

| 配置项类型 | 示例 | 解释 |
| ---------- | -------------------------------------------- | ---------------------------------------------------------------------------- |
| 基本键值对 | `model=gpt-4` | 设置模型为 `gpt-4`|
| 嵌套字段 | `tts.sttLocale=en-US` | 设置文本到语音服务的语言区域为 `en-US`|
| 数组 | `plugins=search-engine,lobe-image-designer` | 启用 `search-engine``lobe-image-designer` 插件。 |
| 中文逗号 | `plugins=search-engine,lobe-image-designer` | 同上,演示支持中文逗号分隔。 |
| 多个配置项 | `model=glm-4;provider=zhipu` | 设置模型为 `glm-4` 且模型服务商为 `zhipu`|
| 数字值 | `params.max_tokens=300` | 设置最大令牌数为 `300`|
| 布尔值 | `enableAutoCreateTopic=true` | 启用自动创建主题。 |
| 特殊字符 | `inputTemplate="Hello; I am a bot;"` | 设置输入模板为 `Hello; I am a bot;`|
| 错误处理 | `model=gpt-4;maxToken` | 忽略无效条目 `maxToken`,仅解析出 `model=gpt-4`|
| 值覆盖 | `model=gpt-4;model=gpt-4-1106-preview` | 如果键重复,使用最后一次出现的值,此处 `model` 的值为 `gpt-4-1106-preview`|

相关阅读:

- [\[RFC\] 022 - 环境变量配置默认助手参数](https://github.com/lobehub/lobe-chat/discussions/913)

## 身份验证服务

### 通用设置
Expand Down
181 changes: 181 additions & 0 deletions src/app/api/config/parseDefaultAgent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { describe, expect, it } from 'vitest';

import { parseAgentConfig } from './parseDefaultAgent';

describe('parseAgentConfig', () => {
describe('single functional feature', () => {
it('parses single key-value pair correctly', () => {
const envStr = 'model=gpt-4';
const expected = { model: 'gpt-4' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses nested fields correctly', () => {
const envStr = 'tts.sttLocale=en-US';
const expected = { tts: { sttLocale: 'en-US' } };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses array values with commas correctly', () => {
const envStr = 'plugins=search-engine,lobe-image-designer';
const expected = { plugins: ['search-engine', 'lobe-image-designer'] };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses array values with Chinese commas correctly', () => {
const envStr = 'plugins=search-engine,lobe-image-designer';
const expected = { plugins: ['search-engine', 'lobe-image-designer'] };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses multiple key-value pairs correctly', () => {
const envStr = 'model=gpt-4;version=1.0.0';
const expected = { model: 'gpt-4', version: '1.0.0' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试数字值是否被正确解析为数字
it('parses numerical values correctly', () => {
const envStr = 'params.max_tokens=300';
const expected = { params: { max_tokens: 300 } };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试多级嵌套属性是否被正确解析
it('parses deeply nested fields correctly', () => {
const envStr = 'tts.voice.openai=english-voice';
const expected = { tts: { voice: { openai: 'english-voice' } } };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses boolean values correctly', () => {
const envStr = 'enableAutoCreateTopic=true;enableCompressThreshold=false';
const expected = {
enableAutoCreateTopic: true,
enableCompressThreshold: false,
};
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses fewShots array with cascading keys correctly', () => {
const envStr =
'fewShots.0.content=Hello;fewShots.0.role=user;fewShots.1.content=Hi;fewShots.1.role=system';
const expected = {
fewShots: [
{ content: 'Hello', role: 'user' },
{ content: 'Hi', role: 'system' },
],
};

// Assuming parseAgentConfig function has been implemented to understand and correctly parse the cascading keys format for fewShots
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses tts voice configuration correctly', () => {
const envStr = 'tts.voice.openai=english-voice;tts.voice.microsoft=spanish-voice';
const expected = {
tts: { voice: { openai: 'english-voice', microsoft: 'spanish-voice' } },
};
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('parses inputTemplate with special characters correctly', () => {
const envStr = 'inputTemplate="Hello, I am {name}"';
const expected = { inputTemplate: 'Hello, I am {name}' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});
});

describe('complex environment', () => {
it.skip('parses a complete environment variable string correctly', () => {
const envStr =
'model=gpt-4-1106-preview;params.max_tokens=300;plugins=search-engine,lobe-image-designer';
const expected = {
model: 'gpt-4-1106-preview',
params: { max_tokens: 300 },
plugins: ['search-engine', 'lobe-image-designer'],
};
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试当配置字符串包含所有可能字段时的行为
it('parses a complex environment variable string correctly', () => {
const envStr =
'model=gpt-4-1106-preview;params.max_tokens=300;params.temperature=0.7;plugins=search-engine,lobe-image-designer;tts.voice.openai=english-voice';
const expected = {
model: 'gpt-4-1106-preview',
params: { max_tokens: 300, temperature: 0.7 },
plugins: ['search-engine', 'lobe-image-designer'],
tts: { voice: { openai: 'english-voice' } },
};
expect(parseAgentConfig(envStr)).toEqual(expected);
});
});

describe('Error Boundary', () => {
it('handles empty string input', () => {
const envStr = '';
const expected = {};
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('ignores entries without an equal sign', () => {
const envStr = 'model=gpt-4;invalidentry';
const expected = { model: 'gpt-4' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('handles entries with missing value', () => {
const envStr = 'model=gpt-4;version=';
const expected = { model: 'gpt-4', version: undefined };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('handles entries with missing key', () => {
const envStr = '=gpt-4;version=1.0.0';
const expected = { version: '1.0.0' }; // Assuming the parser ignores entries with no key
expect(parseAgentConfig(envStr)).toEqual(expected);
});

it('handles multiple consecutive semicolons', () => {
const envStr = 'model=gpt-4;;version=1.0.0';
const expected = { model: 'gpt-4', version: '1.0.0' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试键重复时的覆盖行为
it('overrides duplicate keys with the last occurrence', () => {
const envStr = 'model=gpt-4;model=gpt-4-1106-preview';
const expected = { model: 'gpt-4-1106-preview' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试未提供的数组值是否返回空数组
it('parses missing array values as undefined', () => {
const envStr = 'plugins=';
const expected = {};
expect(parseAgentConfig(envStr)).toEqual(expected);
});
});

// 测试值中包含分号的情况
it('handles values with semicolons correctly', () => {
const envStr = 'inputTemplate="Hello; I am a bot;"';
const expected = { inputTemplate: 'Hello; I am a bot;' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试值中包含等号的情况
it('handles values with equals signs correctly', () => {
const envStr = 'inputTemplate="Hello=world"';
const expected = { inputTemplate: 'Hello=world' };
expect(parseAgentConfig(envStr)).toEqual(expected);
});

// 测试空值是否返回undefined或空字符串
it('parses empty values as undefined or empty string', () => {
const envStr = 'model=';
const expected = { model: undefined }; // 或 { model: '' },取决于应用逻辑
expect(parseAgentConfig(envStr)).toEqual(expected);
});
});
42 changes: 42 additions & 0 deletions src/app/api/config/parseDefaultAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { set } from 'lodash-es';

/**
* Improved parsing function that handles numbers, booleans, semicolons, and equals signs in values.
* @param {string} envStr - The environment variable string to be parsed.
*/
export const parseAgentConfig = (envStr: string) => {
const config = {};
// use regex to match key-value pairs, considering the possibility of semicolons in values
const regex = /([^;=]+)=("[^"]+"|[^;]+)/g;
let match;

while ((match = regex.exec(envStr)) !== null) {
const key = match[1].trim();
let value = match[2].trim();
if (!key || !value) return;

let finalValue: any = value;

// Handle string value
if (value.startsWith('"') && value.endsWith('"')) {
finalValue = value.slice(1, -1);
}
// Handle numeric values
else if (!isNaN(value as any)) {
finalValue = Number(value);
}
// Handle boolean values
else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
finalValue = value.toLowerCase() === 'true';
}
// Handle arrays
else if (value.includes(',') || value.includes(',')) {
const array = value.replaceAll(',', ',').split(',');
finalValue = array.map((item) => (isNaN(item as any) ? item : Number(item)));
}

set(config, key, finalValue);
}

return config;
};
6 changes: 6 additions & 0 deletions src/app/api/config/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getServerConfig } from '@/config/server';
import { GlobalServerConfig } from '@/types/settings';

import { parseAgentConfig } from './parseDefaultAgent';

export const runtime = 'edge';

/**
Expand All @@ -15,10 +17,14 @@ export const GET = async () => {
ENABLED_GOOGLE,
ENABLE_OAUTH_SSO,
ENABLE_OLLAMA,
DEFAULT_AGENT_CONFIG,
} = getServerConfig();

const config: GlobalServerConfig = {
customModelName: CUSTOM_MODELS,
defaultAgent: {
config: parseAgentConfig(DEFAULT_AGENT_CONFIG),
},
enabledOAuthSSO: ENABLE_OAUTH_SSO,
languageModel: {
bedrock: { enabled: ENABLED_AWS_BEDROCK },
Expand Down
4 changes: 4 additions & 0 deletions src/config/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ declare global {

PLUGINS_INDEX_URL?: string;
PLUGIN_SETTINGS?: string;

DEFAULT_AGENT_CONFIG?: string;
}
}
}
Expand All @@ -32,6 +34,8 @@ export const getAppConfig = () => {
return {
ACCESS_CODES,

DEFAULT_AGENT_CONFIG: process.env.DEFAULT_AGENT_CONFIG || '',

SHOW_ACCESS_CODE_CONFIG: !!ACCESS_CODES.length,

METADATA_BASE_URL: process.env.METADATA_BASE_URL,
Expand Down
2 changes: 1 addition & 1 deletion src/features/AgentSetting/AgentConfig/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const AgentConfig = memo(() => {
displayMode,
enableAutoCreateTopic,
enableHistoryCount,
enableMaxTokens,
enableCompressThreshold,
enableMaxTokens,
updateConfig,
] = useStore((s) => [
s.config.displayMode,
Expand Down

0 comments on commit c7c096e

Please sign in to comment.