UNPKG

@unified-llm/core

Version:

Unified LLM interface (in-memory).

265 lines 11.9 kB
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