contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
266 lines (265 loc) ⢠13.1 kB
JavaScript
import { LLMFactory } from "./llm/LLMFactory.js";
import { ToolManager } from "./tools/ToolManager.js";
import { PromptTemplates } from "./prompts/PromptTemplates.js";
import { DebugLogger } from "./DebugLogger.js";
import { ConversationMessageBuilder, ToolResponseHandler } from "./conversation/MessageBuilder.js";
export class ChatService {
constructor(baseDir, debugEnabled = false, debugFile) {
this.sessions = new Map();
this.toolManager = new ToolManager(baseDir || process.cwd());
this.debugLogger = new DebugLogger(debugEnabled, debugFile);
}
getDebugLogger() {
return this.debugLogger;
}
/**
* Create a new chat session
*/
createSession(options) {
const sessionId = this.generateSessionId();
const session = {
id: sessionId,
messages: [],
createdAt: Date.now(),
updatedAt: Date.now(),
systemPrompt: options?.systemPrompt || PromptTemplates.getDefaultSystemPrompt()
};
// Add system message if system prompt is provided
if (session.systemPrompt) {
session.messages.push({
id: this.generateMessageId(),
role: 'system',
content: session.systemPrompt,
timestamp: Date.now()
});
}
this.sessions.set(sessionId, session);
return session;
}
/**
* Get an existing chat session
*/
getSession(sessionId) {
return this.sessions.get(sessionId);
}
/**
* Send a message and get a response
*/
async sendMessage(sessionId, userMessage, options) {
const session = this.sessions.get(sessionId);
if (!session) {
throw new Error(`Chat session ${sessionId} not found`);
}
// Add user message to session
const userMsg = {
id: this.generateMessageId(),
role: 'user',
content: userMessage,
timestamp: Date.now()
};
session.messages.push(userMsg);
try {
// Get LLM provider
const llm = await this.getLLMProvider(options?.provider);
// Execute prompt with conversation context
const promptOptions = {
temperature: options?.temperature ?? 0.7,
maxTokens: options?.maxTokens ?? 2000
};
// Tool call loop with turn tracking
const maxTurns = 10; // Maximum number of tool call iterations
let currentTurn = 0;
// Build messages using hybrid approach
const actualProviderName = this.getActualProviderName(llm);
const messageData = await ConversationMessageBuilder.buildForProvider(session, userMessage, actualProviderName, this.toolManager);
let currentResponse;
// Use appropriate execution method based on provider capabilities
if (llm.supportsConversation() && Array.isArray(messageData)) {
// Log conversation messages for debugging
await this.debugLogger.logPrompt(JSON.stringify(messageData, null, 2), promptOptions, sessionId, actualProviderName, options?.model);
currentResponse = await llm.executeConversation(messageData, promptOptions);
}
else {
// Fallback to prompt string for simple providers
const promptString = typeof messageData === 'string' ? messageData : JSON.stringify(messageData);
await this.debugLogger.logPrompt(promptString, promptOptions, sessionId, actualProviderName, options?.model);
currentResponse = await llm.executePrompt(promptString, promptOptions);
}
// Log the initial response
await this.debugLogger.logResponse(currentResponse.content, undefined, // PromptResponse doesn't have usage field
sessionId, actualProviderName, options?.model);
while (currentTurn < maxTurns) {
currentTurn++;
console.log(`š Turn ${currentTurn}/${maxTurns}`);
// Parse tool calls from current response
const toolCalls = this.toolManager.parseToolCalls(currentResponse.content);
if (toolCalls.length === 0) {
// No tool calls - this is the final response
console.log(`ā
Final response (no tool calls) on turn ${currentTurn}`);
const finalMsg = {
id: this.generateMessageId(),
role: 'assistant',
content: currentResponse.content,
timestamp: Date.now()
};
session.messages.push(finalMsg);
session.updatedAt = Date.now();
return finalMsg;
}
// Process tool calls
console.log(`š§ Processing ${toolCalls.length} tool calls on turn ${currentTurn}...`);
// Log each tool call
for (const toolCall of toolCalls) {
await this.debugLogger.logToolCall(toolCall.tool_name, toolCall.parameters, sessionId);
}
// Add the assistant message with tool calls to session
const assistantMsgWithTools = {
id: this.generateMessageId(),
role: 'assistant',
content: currentResponse.content,
timestamp: Date.now()
};
session.messages.push(assistantMsgWithTools);
// Execute tool calls and collect results
const toolResponses = [];
for (const toolCall of toolCalls) {
console.log(` š ļø Executing tool: ${toolCall.tool_name} with parameters:`, toolCall.parameters);
const toolResult = await this.toolManager.executeTool(toolCall);
// Generate unique ID if not provided
const responseId = toolCall.id || `${toolCall.tool_name}_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
const toolResponse = {
id: responseId,
tool_call: toolCall,
result: toolResult
};
toolResponses.push(toolResponse);
// Log tool response
await this.debugLogger.logToolResponse(toolCall.tool_name, toolResult.success, toolResult.success ? toolResult.message : undefined, toolResult.success ? undefined : toolResult.error, sessionId);
if (toolResult.success) {
console.log(` ā
Tool execution successful: ${toolResult.message}`);
}
else {
console.log(` ā Tool execution failed: ${toolResult.error}`);
}
}
// Integrate tool responses using the new handler
await ToolResponseHandler.integrateToolResponses(session, toolResponses, actualProviderName, this.toolManager);
// Generate next response with tool results
console.log(`š¤ Generating follow-up response with tool results (turn ${currentTurn})...`);
try {
// Build follow-up messages using hybrid approach
const followUpMessageData = await ConversationMessageBuilder.buildForProvider(session, '', // No new user message, just continuing conversation
actualProviderName, this.toolManager);
// Use appropriate execution method based on provider capabilities
if (llm.supportsConversation() && Array.isArray(followUpMessageData)) {
// Log conversation messages for debugging
await this.debugLogger.logPrompt(JSON.stringify(followUpMessageData, null, 2), promptOptions, sessionId, actualProviderName, options?.model);
currentResponse = await llm.executeConversation(followUpMessageData, promptOptions);
}
else {
// Fallback to prompt string for simple providers
const promptString = typeof followUpMessageData === 'string' ? followUpMessageData : JSON.stringify(followUpMessageData);
await this.debugLogger.logPrompt(promptString, promptOptions, sessionId, actualProviderName, options?.model);
currentResponse = await llm.executePrompt(promptString, promptOptions);
}
// Log follow-up response
await this.debugLogger.logResponse(currentResponse.content, undefined, // PromptResponse doesn't have usage field
sessionId, actualProviderName, options?.model);
}
catch (error) {
console.error(`ā Error generating follow-up response on turn ${currentTurn}:`, error);
// Log the error
await this.debugLogger.logError(`Error generating follow-up response on turn ${currentTurn}: ${error.message}`, error.stack, sessionId);
const errorContent = PromptTemplates.getErrorHandlingPrompt(error.message);
const errorMsg = {
id: this.generateMessageId(),
role: 'assistant',
content: errorContent,
timestamp: Date.now()
};
session.messages.push(errorMsg);
session.updatedAt = Date.now();
return errorMsg;
}
}
// Max turns reached - return current response with warning
console.log(`ā ļø Maximum turns (${maxTurns}) reached. Returning current response.`);
const maxTurnsMsg = {
id: this.generateMessageId(),
role: 'assistant',
content: `${currentResponse.content}\n\nā ļø *Note: Maximum tool call iterations (${maxTurns}) reached. Some operations may be incomplete.*`,
timestamp: Date.now()
};
session.messages.push(maxTurnsMsg);
session.updatedAt = Date.now();
return maxTurnsMsg;
}
catch (error) {
console.error('Error in chat service:', error);
throw new Error(`Failed to generate response: ${error.message}`);
}
}
/**
* Get all messages from a session (filtered for UI display)
*/
getMessages(sessionId) {
const session = this.sessions.get(sessionId);
return session ? session.messages.filter(msg => msg.role !== 'system' && !msg.metadata?.internal && !msg.metadata?.isToolResponse) : [];
}
/**
* Get all messages from a session including tool responses (for internal use)
*/
getAllMessages(sessionId) {
const session = this.sessions.get(sessionId);
return session ? session.messages : [];
}
/**
* Clear a chat session
*/
clearSession(sessionId) {
return this.sessions.delete(sessionId);
}
/**
* List all active sessions
*/
listSessions() {
return Array.from(this.sessions.values());
}
async getLLMProvider(providerName) {
if (providerName) {
const provider = LLMFactory.getProvider(providerName);
// Ensure the provider is configured
if (!(await provider.isConfigured())) {
throw new Error(`${providerName} provider is not configured. Please configure it first.`);
}
return provider;
}
// Try to get a configured provider
const configuredProvider = await LLMFactory.getConfiguredProvider();
if (configuredProvider) {
return configuredProvider;
}
throw new Error('No LLM provider configured. Please configure a provider first.');
}
getActualProviderName(llmProvider) {
// Get the provider name from the LLM provider instance
const config = llmProvider.getConfig();
// Check if the provider has a name in its config
if (config && config.name) {
return config.name;
}
// Fallback: try to determine from the constructor name
const constructorName = llmProvider.constructor.name;
if (constructorName.endsWith('Provider')) {
return constructorName.replace('Provider', '');
}
// Last resort: return a default
return 'OpenAI'; // Default to OpenAI as it's most common
}
generateSessionId() {
return `chat_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}
generateMessageId() {
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
}
}