egw-chat-cli-local.jsā¢15.1 kB
#!/usr/bin/env node
const { spawn } = require('child_process');
const readline = require('readline');
require('dotenv').config();
class EGWChatCLILocal {
constructor() {
this.mcpProcess = null;
this.isConnected = false;
this.connectionRetries = 0;
this.maxRetries = 3;
this.llmApiKey = process.env.LLM_API_KEY;
this.llmBaseUrl = process.env.LLM_BASE_URL || 'https://api.openai.com/v1';
this.llmModel = process.env.LLM_MODEL || 'gpt-3.5-turbo';
if (!this.llmApiKey) {
console.error('ā Error: LLM_API_KEY environment variable is required');
console.log('Please set your LLM API key in .env file or environment');
process.exit(1);
}
this.setupMCPConnection();
this.setupChatInterface();
}
setupMCPConnection() {
console.log('š Connecting to Local EGW MCP Server...');
// Connect to our local MCP server via HTTP
this.mcpProcess = spawn('node', [
'../apps/local-server/src/server-universal.js'
], {
stdio: ['pipe', 'pipe', 'pipe'],
shell: true,
cwd: __dirname,
env: {
...process.env,
NODE_OPTIONS: '--max-old-space-size=4096'
}
});
this.mcpProcess.on('error', (error) => {
console.error('ā Failed to connect to MCP server:', error.message);
if (this.connectionRetries < this.maxRetries) {
console.log(`š Retrying connection... (${this.connectionRetries + 1}/${this.maxRetries})`);
this.connectionRetries++;
setTimeout(() => this.setupMCPConnection(), 3000);
} else {
console.error('ā Max connection attempts reached. Please check that local server is running.');
process.exit(1);
}
});
this.mcpProcess.on('close', (code) => {
if (code !== 0) {
console.error('ā MCP server disconnected');
console.log('š You can try again or type "exit" to quit');
this.isConnected = false;
}
});
// Add stderr handling for better debugging
this.mcpProcess.stderr.on('data', (data) => {
const stderr = data.toString();
if (!stderr.includes('Debug:')) {
console.error('š MCP Debug:', stderr);
}
});
// Initialize MCP connection
this.initializeMCPConnection();
}
initializeMCPConnection() {
const initialize = () => {
if (!this.mcpProcess || this.mcpProcess.killed) {
return;
}
// Send initialize request
this.sendMCPRequest({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2025-06-18',
capabilities: {},
clientInfo: {
name: 'egw-chat-cli-local',
version: '1.0.0'
}
}
});
// Wait a bit, then get available tools
setTimeout(() => {
if (!this.mcpProcess || this.mcpProcess.killed) {
return;
}
this.sendMCPRequest({
jsonrpc: '2.0',
id: 2,
method: 'tools/list'
});
}, 2000);
};
// Set up response handler
if (this.mcpProcess) {
let mcpBuffer = '';
this.mcpProcess.stdout.on('data', (data) => {
mcpBuffer += data.toString();
const lines = mcpBuffer.split('\n');
mcpBuffer = lines.pop() || '';
lines.forEach(line => {
if (line.trim()) {
try {
const response = JSON.parse(line);
this.handleMCPResponse(response);
} catch (error) {
// Ignore parsing errors for now
if (!line.includes('Debug:')) {
console.error('š Parse error:', error.message);
}
}
}
});
});
}
// Initial connection attempt
setTimeout(initialize, 2000);
// Set timeout for connection
setTimeout(() => {
if (!this.isConnected && this.mcpProcess && !this.mcpProcess.killed) {
console.log('ā ļø Connection taking longer than expected. Retrying...');
if (this.connectionRetries < this.maxRetries) {
this.connectionRetries++;
this.mcpProcess.kill();
setTimeout(() => this.setupMCPConnection(), 2000);
} else {
console.error('ā Unable to establish MCP connection. Please try again later.');
}
}
}, 15000); // 15 second timeout
}
sendMCPRequest(request) {
if (this.mcpProcess && this.mcpProcess.stdin && !this.mcpProcess.stdin.destroyed) {
try {
this.mcpProcess.stdin.write(JSON.stringify(request) + '\n');
} catch (error) {
console.error('ā Failed to send MCP request:', error.message);
this.isConnected = false;
}
} else {
console.error('ā MCP process not available for writing');
this.isConnected = false;
}
}
setupChatInterface() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'š EGW Chat > '
});
console.log('\nš Welcome to EGW Writings Chat CLI (Local)!');
console.log('š¬ Ask questions about Ellen G. White writings, search for content, or get book information');
console.log('šŖ Type "exit", "quit", or press Ctrl+C to leave\n');
rl.prompt();
rl.on('line', async (input) => {
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
console.log('š Goodbye!');
if (this.mcpProcess) {
this.mcpProcess.kill();
}
rl.close();
process.exit(0);
}
if (input.trim()) {
await this.processUserMessage(input.trim());
}
rl.prompt();
});
rl.on('close', () => {
if (this.mcpProcess) {
this.mcpProcess.kill();
}
process.exit(0);
});
}
async handleMCPResponse(response) {
// Store tools information when received
if (response.id === 2 && response.result && response.result.tools) {
this.availableTools = response.result.tools;
this.isConnected = true;
console.log(`ā
Connected to Local EGW MCP Server! Available tools: ${this.availableTools.map(t => t.name).join(', ')}\n`);
}
}
async processUserMessage(userMessage) {
try {
console.log('š¤ Processing your query...');
// Check if MCP is connected
if (!this.isConnected) {
console.log('ā ļø MCP server not connected. Please wait a moment and try again.');
return;
}
// First, let LLM analyze what MCP tool to use
const toolChoice = await this.chooseToolWithLLM(userMessage);
if (toolChoice.tool) {
console.log(`š Using ${toolChoice.tool} tool to search EGW writings...`);
// Execute chosen MCP tool
const result = await this.executeMCPTool(toolChoice.tool, toolChoice.params);
// Let LLM format response based on tool results
const formattedResponse = await this.formatResponseWithLLM(userMessage, result, toolChoice.tool);
console.log('\nš Answer:');
console.log('ā'.repeat(50));
console.log(formattedResponse);
console.log('ā'.repeat(50) + '\n');
} else {
// Direct chat response without tools
const response = await this.callLLM(userMessage);
console.log('\nš¬ Response:');
console.log('ā'.repeat(50));
console.log(response);
console.log('ā'.repeat(50) + '\n');
}
} catch (error) {
console.error('ā Error processing your request:', error.message);
this.isConnected = false; // Mark as disconnected on error
}
}
async chooseToolWithLLM(userMessage) {
const systemPrompt = `You are an assistant that helps users interact with EGW (Ellen G. White) writings database.
Available MCP tools:
1. search_local - Search EGW writings database for specific content (basic search)
2. get_local_book - Get information about a specific book by ID
3. get_local_content - Get content from a specific book (with pagination)
4. list_local_books - List all available books
5. get_database_stats - Get database statistics
6. find_egw_quotes - Find specific EGW quotes containing a search term with proper filtering for genuine EGW content (BEST for finding quotes)
Analyze user's message and determine if a tool should be used and which one.
IMPORTANT: For quote searches or finding specific EGW writings, prefer "find_egw_quotes" over "search_local" as it provides better filtering and formatting.
SMART SEARCH PRIORITY: When user asks for quotes on a topic, prioritize most likely search term:
- Single words: "love" -> search for "love" first, then "loving", then "charity"
- Single words: "faith" -> search for "faith" first, then "faithful", then "belief"
- Single words: "prayer" -> search for "prayer" first, then "praying", then "pray"
- Single words: "hope" -> search for "hope" first, then "hopeful", then "trust"
- Multi-word phrases: "sunday law" -> search for exact phrase "sunday law" first
- Multi-word phrases: Use exact phrase matching for compound terms
For multi-word queries, use exact phrase as primary search term.
If no results are found for primary term, MCP tool will automatically try variations.
Respond with ONLY a JSON object in this format:
{
"tool": "tool_name or null",
"params": {"param1": "value1", "param2": "value2"} or {}
}
Examples:
- "find quotes about prayer" -> {"tool": "find_egw_quotes", "params": {"query": "prayer", "numQuotes": 3}}
- "quotes on faith" -> {"tool": "find_egw_quotes", "params": {"query": "faith", "numQuotes": 3}}
- "what did EGW say about faith" -> {"tool": "find_egw_quotes", "params": {"query": "faith", "numQuotes": 3}}
- "tell me about book 5" -> {"tool": "get_local_book", "params": {"bookId": 5}}
- "list all books" -> {"tool": "list_local_books", "params": {"limit": 20}}
- "how many books do you have" -> {"tool": "get_database_stats", "params": {}}
- "hello" -> {"tool": null, "params": {}}
- general questions about EGW -> {"tool": null, "params": {}}`;
try {
const response = await this.callLLMAPI(systemPrompt, userMessage);
return JSON.parse(response);
} catch (error) {
console.log('ā ļø Could not determine tool, using direct chat');
return { tool: null, params: {} };
}
}
async executeMCPTool(toolName, params) {
return new Promise((resolve, reject) => {
const requestId = Date.now();
const request = {
jsonrpc: '2.0',
id: requestId,
method: 'tools/call',
params: {
name: toolName,
arguments: params
}
};
let responseHandler;
const timeout = setTimeout(() => {
if (responseHandler) {
this.mcpProcess.stdout.off('data', responseHandler);
}
reject(new Error('Tool execution timeout (30 seconds)'));
}, 30000); // Reduced timeout to 30 seconds
let responseBuffer = '';
responseHandler = (data) => {
responseBuffer += data.toString();
const responses = responseBuffer.split('\n').filter(line => line.trim());
for (const line of responses) {
try {
const response = JSON.parse(line);
if (response.id === requestId) {
clearTimeout(timeout);
this.mcpProcess.stdout.off('data', responseHandler);
if (response.error) {
reject(new Error(response.error.message));
} else {
console.log('š MCP Response received successfully');
resolve(response.result);
}
break;
}
} catch (error) {
// Continue parsing, don't log every parse error
}
}
};
this.mcpProcess.stdout.on('data', responseHandler);
this.sendMCPRequest(request);
});
}
async formatResponseWithLLM(userMessage, toolResult, toolUsed) {
// CRITICAL: For find_egw_quotes, ALWAYS display raw formatted quotes without ANY LLM processing
if (toolUsed === 'find_egw_quotes') {
// Handle different response structures from MCP
let actualResult = toolResult;
// If result is wrapped in content array (MCP response format)
if (toolResult.content && Array.isArray(toolResult.content) && toolResult.content[0] && toolResult.content[0].text) {
try {
actualResult = JSON.parse(toolResult.content[0].text);
} catch (parseError) {
console.log('DEBUG: Could not parse nested content, using original');
}
}
if (actualResult.success && actualResult.formatted_output) {
// Return exact formatted output from database tool - NO SUMMARIZATION, NO COMMENTS
return actualResult.formatted_output;
} else if (actualResult.success === false) {
// Return error message directly without LLM processing
return `ā ${actualResult.message || 'No quotes found'}`;
} else {
// Fallback for unexpected result format
return JSON.stringify(actualResult, null, 2);
}
}
// For all other tools, use LLM for formatting
const systemPrompt = `You are a helpful assistant that provides information about Ellen G. White's writings.
The user asked: "${userMessage}"
Tool used: ${toolUsed}
Tool result: ${JSON.stringify(toolResult)}
Please provide a helpful, friendly response based on tool results. If search results were returned, summarize the findings and provide context. If book information was requested, present it clearly. If statistics were requested, explain what they mean.
Be conversational and helpful. Focus on spiritual and practical insights from EGW's writings.`;
return await this.callLLMAPI(systemPrompt, '');
}
async callLLMAPI(systemPrompt, userMessage) {
// Use OpenAI SDK for compatibility
const OpenAI = require('openai');
const client = new OpenAI({
apiKey: this.llmApiKey,
baseURL: this.llmBaseUrl
});
const messages = [
{ role: 'system', content: systemPrompt },
...(userMessage ? [{ role: 'user', content: userMessage }] : [])
];
try {
const completion = await client.chat.completions.create({
model: this.llmModel,
messages: messages,
max_tokens: 1000,
temperature: 0.7
});
return completion.choices[0].message.content;
} catch (error) {
throw new Error(`LLM API error: ${error.message}`);
}
}
async callLLM(message) {
return await this.callLLMAPI(
'You are a helpful assistant knowledgeable about Ellen G. White and her writings. Provide helpful, accurate information about her spiritual insights, books, and teachings.',
message
);
}
}
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\nš Goodbye!');
process.exit(0);
});
// Start CLI
if (require.main === module) {
new EGWChatCLILocal();
}
module.exports = EGWChatCLILocal;