UNPKG

stock-nse-india

Version:

This package will help us to get equity/index details and historical data from National Stock Exchange of India.

778 lines 35 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createMCPClient = exports.getMcpClient = exports.MCPClient = void 0; const openai_1 = __importDefault(require("openai")); const index_js_1 = require("../../index.js"); const mcp_tools_js_1 = require("../mcp-tools.js"); const memory_manager_js_1 = require("../memory-manager.js"); /** * Unified MCP Client with OpenAI Function Calling, Memory, and Context Summarization * * This is the single, unified MCP client that combines all features: * - OpenAI function calling for natural language queries * - Memory management for context awareness * - Context summarization for handling long conversations * - Session management for multi-user support */ class MCPClient { constructor(config = {}) { this.allToolsUsed = []; // Track all tools used across iterations this.config = { enableMemory: true, enableContextSummarization: true, enableDebugLogging: false, ...config }; // Initialize NSE India client this.nseClient = new index_js_1.NseIndia(); // Initialize available tools this.availableTools = mcp_tools_js_1.mcpTools; } getOpenAI() { if (!this.openaiClient) { const apiKey = this.config.openaiApiKey || process.env.OPENAI_API_KEY; if (!apiKey) { throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable.'); } this.openaiClient = new openai_1.default({ apiKey }); } return this.openaiClient; } getMemoryManager() { if (!this.memoryManager) { this.memoryManager = new memory_manager_js_1.MemoryManager(this.config.memoryConfig); } return this.memoryManager; } /** * Debug logging method */ debugLog(message, data) { if (this.config.enableDebugLogging) { // eslint-disable-next-line no-console console.log(`[MCP DEBUG] ${message}`); if (data) { // eslint-disable-next-line no-console console.log(`[MCP DEBUG] Data:`, JSON.stringify(data, null, 2)); } } } /** * Enable or disable debug logging on this client instance */ setDebugLogging(enabled) { this.config.enableDebugLogging = enabled; } /** * Get current debug logging status */ isDebugLoggingEnabled() { return this.config.enableDebugLogging === true; } /** * Convert MCP tools to OpenAI function format */ convertToolsToOpenAIFunctions() { return this.availableTools.map(tool => ({ name: tool.name, description: tool.description, parameters: tool.inputSchema })); } /** * Extract stock symbols from query for context tracking */ extractStockSymbols(query) { const symbolRegex = /\b[A-Z]{2,10}\b/g; const matches = query.match(symbolRegex) || []; const commonWords = ['THE', 'AND', 'OR', 'FOR', 'WITH', 'FROM', 'TO', 'IN', 'ON', 'AT', 'BY']; return matches.filter(symbol => !commonWords.includes(symbol)); } /** * Get base system prompt without context */ getBaseSystemPrompt() { return 'You are a helpful assistant that provides information about Indian stock market data from NSE India. ' + 'You have access to various tools to fetch real-time market data, stock information, ' + 'historical data, and more. ' + 'When a user asks a question, use the appropriate tools to gather the necessary data ' + 'and provide a comprehensive answer. ' + 'Always format your responses in a clear, professional manner with proper markdown ' + 'formatting when appropriate.'; } /** * Process a natural language query with multi-iteration support (default: 5 iterations) */ async processQuery(request) { var _a, _b, _c, _d; const { query, sessionId, userId, model = 'gpt-4o-mini', temperature = 0.7, max_tokens = 2000, includeContext = true, updatePreferences = true, useMemory = true, maxIterations = 5 } = request; try { // Initialize query tracking for smart iteration decisions this.currentQuery = query; this.allToolsUsed = []; // Determine if we should use memory features const shouldUseMemory = !!(useMemory && this.config.enableMemory && sessionId); let session = null; let conversationContext = { messages: [], wasSummarized: false, tokenCount: { totalTokens: 0 } }; // Initialize session and memory if enabled if (shouldUseMemory) { session = this.getMemoryManager().getOrCreateSession(sessionId, userId); // Add user message to conversation history const userMessage = { role: 'user', content: query, timestamp: new Date().toISOString() }; this.getMemoryManager().addMessage(sessionId, userMessage); // Extract stock symbols for context tracking const stockSymbols = this.extractStockSymbols(query); stockSymbols.forEach(symbol => { this.getMemoryManager().updateStockAccess(sessionId, symbol); }); // Get conversation history with context summarization if (includeContext) { conversationContext = await this.getMemoryManager().getConversationContext(sessionId); } } // Get system prompt (contextual if memory is enabled) const systemPrompt = shouldUseMemory && includeContext ? this.getMemoryManager().getContextualSystemPrompt(sessionId) : this.getBaseSystemPrompt(); // Initialize iteration tracking let currentIteration = 0; const uniqueToolsUsed = new Set(); // Use Set for true uniqueness const iterationDetails = []; // Build initial messages array const allMessages = [ { role: 'system', content: systemPrompt } ]; // Add conversation history if memory is enabled if (shouldUseMemory && includeContext) { conversationContext.messages.forEach((msg) => { if (msg.role === 'assistant' && msg.content) { allMessages.push({ role: 'assistant', content: msg.content }); } else if (msg.role === 'user' && msg.content !== query) { allMessages.push({ role: 'user', content: msg.content }); } else if (msg.role === 'system' && msg.content) { allMessages.push({ role: 'system', content: msg.content }); } }); } // Add current user query allMessages.push({ role: 'user', content: query }); // Convert MCP tools to OpenAI functions const functions = this.convertToolsToOpenAIFunctions(); // Main iteration loop while (currentIteration < maxIterations) { currentIteration++; this.debugLog(`Iteration ${currentIteration}/${maxIterations}`); this.debugLog('Sending messages to OpenAI:', allMessages); const response = await this.getOpenAI().chat.completions.create({ model, messages: allMessages, tools: functions.map(fn => ({ type: 'function', function: fn })), tool_choice: 'auto', temperature, max_tokens }); this.debugLog('OpenAI Response:', { model: response.model, usage: response.usage, message: (_a = response.choices[0]) === null || _a === void 0 ? void 0 : _a.message }); const message = (_b = response.choices[0]) === null || _b === void 0 ? void 0 : _b.message; if (!message) { throw new Error(`No response from OpenAI in iteration ${currentIteration}`); } // Add assistant message to conversation allMessages.push({ role: 'assistant', content: message.content, tool_calls: message.tool_calls }); // Check if tools were called if (message.tool_calls && message.tool_calls.length > 0) { const iterationTools = message.tool_calls.map(tc => tc.function.name); this.debugLog(`Iteration ${currentIteration}: Calling ${iterationTools.length} tools: ${iterationTools.join(', ')}`); // Log tool parameters for debugging message.tool_calls.forEach((toolCall, index) => { this.debugLog(`Tool ${index + 1}: ${toolCall.function.name}`); try { const params = JSON.parse(toolCall.function.arguments); this.debugLog(`Parameters:`, params); } catch (e) { this.debugLog(`Invalid JSON parameters: ${toolCall.function.arguments}`); } }); // Execute all function calls in parallel const functionCallPromises = message.tool_calls.map(async (toolCall) => { this.debugLog(`Executing tool: ${toolCall.function.name}`); const functionResult = await this.executeFunctionCall(toolCall); this.debugLog(`Tool ${toolCall.function.name} result:`, functionResult); return { toolCall, result: functionResult }; }); const functionCallResults = await Promise.all(functionCallPromises); this.debugLog(`Completed ${functionCallResults.length} tool executions`); // Add unique tools to Set (automatically handles duplicates) iterationTools.forEach(tool => { uniqueToolsUsed.add(tool); }); // Track all tools (including duplicates) for internal logic this.allToolsUsed.push(...iterationTools); // Capture tool parameters for iteration details const toolParameters = message.tool_calls.map(toolCall => ({ tool_name: toolCall.function.name, parameters: (() => { try { return JSON.parse(toolCall.function.arguments); } catch (e) { return { raw_arguments: toolCall.function.arguments, parse_error: e instanceof Error ? e.message : String(e) }; } })() })); // Add iteration details iterationDetails.push({ iteration: currentIteration, tools_called: iterationTools, purpose: this.inferIterationPurpose(iterationTools, currentIteration), tool_parameters: toolParameters }); // Add tool results to conversation functionCallResults.forEach(({ toolCall, result }) => { allMessages.push({ role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(result) }); }); // Check if we should continue iterating if (this.shouldContinueIterating(message, currentIteration, maxIterations)) { // Add instruction for next iteration with specific guidance let nextInstruction = `You now have additional data from iteration ${currentIteration}. `; // Provide specific guidance based on the original query and current context if (query.toLowerCase().includes('technical indicators') && !this.allToolsUsed.includes('get_equity_technical_indicators')) { nextInstruction += `IMPORTANT: The user specifically asked about technical indicators ` + `for investment decisions. You MUST call get_equity_technical_indicators for several ` + `promising stocks from the data you received. Do not provide investment recommendations ` + `without actual technical analysis data.`; } else if (query.toLowerCase().includes('invest') && currentIteration <= 2) { nextInstruction += `The user is asking for investment recommendations. You need to ` + `analyze the data more deeply by calling get_equity_technical_indicators or ` + `get_equity_details for specific stocks to make informed recommendations.`; } else if (query.toLowerCase().includes('nifty') && query.toLowerCase().includes('invest')) { nextInstruction += `You have NIFTY stock data. Now you need to select promising ` + `stocks and get their technical indicators by calling get_equity_technical_indicators ` + `for informed investment recommendations.`; } else { nextInstruction += `If you need more specific information to provide a comprehensive ` + `answer, call the appropriate tools. If you have sufficient data, provide your final analysis.`; } allMessages.push({ role: 'system', content: nextInstruction }); continue; } else { // Final synthesis iteration: always perform a final completion and return allMessages.push({ role: 'system', content: 'Based on all the data gathered from previous tool calls, provide your ' + 'comprehensive final analysis and recommendations.' }); const finalResponse = await this.getOpenAI().chat.completions.create({ model, messages: allMessages, temperature, max_tokens }); const finalMessage = ((_d = (_c = finalResponse.choices[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) || 'Unable to generate final response'; // Add final response to conversation history if memory is enabled if (shouldUseMemory) { const assistantMessage = { role: 'assistant', content: finalMessage, timestamp: new Date().toISOString(), tools_used: Array.from(uniqueToolsUsed), metadata: { model, temperature, max_tokens, iterations_used: currentIteration + 1 } }; this.getMemoryManager().addMessage(sessionId, assistantMessage); } return this.buildFinalResponse(finalMessage, Array.from(uniqueToolsUsed), currentIteration + 1, iterationDetails, request, shouldUseMemory, session, conversationContext, updatePreferences || false); } } else { // No tools called, this is the final response // Iteration ${currentIteration}: No more tools needed, providing final response const finalResponse = message.content || 'Unable to process query'; // Add final response to conversation history if memory is enabled if (shouldUseMemory) { const assistantMessage = { role: 'assistant', content: finalResponse, timestamp: new Date().toISOString(), tools_used: Array.from(uniqueToolsUsed), metadata: { model, temperature, max_tokens, iterations_used: currentIteration } }; this.getMemoryManager().addMessage(sessionId, assistantMessage); } return this.buildFinalResponse(finalResponse, Array.from(uniqueToolsUsed), currentIteration, iterationDetails, request, shouldUseMemory, session, conversationContext, updatePreferences || false); } } throw new Error(`Query exceeded maximum iterations (${maxIterations}). ` + `Consider simplifying the query or increasing maxIterations.`); } catch (error) { throw new Error(`MCP Client Error: ${error instanceof Error ? error.message : String(error)}`); } } /** * Execute function calls using the NSE India API */ async executeFunctionCall(toolCall) { const { function: { name, arguments: args } } = toolCall; try { const parsedArgs = typeof args === 'string' ? JSON.parse(args) : args; const result = await (0, mcp_tools_js_1.handleMCPToolCall)(this.nseClient, name, parsedArgs); return result; } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } } /** * Determine if iterations should continue based on AI response and context */ shouldContinueIterating(message, currentIteration, maxIterations) { var _a, _b; // Don't exceed max iterations (save one for final synthesis) if (currentIteration >= maxIterations - 1) return false; // Always continue for specific complex query patterns that need multiple steps const originalQuery = ((_a = this.currentQuery) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || ''; // AGGRESSIVE: For technical indicator investment queries, force continuation until we get technical data if (originalQuery.includes('technical indicators') && originalQuery.includes('invest')) { // Continue until we've called technical indicators tool OR reached iteration 4 if (!this.allToolsUsed.includes('get_equity_technical_indicators') && currentIteration <= 3) { // Forcing continuation for technical indicators query (iteration ${currentIteration}) return true; } } // AGGRESSIVE: For NIFTY investment queries, ensure we go deeper than just index lookup if (originalQuery.includes('nifty') && originalQuery.includes('invest') && currentIteration <= 2) { // Forcing continuation for NIFTY investment query (iteration ${currentIteration}) return true; } // Check if assistant indicates more work is needed const content = ((_b = message.content) === null || _b === void 0 ? void 0 : _b.toLowerCase()) || ''; const needsMoreData = content.includes('i need to') || content.includes('let me analyze') || content.includes('i should get') || content.includes('need more information') || content.includes('let me check') || content.includes('i will analyze') || content.includes('let me gather') || content.includes('additional data needed'); // For investment/analysis queries, be more aggressive about continuing const isInvestmentQuery = content.includes('invest') || content.includes('recommend') || content.includes('technical') || content.includes('analysis') || content.includes('indicators'); // If it's an investment query and we're early in iterations, continue if (isInvestmentQuery && currentIteration <= 2) { return true; } return needsMoreData; } /** * Infer the purpose of an iteration based on tools called */ inferIterationPurpose(tools, iteration) { if (tools.includes('get_equity_stock_indices')) { return 'Getting index composition data'; } else if (tools.includes('get_equity_technical_indicators')) { return 'Analyzing technical indicators'; } else if (tools.includes('get_equity_details') || tools.includes('get_equity_trade_info')) { return 'Gathering stock details and trade information'; } else if (tools.includes('get_market_status')) { return 'Checking market status'; } else if (tools.includes('get_all_stock_symbols')) { return 'Getting available stock symbols'; } else { return `Data collection (iteration ${iteration})`; } } /** * Build the final response with all tracking information */ buildFinalResponse(finalResponse, toolsUsed, iterationsUsed, iterationDetails, request, shouldUseMemory, session, conversationContext, updatePreferences) { var _a; // Update user preferences if requested and memory is enabled let preferencesUpdated = false; if (shouldUseMemory && updatePreferences && request.sessionId) { preferencesUpdated = this.updateUserPreferencesFromQuery(request.sessionId, request.query, toolsUsed); } // Build response const clientResponse = { response: finalResponse, tools_used: toolsUsed, data_sources: ['NSE India API via MCP'], timestamp: new Date().toISOString(), iterations_used: iterationsUsed, iteration_details: iterationDetails }; // Add memory-related fields if memory is enabled if (shouldUseMemory) { clientResponse.sessionId = request.sessionId; clientResponse.context_used = request.includeContext; clientResponse.user_preferences_updated = preferencesUpdated; clientResponse.conversation_length = ((_a = session === null || session === void 0 ? void 0 : session.conversationHistory) === null || _a === void 0 ? void 0 : _a.length) || 0; clientResponse.context_summarized = conversationContext.wasSummarized; clientResponse.context_summary = conversationContext.summary; clientResponse.token_count = conversationContext.tokenCount; } return clientResponse; } /** * Update user preferences based on query patterns */ updateUserPreferencesFromQuery(sessionId, query, toolsUsed) { if (!this.config.enableMemory) return false; const session = this.getMemoryManager().getOrCreateSession(sessionId); let updated = false; const queryLower = query.toLowerCase(); // Detect analysis style preference if (queryLower.includes('brief') || queryLower.includes('summary')) { if (session.userPreferences.analysisStyle !== 'brief') { session.userPreferences.analysisStyle = 'brief'; updated = true; } } else if (queryLower.includes('detailed') || queryLower.includes('comprehensive')) { if (session.userPreferences.analysisStyle !== 'detailed') { session.userPreferences.analysisStyle = 'detailed'; updated = true; } } else if (queryLower.includes('technical') || queryLower.includes('indicators')) { if (session.userPreferences.analysisStyle !== 'technical') { session.userPreferences.analysisStyle = 'technical'; updated = true; } } // Detect preferred stocks from query const stockSymbols = this.extractStockSymbols(query); stockSymbols.forEach(symbol => { if (!session.userPreferences.preferredStocks.includes(symbol)) { session.userPreferences.preferredStocks.push(symbol); if (session.userPreferences.preferredStocks.length > 10) { session.userPreferences.preferredStocks = session.userPreferences.preferredStocks.slice(-10); } updated = true; } }); // Detect preferred indices const indexKeywords = ['nifty', 'banknifty', 'sensex', 'midcap', 'smallcap']; indexKeywords.forEach(keyword => { if (queryLower.includes(keyword)) { const indexName = keyword.toUpperCase(); if (!session.userPreferences.preferredIndices.includes(indexName)) { session.userPreferences.preferredIndices.push(indexName); updated = true; } } }); if (updated) { this.getMemoryManager().updatePreferences(sessionId, session.userPreferences); } return updated; } // ============================================================================ // Memory Management Methods (only available if memory is enabled) // ============================================================================ /** * Get session information */ getSessionInfo(sessionId) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().getSessionStats(sessionId); } /** * Update user preferences manually */ updateUserPreferences(sessionId, preferences) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } this.getMemoryManager().updatePreferences(sessionId, preferences); } /** * Get conversation history */ getConversationHistory(sessionId, maxMessages) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().getConversationContextSync(sessionId, maxMessages); } /** * Get conversation history with context summarization */ async getConversationHistoryWithSummarization(sessionId, maxMessages, systemPrompt) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().getConversationContext(sessionId, maxMessages, systemPrompt); } /** * Clear session data */ clearSession(sessionId) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } this.getMemoryManager().clearSession(sessionId); } /** * Export session data */ exportSessionData(sessionId) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().exportSessionData(sessionId); } /** * Check if context needs summarization */ async needsContextSummarization(sessionId, systemPrompt) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().needsContextSummarization(sessionId, systemPrompt); } /** * Get context statistics */ async getContextStats(sessionId, systemPrompt) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().getContextStats(sessionId, systemPrompt); } /** * Force context summarization */ async forceContextSummarization(sessionId, systemPrompt) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().forceContextSummarization(sessionId, systemPrompt); } /** * Update context window configuration */ updateContextWindowConfig(config) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } this.getMemoryManager().updateContextWindowConfig(config); } /** * Get context window configuration */ getContextWindowConfig() { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } return this.getMemoryManager().getContextWindowConfig(); } /** * Cleanup expired sessions */ cleanupExpiredSessions() { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } this.getMemoryManager().cleanupExpiredSessions(); } // ============================================================================ // Utility Methods // ============================================================================ /** * Get available tools information */ getAvailableTools() { return this.availableTools; } /** * Get tools in OpenAI function format */ getOpenAIFunctions() { return this.convertToolsToOpenAIFunctions(); } /** * Test the MCP client connection */ async testConnection() { try { await this.processQuery({ query: 'What is the current market status?', useMemory: false }); return true; } catch (error) { console.error('MCP Client test failed:', error); return false; } } /** * Test the MCP client with memory */ async testConnectionWithMemory(sessionId) { if (!this.config.enableMemory) { throw new Error('Memory is not enabled for this MCP client'); } try { await this.processQuery({ query: 'What is the current market status?', sessionId, useMemory: true, includeContext: false }); return true; } catch (error) { console.error('MCP Client with Memory test failed:', error); return false; } } /** * Get client configuration */ getConfig() { return { ...this.config }; } /** * Check if memory is enabled */ isMemoryEnabled() { return this.config.enableMemory === true; } /** * Check if context summarization is enabled */ isContextSummarizationEnabled() { return this.config.enableContextSummarization === true && this.config.enableMemory === true; } /** * Get last summarization for a session */ getLastSummarization(sessionId) { if (!this.config.enableMemory) return null; return this.getMemoryManager().getLastSummarization(sessionId); } /** * Get summarization history for a session */ getSummarizationHistory(sessionId, limit) { if (!this.config.enableMemory) return []; return this.getMemoryManager().getSummarizationHistory(sessionId, limit); } /** * Get summarization summary for a session */ getSummarizationSummary(sessionId) { if (!this.config.enableMemory) return null; return this.getMemoryManager().getSummarizationSummary(sessionId); } /** * Get OpenAI messages for a session (including system message) */ getOpenAIMessages(sessionId) { if (!this.config.enableMemory) return null; const session = this.getMemoryManager().getOrCreateSession(sessionId); const systemPrompt = this.getMemoryManager().getContextualSystemPrompt(sessionId); return { systemPrompt, conversationHistory: session.conversationHistory }; } } exports.MCPClient = MCPClient; let sharedMcpClient; /** Lazy singleton — does not require OPENAI_API_KEY until MCP AI routes are used. */ function getMcpClient() { if (!sharedMcpClient) { sharedMcpClient = new MCPClient({ enableMemory: true, enableContextSummarization: true }); } return sharedMcpClient; } exports.getMcpClient = getMcpClient; // Export factory function for custom configurations function createMCPClient(config) { return new MCPClient(config); } exports.createMCPClient = createMCPClient; //# sourceMappingURL=mcp-client.js.map