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
JavaScript
"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