@unified-llm/core
Version:
Unified LLM interface (in-memory).
265 lines • 11.9 kB
JavaScript
import { OpenAIProvider } from './providers/openai';
import { AnthropicProvider } from './providers/anthropic';
import { GeminiProvider } from './providers/google';
import { DeepSeekProvider } from './providers/deepseek';
// ファクトリークラス
export class LLMClient {
constructor(config) {
this.id = config.id;
this.tools = config.tools;
this.systemPrompt = config.systemPrompt;
switch (config.provider) {
case 'openai':
this.baseProvider = new OpenAIProvider({
apiKey: config.apiKey,
model: config.model,
tools: this.tools
});
break;
case 'anthropic':
this.baseProvider = new AnthropicProvider({
apiKey: config.apiKey,
model: config.model || 'claude-3-haiku-20240307',
tools: this.tools
});
break;
case 'google':
this.baseProvider = new GeminiProvider({
apiKey: config.apiKey,
model: config.model,
tools: this.tools
});
break;
case 'deepseek':
this.baseProvider = new DeepSeekProvider({
apiKey: config.apiKey,
model: config.model || 'deepseek-chat',
tools: this.tools
});
break;
default:
throw new Error(`Unsupported provider: ${config.provider}`);
}
}
// 静的ファクトリーメソッド(後方互換性のため)
static create(provider, apiKey, model) {
switch (provider) {
case 'openai':
return new OpenAIProvider({ apiKey, model });
case 'anthropic':
return new AnthropicProvider({ apiKey, model });
case 'google':
return new GeminiProvider({ apiKey, model });
case 'deepseek':
return new DeepSeekProvider({ apiKey, model });
default:
throw new Error(`Unsupported provider: ${provider}`);
}
}
// 利用可能な関数をツール定義に変換
generateToolDefinitions() {
if (!this.tools)
return undefined;
return Object.keys(this.tools).map(functionName => {
let description = `Execute ${functionName} function`;
// 特定の関数に対してより具体的な説明を提供
switch (functionName) {
case 'getAuthor':
description = 'Get information about the project author. Returns the name of the person who created this project.';
break;
case 'getProjectInfo':
description = 'Get detailed internal project information including secret keys and build numbers that cannot be guessed.';
break;
case 'getCurrentTime':
description = 'Get the current date and time in ISO format. Use this when you need to know what time it is right now.';
break;
case 'cat':
description = 'Display the contents of a file, similar to the Unix cat command.';
break;
case 'tree':
description = 'Display directory structure in a tree format.';
break;
case 'callAnotherClient':
description = 'Call another LLM client to help with a task.';
break;
}
return {
type: 'function',
function: {
name: functionName,
description,
parameters: {
type: 'object',
properties: {},
required: []
}
}
};
});
}
// 関数を実行
async executeFunction(functionName, args) {
if (!this.tools || !Array.isArray(this.tools)) {
throw new Error('No tools available');
}
const func = this.tools.find(f => f.function.name === functionName);
if (!func || typeof func.handler !== 'function') {
throw new Error(`Function ${functionName} not found`);
}
// argumentMapから固定引数を取得してマージ
const fixedArgs = func.args || {};
const mergedArgs = { ...fixedArgs, ...args };
return await func.handler(mergedArgs);
}
// チャット機能(function callingサポート付き)
async chat(request) {
// ツール定義を追加
const tools = this.generateToolDefinitions();
// システムプロンプトを注入(存在する場合)
let messages = request.messages || [];
// メッセージのコンテンツを正規化(文字列を配列に変換)
messages = messages.map((msg) => ({
...msg,
content: typeof msg.content === 'string'
? [{ type: 'text', text: msg.content }]
: msg.content
}));
if (this.systemPrompt && !messages.some((m) => m.role === 'system')) {
messages = [
{
id: this.generateMessageId(),
role: 'system',
content: [{ type: 'text', text: this.systemPrompt }],
created_at: new Date()
},
...messages
];
}
const enhancedRequest = {
...request,
messages,
tools
};
let response = await this.baseProvider.chat(enhancedRequest);
// ツール呼び出しがある場合は実行
if (response.message.content) {
const contents = Array.isArray(response.message.content) ? response.message.content : [response.message.content];
const toolUseContents = contents.filter(c => typeof c === 'object' && c.type === 'tool_use');
// Debug logging for tool use detection can be enabled if needed
if (toolUseContents.length > 0) {
const toolResults = [];
for (const toolUse of toolUseContents) {
try {
// console.log('🔧 Executing function:', toolUse.name, 'with args:', toolUse.input);
const result = await this.executeFunction(toolUse.name, toolUse.input);
// console.log('✅ Function result:', result);
toolResults.push({
type: 'tool_result',
toolUseId: toolUse.id,
function_name: toolUse.name, // Add function name for providers that need it
content: [{ type: 'text', text: JSON.stringify(result) }]
});
}
catch (error) {
// console.log('❌ Function execution error:', error);
toolResults.push({
type: 'tool_result',
toolUseId: toolUse.id,
function_name: toolUse.name, // Add function name for providers that need it
isError: true,
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }]
});
}
}
// ツール結果を含む新しいリクエストを作成
const followUpRequest = {
...request,
messages: [
...request.messages,
response.message,
{
id: this.generateMessageId(),
role: 'tool',
content: toolResults,
created_at: new Date()
}
]
};
// Debug logging for follow-up requests can be enabled if needed
// Check if this is a Google provider - handle differently
if (response.provider === 'google') {
// For Google/Gemini, don't send function results back to the model
// Instead, create a response that includes both the function call and the result
const functionResult = toolResults[0]; // Assuming single function call for now
const resultText = Array.isArray(functionResult.content)
? functionResult.content.map(c => c.type === 'text' ? c.text : '[Non-text]').join('\n')
: '[No result]';
// Parse the JSON result to get the actual return value
let actualResult = resultText;
try {
const parsed = JSON.parse(resultText);
actualResult = typeof parsed === 'string' ? parsed : JSON.stringify(parsed);
}
catch (_a) {
// Keep original if not JSON
}
// Create a response that includes the function execution result
const existingContent = Array.isArray(response.message.content)
? response.message.content
: typeof response.message.content === 'string'
? [{ type: 'text', text: response.message.content }]
: [response.message.content];
response.message.content = [
...existingContent,
{
type: 'text',
text: `\n\nFunction execution result: ${actualResult}`
}
];
// console.log('📥 Google: Enhanced response with function result');
}
else {
// For other providers, send function results back to the model
response = await this.baseProvider.chat(followUpRequest);
// console.log('📥 Follow-up response received:', response.message.content);
}
}
}
return response;
}
// ストリーミングチャット
async *stream(request) {
const tools = this.generateToolDefinitions();
// システムプロンプトを注入(存在する場合)
let messages = request.messages || [];
// メッセージのコンテンツを正規化(文字列を配列に変換)
messages = messages.map((msg) => ({
...msg,
content: typeof msg.content === 'string'
? [{ type: 'text', text: msg.content }]
: msg.content
}));
if (this.systemPrompt && !messages.some((m) => m.role === 'system')) {
messages = [
{
id: this.generateMessageId(),
role: 'system',
content: [{ type: 'text', text: this.systemPrompt }],
created_at: new Date()
},
...messages
];
}
const enhancedRequest = {
...request,
messages,
tools
};
yield* this.baseProvider.stream(enhancedRequest);
}
generateMessageId() {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
export default LLMClient;
//# sourceMappingURL=llm-client.js.map