feat: Support custom response language (#510)

* feat: Support custom response language

* fix: Add default values for response language in config-manager.js

* chore: Update configuration file and add default response language settings

* feat: Support MCP/CLI custom response language

* chore: Update test comments to English for consistency

* docs: Auto-update and format models.md

* chore: fix format

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
shenysun
2025-07-03 04:35:49 +08:00
committed by GitHub
parent 5eafc5ea11
commit c99df64f65
15 changed files with 454 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ const mockGetResearchModelId = jest.fn();
const mockGetFallbackProvider = jest.fn();
const mockGetFallbackModelId = jest.fn();
const mockGetParametersForRole = jest.fn();
const mockGetResponseLanguage = jest.fn();
const mockGetUserId = jest.fn();
const mockGetDebugFlag = jest.fn();
const mockIsApiKeySet = jest.fn();
@@ -98,6 +99,7 @@ jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
getFallbackMaxTokens: mockGetFallbackMaxTokens,
getFallbackTemperature: mockGetFallbackTemperature,
getParametersForRole: mockGetParametersForRole,
getResponseLanguage: mockGetResponseLanguage,
getUserId: mockGetUserId,
getDebugFlag: mockGetDebugFlag,
getBaseUrlForRole: mockGetBaseUrlForRole,
@@ -277,6 +279,7 @@ describe('Unified AI Services', () => {
if (role === 'fallback') return { maxTokens: 150, temperature: 0.6 };
return { maxTokens: 100, temperature: 0.5 }; // Default
});
mockGetResponseLanguage.mockReturnValue('English');
mockResolveEnvVariable.mockImplementation((key) => {
if (key === 'ANTHROPIC_API_KEY') return 'mock-anthropic-key';
if (key === 'PERPLEXITY_API_KEY') return 'mock-perplexity-key';
@@ -463,6 +466,68 @@ describe('Unified AI Services', () => {
expect(mockAnthropicProvider.generateText).toHaveBeenCalledTimes(1);
});
test('should use configured responseLanguage in system prompt', async () => {
mockGetResponseLanguage.mockReturnValue('中文');
mockAnthropicProvider.generateText.mockResolvedValue('中文回复');
const params = {
role: 'main',
systemPrompt: 'You are an assistant',
prompt: 'Hello'
};
await generateTextService(params);
expect(mockAnthropicProvider.generateText).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{
role: 'system',
content: expect.stringContaining('Always respond in 中文')
},
{ role: 'user', content: 'Hello' }
]
})
);
expect(mockGetResponseLanguage).toHaveBeenCalledWith(fakeProjectRoot);
});
test('should pass custom projectRoot to getResponseLanguage', async () => {
const customRoot = '/custom/project/root';
mockGetResponseLanguage.mockReturnValue('Español');
mockAnthropicProvider.generateText.mockResolvedValue(
'Respuesta en Español'
);
const params = {
role: 'main',
systemPrompt: 'You are an assistant',
prompt: 'Hello',
projectRoot: customRoot
};
await generateTextService(params);
expect(mockGetResponseLanguage).toHaveBeenCalledWith(customRoot);
expect(mockAnthropicProvider.generateText).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
{
role: 'system',
content: expect.stringContaining('Always respond in Español')
},
{ role: 'user', content: 'Hello' }
]
})
);
});
// Add more tests for edge cases:
// - Missing API keys (should throw from _resolveApiKey)
// - Unsupported provider configured (should skip and log)
// - Missing provider/model config for a role (should skip and log)
// - Missing prompt
// - Different initial roles (research, fallback)
// - generateObjectService (mock schema, check object result)
// - streamTextService (more complex to test, might need stream helpers)
test('should skip provider with missing API key and try next in fallback sequence', async () => {
// Setup isApiKeySet to return false for anthropic but true for perplexity
mockIsApiKeySet.mockImplementation((provider, session, root) => {

View File

@@ -141,7 +141,8 @@ const DEFAULT_CONFIG = {
defaultPriority: 'medium',
projectName: 'Task Master',
ollamaBaseURL: 'http://localhost:11434/api',
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com'
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
responseLanguage: 'English'
}
};
@@ -685,6 +686,82 @@ describe('Getter Functions', () => {
expect(logLevel).toBe(VALID_CUSTOM_CONFIG.global.logLevel);
});
test('getResponseLanguage should return responseLanguage from config', () => {
// Arrange
// Prepare a config object with responseLanguage property for this test
const configWithLanguage = JSON.stringify({
models: {
main: { provider: 'openai', modelId: 'gpt-4-turbo' }
},
global: {
projectName: 'Test Project',
responseLanguage: '中文'
}
});
// Set up fs.readFileSync to return our test config
fsReadFileSyncSpy.mockImplementation((filePath) => {
if (filePath === MOCK_CONFIG_PATH) {
return configWithLanguage;
}
if (path.basename(filePath) === 'supported-models.json') {
return JSON.stringify({
openai: [{ id: 'gpt-4-turbo' }]
});
}
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
});
fsExistsSyncSpy.mockReturnValue(true);
// Ensure getConfig returns new values instead of cached ones
configManager.getConfig(MOCK_PROJECT_ROOT, true);
// Act
const responseLanguage =
configManager.getResponseLanguage(MOCK_PROJECT_ROOT);
// Assert
expect(responseLanguage).toBe('中文');
});
test('getResponseLanguage should return undefined when responseLanguage is not in config', () => {
// Arrange
const configWithoutLanguage = JSON.stringify({
models: {
main: { provider: 'openai', modelId: 'gpt-4-turbo' }
},
global: {
projectName: 'Test Project'
// No responseLanguage property
}
});
fsReadFileSyncSpy.mockImplementation((filePath) => {
if (filePath === MOCK_CONFIG_PATH) {
return configWithoutLanguage;
}
if (path.basename(filePath) === 'supported-models.json') {
return JSON.stringify({
openai: [{ id: 'gpt-4-turbo' }]
});
}
throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
});
fsExistsSyncSpy.mockReturnValue(true);
// Ensure getConfig returns new values instead of cached ones
configManager.getConfig(MOCK_PROJECT_ROOT, true);
// Act
const responseLanguage =
configManager.getResponseLanguage(MOCK_PROJECT_ROOT);
// Assert
expect(responseLanguage).toBe('English');
});
// Add more tests for other getters (getResearchProvider, getProjectName, etc.)
});