semem
Version:
Semantic Memory for Intelligent Agents
1,234 lines (1,055 loc) • 52.9 kB
JavaScript
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import logger from 'loglevel';
import SearchService from './SearchService.js';
import EmbeddingService from '../embeddings/EmbeddingService.js';
import SPARQLService from '../embeddings/SPARQLService.js';
import APIRegistry from '../../api/common/APIRegistry.js';
import ChatAPI from '../../api/features/ChatAPI.js';
import MemoryManager from '../../MemoryManager.js';
import LLMHandler from '../../handlers/LLMHandler.js';
import OllamaConnector from '../../connectors/OllamaConnector.js';
import HClaudeClientConnector from '../../connectors/ClaudeConnector.js';
import MistralConnector from '../../connectors/MistralConnector.js';
/**
* UI server application that serves the web interface and API endpoints
*/
class UIServer {
/**
* Creates a new UIServer
* @param {Object} options - Configuration options
* @param {number} options.port - The port to listen on
* @param {string} options.graphName - The graph name to search in
*/
constructor(options = {}) {
this.port = options.port || 4100;
this.graphName = options.graphName || 'http://danny.ayers.name/content';
this.chatModel = options.chatModel || 'qwen2:1.5b';
this.embeddingModel = options.embeddingModel || 'nomic-embed-text';
// Configure SPARQL endpoints (list of servers to try in order)
this.sparqlEndpoints = options.sparqlEndpoints || [
{
queryEndpoint: 'http://localhost:4030/semem/query',
updateEndpoint: 'http://localhost:4030/semem/update',
auth: {
user: 'admin',
password: 'admin123'
}
},
{
queryEndpoint: 'http://localhost:3030/semem/query',
updateEndpoint: 'http://localhost:3030/semem/update',
auth: {
user: 'admin',
password: 'admin'
}
}
];
// Configure LLM provider endpoints
this.llmProviders = options.llmProviders || [
{
type: 'mistral',
apiKey: process.env.MISTRAL_API_KEY || '',
baseUrl: process.env.MISTRAL_API_BASE || 'https://api.mistral.ai/v1',
chatModel: process.env.MISTRAL_MODEL || 'mistral-medium',
priority: 1
},
{
type: 'claude',
implementation: 'hyperdata', // Uses hyperdata-clients library
apiKey: process.env.CLAUDE_API_KEY || '',
chatModel: process.env.CLAUDE_MODEL || 'claude-3-opus-20240229',
priority: 2
},
{
type: 'ollama',
baseUrl: 'http://localhost:11434',
chatModel: this.chatModel,
embeddingModel: this.embeddingModel,
priority: 3
},
{
type: 'claude',
implementation: 'direct', // Uses direct API connection
apiKey: process.env.CLAUDE_API_KEY || '',
baseUrl: process.env.CLAUDE_API_BASE || 'https://api.anthropic.com',
chatModel: process.env.CLAUDE_MODEL || 'claude-3-opus-20240229',
priority: 4
},
{
type: 'openai',
apiKey: process.env.OPENAI_API_KEY || '',
chatModel: 'gpt-3.5-turbo',
embeddingModel: 'text-embedding-3-small',
priority: 5
}
];
// Separate default providers for chat and embedding
this.defaultChatProvider = this.llmProviders.find(p => p.type === 'mistral') ||
this.llmProviders.find(p => p.type === 'claude' && p.implementation === 'direct') ||
this.llmProviders.find(p => p.type === 'claude' && p.implementation === 'hyperdata') ||
this.llmProviders.find(p => p.type === 'ollama') ||
this.llmProviders[0];
this.defaultEmbeddingProvider = this.llmProviders.find(p => p.type === 'ollama') ||
this.llmProviders.find(p => p.type === 'openai') ||
this.llmProviders[0];
// Initialize services
this.embeddingService = new EmbeddingService();
// Initialize SPARQL service with first endpoint, others will be tried if this fails
this.sparqlService = new SPARQLService({
queryEndpoint: this.sparqlEndpoints[0].queryEndpoint,
updateEndpoint: this.sparqlEndpoints[0].updateEndpoint,
graphName: this.graphName,
auth: this.sparqlEndpoints[0].auth
});
this.searchService = new SearchService({
embeddingService: this.embeddingService,
sparqlService: this.sparqlService,
graphName: this.graphName
});
// Initialize API registry
this.apiRegistry = new APIRegistry();
// Create Express app
this.app = express();
// Get directory name for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Calculate paths for project root and public directory
this.projectRoot = path.resolve(__dirname, '..', '..', '..');
this.publicDir = path.join(this.projectRoot, 'public');
logger.info(`UIServer initialized with port: ${this.port}, graph: ${this.graphName}`);
}
/**
* Configure the Express app
*/
configureApp() {
// Middleware
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(express.static(this.publicDir));
// API endpoint for searching
this.app.get('/api/search', this.handleSearch.bind(this));
// Add health check endpoint
this.app.get('/api/health', this.handleHealthCheck.bind(this));
// Chat endpoints
this.app.post('/api/chat', this.handleChat.bind(this));
this.app.post('/api/chat/stream', this.handleChatStream.bind(this));
this.app.post('/api/chat/completion', this.handleChatCompletion.bind(this));
// Memory endpoints
this.app.post('/api/memory', this.handleMemoryStore.bind(this));
this.app.get('/api/memory/search', this.handleMemorySearch.bind(this));
this.app.post('/api/memory/embedding', this.handleEmbedding.bind(this));
this.app.post('/api/memory/concepts', this.handleConcepts.bind(this));
// Provider endpoints
this.app.get('/api/providers', this.handleListProviders.bind(this));
// HTML route for the UI
this.app.get('/', (req, res) => {
res.sendFile(path.join(this.publicDir, 'index.html'));
});
}
/**
* Handle search API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleSearch(req, res) {
try {
const query = req.query.q || '';
const limit = parseInt(req.query.limit) || 5;
logger.info(`Search request for: "${query}" with limit: ${limit}`);
if (!query.trim()) {
return res.json({ results: [] });
}
// Perform search
const results = await this.searchService.search(query, limit);
logger.info(`Found ${results.length} results for query: "${query}"`);
res.json({ results });
} catch (error) {
logger.error('Search error:', error);
res.status(500).json({
error: 'Search failed',
message: error.message
});
}
}
/**
* Handle health check requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleHealthCheck(req, res) {
try {
// Check services status safely
let chatStatus = 'offline';
let memoryStatus = 'offline';
try {
if (this.chatAPI) {
chatStatus = 'online';
}
} catch (e) {
logger.warn('Error checking chat API status:', e);
}
try {
if (this.memoryManager) {
memoryStatus = 'online';
}
} catch (e) {
logger.warn('Error checking memory API status:', e);
}
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
services: {
search: 'online',
chat: chatStatus,
memory: memoryStatus
}
};
res.json(health);
} catch (error) {
logger.error('Health check error:', error);
res.status(500).json({
status: 'degraded',
timestamp: new Date().toISOString(),
error: error.message
});
}
}
/**
* Handle chat API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
}
/**
* Handle chat API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleChat(req, res) {
try {
// Validate request
const {
prompt,
conversationId,
useMemory,
temperature,
useSearchInterjection,
providerId
} = req.body;
if (!prompt) {
return res.status(400).json({
error: 'Bad Request',
message: 'Prompt is required'
});
}
logger.info(`Chat request with prompt: "${prompt.slice(0, 30)}..."`);
try {
// Optionally enrich the prompt with search results if requested
let enrichedPrompt = prompt;
let searchResults = [];
// If search interjection is requested, find relevant content
if (useSearchInterjection) {
try {
logger.info('Searching for relevant content to interject...');
searchResults = await this.performEmbeddingSearch(prompt);
if (searchResults && searchResults.length > 0) {
// Format search results as context
const contextBlocks = searchResults.map((result, index) =>
`[DOCUMENT ${index + 1}]\nTitle: ${result.title || 'Untitled'}\nContent: ${result.content}\nScore: ${result.score}\n`
).join('\n');
// Append context to prompt with instructions
enrichedPrompt = `I found some relevant information that might help answer your question. Please consider this information when formulating your response:
${contextBlocks}
Based on the above information and your knowledge, here is the user's question: ${prompt}`;
logger.info(`Enriched prompt with ${searchResults.length} search results`);
}
} catch (searchError) {
logger.warn('Failed to enrich prompt with search results:', searchError);
// Continue with original prompt if search fails
}
}
// Get the selected provider by ID or use the default
let selectedProvider;
// If providerId is in the format 'provider-N', extract the index
if (providerId && providerId.startsWith('provider-')) {
const index = parseInt(providerId.split('-')[1], 10);
if (!isNaN(index) && this.llmProviders && this.llmProviders[index]) {
selectedProvider = this.llmProviders[index];
}
}
// If no provider found by ID, use the default
if (!selectedProvider) {
selectedProvider = this.defaultChatProvider;
logger.warn(`Provider ${providerId} not found, falling back to default: ${selectedProvider?.type}`);
}
if (!selectedProvider) {
throw new Error('No chat provider available');
}
logger.info(`Using provider: ${selectedProvider.type}${selectedProvider.implementation ? ` (${selectedProvider.implementation})` : ''}`);
// Make sure the provider has a connector
if (!selectedProvider.connector) {
logger.warn(`Provider ${selectedProvider.type} has no connector, initializing...`);
// Initialize the connector if it doesn't exist
switch (selectedProvider.type) {
case 'mistral':
selectedProvider.connector = new MistralConnector(
selectedProvider.apiKey,
selectedProvider.baseUrl,
selectedProvider.chatModel
);
break;
case 'claude':
selectedProvider.connector = new HClaudeClientConnector(
selectedProvider.apiKey,
selectedProvider.chatModel
);
break;
case 'ollama':
selectedProvider.connector = new OllamaConnector({
baseUrl: selectedProvider.baseUrl,
chatModel: selectedProvider.chatModel,
embeddingModel: selectedProvider.embeddingModel
});
break;
default:
throw new Error(`Unsupported provider type: ${selectedProvider.type}`);
}
logger.info(`Initialized connector for provider: ${selectedProvider.type}`);
}
// Use the model from the request or fall back to the provider's default
const modelToUse = req.body.model || selectedProvider.chatModel;
logger.info(`Using model: ${modelToUse} for provider: ${selectedProvider.type}`);
// Create chat API with selected provider and model
const chatAPI = await this.createChatAPI(selectedProvider, modelToUse);
// Generate response with the selected model
const result = await chatAPI.executeOperation('chat', {
prompt: enrichedPrompt, // Use the potentially enriched prompt
conversationId,
useMemory: useMemory !== false, // Default to true if not specified
temperature: temperature || 0.7,
model: modelToUse // Use the selected model
});
// Add search results to the response
if (searchResults && searchResults.length > 0) {
result.searchResults = searchResults.map(r => ({
title: r.title || 'Untitled',
content: r.content.substring(0, 200) + (r.content.length > 200 ? '...' : ''),
score: r.score
}));
}
// Return response
res.json(result);
} catch (apiError) {
logger.error('Chat API error:', apiError);
// Fallback: Use direct LLM if chatAPI fails
if (this.llmHandler) {
logger.info('Using LLM fallback for chat request');
const response = await this.llmHandler.generateResponse(
prompt,
"", // No context in fallback mode
"I'm an AI assistant. How can I help you today?"
);
res.json({
response,
conversationId: null,
fallback: true
});
} else {
throw apiError;
}
}
} catch (error) {
logger.error('Chat error:', error);
res.status(500).json({
error: 'Chat request failed',
message: error.message
});
}
}
/**
* Perform embedding search to find relevant content
* @param {string} query - The search query
* @param {number} limit - Maximum number of results to return (default: 3)
* @param {number} threshold - Similarity threshold (default: 0.6)
* @returns {Promise<Array>} - Array of search results with content and scores
*/
async performEmbeddingSearch(query, limit = 3, threshold = 0.6) {
try {
logger.info(`Performing embedding search for: "${query.slice(0, 30)}..."`);
// Use the search service to find relevant content
const results = await this.searchService.search(query, limit, threshold);
// Return formatted results
return results.map(result => ({
title: result.title || '',
content: result.content || '',
type: result.type || 'unknown',
score: result.score || 0
}));
} catch (error) {
logger.error('Embedding search error:', error);
return []; // Return empty array on error
}
}
/**
* Handle streaming chat API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleChatStream(req, res) {
try {
// Validate request
const {
prompt,
conversationId,
useMemory,
temperature,
useSearchInterjection,
providerId
} = req.body;
if (!prompt) {
return res.status(400).json({
error: 'Bad Request',
message: 'Prompt is required'
});
}
logger.info(`Chat stream request with prompt: "${prompt.slice(0, 30)}..."`);
// Set up SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// Optionally enrich the prompt with search results if requested
let enrichedPrompt = prompt;
let searchResults = [];
// If search interjection is requested, find relevant content
if (useSearchInterjection) {
try {
logger.info('Searching for relevant content to interject...');
// Send event about searching
res.write(`data: ${JSON.stringify({ info: "Searching for relevant content..." })}\n\n`);
searchResults = await this.performEmbeddingSearch(prompt);
if (searchResults && searchResults.length > 0) {
// Format search results as context
const contextBlocks = searchResults.map((result, index) =>
`[DOCUMENT ${index + 1}]\nTitle: ${result.title || 'Untitled'}\nContent: ${result.content}\nScore: ${result.score}\n`
).join('\n');
// Append context to prompt with instructions
enrichedPrompt = `I found some relevant information that might help answer your question. Please consider this information when formulating your response:
${contextBlocks}
Based on the above information and your knowledge, here is the user's question: ${prompt}`;
logger.info(`Enriched prompt with ${searchResults.length} search results`);
// Send event about found results
res.write(`data: ${JSON.stringify({
info: `Found ${searchResults.length} relevant documents`,
searchResults: searchResults.map(r => ({
title: r.title || 'Untitled',
snippet: r.content.substring(0, 150) + (r.content.length > 150 ? '...' : ''),
score: r.score
}))
})}\n\n`);
} else {
res.write(`data: ${JSON.stringify({ info: "No relevant content found, using general knowledge" })}\n\n`);
}
} catch (searchError) {
logger.warn('Failed to enrich prompt with search results:', searchError);
// Continue with original prompt if search fails
res.write(`data: ${JSON.stringify({ info: "Search failed, using general knowledge" })}\n\n`);
}
}
// Get the selected provider or use the default
const selectedProvider = this.chatProviders?.find(p => p.id === providerId) || this.chatProviders?.[0];
if (!selectedProvider) {
throw new Error('No chat provider available');
}
logger.info(`Using provider: ${selectedProvider.type}${selectedProvider.implementation ? ` (${selectedProvider.implementation})` : ''}`);
// Create chat API with selected provider
const chatAPI = this.createChatAPI(selectedProvider, selectedProvider.chatModel);
// Send event that we're generating a response
res.write(`data: ${JSON.stringify({ info: "Generating response..." })}\n\n`);
// Generate streaming response
const stream = await chatAPI.executeOperation('stream', {
prompt: enrichedPrompt, // Use the potentially enriched prompt
conversationId,
useMemory: useMemory !== false,
temperature: temperature || 0.7
});
// Handle stream events
stream.on('data', (data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
stream.on('error', (error) => {
logger.error('Chat stream error:', error);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
});
stream.on('end', () => {
if (searchResults && searchResults.length > 0) {
// Send final message with sources
res.write(`data: ${JSON.stringify({
sources: searchResults.map(r => ({
title: r.title || 'Untitled',
snippet: r.content.substring(0, 150) + (r.content.length > 150 ? '...' : ''),
score: r.score
}))
})}\n\n`);
}
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
res.end();
});
// Handle client disconnect
req.on('close', () => {
stream.removeAllListeners();
res.end();
});
} catch (apiError) {
logger.error('Chat API streaming error:', apiError);
// Fallback: Use direct LLM if chatAPI fails and simulate streaming
if (this.llmHandler) {
logger.info('Using LLM fallback for chat stream request');
// Use non-streaming response in fallback mode
try {
res.write(`data: ${JSON.stringify({ info: "Using fallback response generation..." })}\n\n`);
const response = await this.llmHandler.generateResponse(
prompt,
"", // No context in fallback mode
"I'm an AI assistant. How can I help you today?"
);
// Simulate streaming by sending the whole response at once
res.write(`data: ${JSON.stringify({ chunk: response })}\n\n`);
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
res.end();
} catch (fallbackError) {
logger.error('Fallback LLM error:', fallbackError);
res.write(`data: ${JSON.stringify({ error: 'Fallback LLM failed: ' + fallbackError.message })}\n\n`);
res.end();
}
} else {
res.write(`data: ${JSON.stringify({ error: 'Chat API unavailable: ' + apiError.message })}\n\n`);
res.end();
}
}
} catch (error) {
logger.error('Chat stream error:', error);
// For non-SSE requests that error out before headers are sent
if (!res.headersSent) {
res.status(500).json({
error: 'Chat stream request failed',
message: error.message
});
} else {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
}
}
/**
* Handle chat completion API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleChatCompletion(req, res) {
try {
// Validate request
const {
prompt,
max_tokens,
temperature,
providerId
} = req.body;
if (!prompt) {
return res.status(400).json({
error: 'Bad Request',
message: 'Prompt is required'
});
}
logger.info(`Chat completion request with prompt: "${prompt.slice(0, 30)}..."`);
// Get the selected provider or use the default
const selectedProvider = this.chatProviders?.find(p => p.id === providerId) || this.chatProviders?.[0];
if (!selectedProvider) {
throw new Error('No chat provider available');
}
logger.info(`Using provider: ${selectedProvider.type}${selectedProvider.implementation ? ` (${selectedProvider.implementation})` : ''}`);
// Create chat API with selected provider
const chatAPI = this.createChatAPI(selectedProvider, selectedProvider.chatModel);
// Generate completion
const result = await chatAPI.executeOperation('completion', {
prompt,
max_tokens: max_tokens || 100,
temperature: temperature || 0.7
});
// Return response
res.json(result);
} catch (error) {
logger.error('Chat completion error:', error);
res.status(500).json({
error: 'Chat completion request failed',
message: error.message
});
}
}
/**
* Handle memory store API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleMemoryStore(req, res) {
try {
// Validate request
const { prompt, response, metadata } = req.body;
if (!prompt || !response) {
return res.status(400).json({
error: 'Bad Request',
message: 'Both prompt and response are required'
});
}
logger.info('Memory store request received');
// Get memory manager
const memoryManager = this.apiRegistry.get('memory');
// Generate embedding and concepts
const embedding = await memoryManager.generateEmbedding(`${prompt} ${response}`);
const concepts = await memoryManager.extractConcepts(`${prompt} ${response}`);
// Store in memory
await memoryManager.addInteraction(prompt, response, embedding, concepts);
// Return response
res.json({
id: memoryManager.memStore.shortTermMemory[memoryManager.memStore.shortTermMemory.length - 1].id,
timestamp: Date.now(),
concepts
});
} catch (error) {
logger.error('Memory store error:', error);
res.status(500).json({
error: 'Memory store failed',
message: error.message
});
}
}
/**
* Handle memory search API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleMemorySearch(req, res) {
try {
// Get query parameters
const query = req.query.query || '';
const limit = parseInt(req.query.limit) || 5;
const threshold = parseFloat(req.query.threshold) || 0.7;
if (!query.trim()) {
return res.json({ results: [] });
}
logger.info(`Memory search request for: "${query}" with threshold: ${threshold}`);
// Get memory manager
const memoryManager = this.apiRegistry.get('memory');
// Retrieve memories
const memories = await memoryManager.retrieveRelevantInteractions(query, threshold);
// Limit results and format response
const results = memories.slice(0, limit).map(memory => ({
id: memory.interaction.id,
prompt: memory.interaction.prompt,
output: memory.interaction.output,
score: memory.similarity,
timestamp: memory.interaction.timestamp,
concepts: memory.interaction.concepts
}));
res.json({ results });
} catch (error) {
logger.error('Memory search error:', error);
res.status(500).json({
error: 'Memory search failed',
message: error.message
});
}
}
/**
* Handle embedding generation API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleEmbedding(req, res) {
try {
// Validate request
const { text, model } = req.body;
if (!text) {
return res.status(400).json({
error: 'Bad Request',
message: 'Text is required'
});
}
logger.info('Embedding generation request received');
// Get memory manager
const memoryManager = this.apiRegistry.get('memory');
// Generate embedding
const embedding = await memoryManager.generateEmbedding(text);
// Return response
res.json({
embedding,
dimension: embedding.length,
model: model || memoryManager.embeddingModel
});
} catch (error) {
logger.error('Embedding generation error:', error);
res.status(500).json({
error: 'Embedding generation failed',
message: error.message
});
}
}
/**
* Handle concept extraction API requests
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleConcepts(req, res) {
try {
// Validate request
const { text } = req.body;
if (!text) {
return res.status(400).json({
error: 'Bad Request',
message: 'Text is required'
});
}
logger.info('Concept extraction request received');
// Get memory manager
const memoryManager = this.apiRegistry.get('memory');
// Extract concepts
const concepts = await memoryManager.extractConcepts(text);
// Return response
res.json({ concepts });
} catch (error) {
logger.error('Concept extraction error:', error);
res.status(500).json({
error: 'Concept extraction failed',
message: error.message
});
}
}
/**
* Start the server
* @returns {Promise<void>}
*/
async start() {
try {
// Configure the app
this.configureApp();
// Initialize the search service with fallback capability
logger.info('Initializing search service...');
await this.initializeSearchServiceWithFallback();
// Initialize LLM and chat features with fallback capability
logger.info('Initializing LLM and chat features...');
await this.initializeChatFeatures();
// Start the Express server
this.server = this.app.listen(this.port, () => {
logger.info(`UI server running at http://localhost:${this.port}`);
});
} catch (error) {
logger.error('Failed to start server:', error);
throw error;
}
}
/**
* Initialize search service with fallback to alternative SPARQL endpoints if needed
*/
async initializeSearchServiceWithFallback() {
let lastError = null;
// Try each SPARQL endpoint in order until one works
for (let i = 0; i < this.sparqlEndpoints.length; i++) {
const endpoint = this.sparqlEndpoints[i];
try {
logger.info(`Trying SPARQL endpoint ${i + 1}/${this.sparqlEndpoints.length}: ${endpoint.queryEndpoint}`);
// If not the first endpoint, create a new SPARQL service with this endpoint
if (i > 0) {
this.sparqlService = new SPARQLService({
queryEndpoint: endpoint.queryEndpoint,
updateEndpoint: endpoint.updateEndpoint,
graphName: this.graphName,
auth: endpoint.auth
});
// Update the search service with the new SPARQL service
this.searchService = new SearchService({
embeddingService: this.embeddingService,
sparqlService: this.sparqlService,
graphName: this.graphName
});
}
// Try to initialize with this endpoint
await this.searchService.initialize();
// If we get here, initialization succeeded
logger.info(`Successfully connected to SPARQL endpoint: ${endpoint.queryEndpoint}`);
return;
} catch (error) {
lastError = error;
logger.warn(`Failed to connect to SPARQL endpoint ${endpoint.queryEndpoint}:`, error.message);
}
}
// If we get here, all endpoints failed
logger.error(`All SPARQL endpoints failed. Last error:`, lastError);
throw new Error(`Failed to connect to any SPARQL endpoint. Last error: ${lastError.message}`);
}
/**
* Initialize chat features and register API
*/
async initializeChatFeatures() {
try {
// Try each LLM provider in priority order
const availableProviders = await this.initializeLLMProvidersWithFallback();
if (availableProviders.length === 0) {
throw new Error('No LLM providers available. Unable to initialize chat features.');
}
// Select the best provider for each capability
const chatProvider = this.chatProviders.find(p => p.type === 'claude') || this.chatProviders[0];
const embeddingProvider = this.embeddingProviders.find(p => p.type === 'ollama') || this.embeddingProviders[0];
if (!chatProvider) {
logger.error('No chat provider available');
throw new Error('No chat provider available. Unable to initialize chat features.');
}
if (!embeddingProvider) {
logger.error('No embedding provider available');
throw new Error('No embedding provider available. Unable to initialize memory features.');
}
logger.info(`Using ${chatProvider.type}${chatProvider.implementation ? ` (${chatProvider.implementation})` : ''} for chat`);
logger.info(`Using ${embeddingProvider.type} for embeddings`);
// Create memory manager for semantic memory with separate providers for different functions
logger.info('Initializing memory manager...');
this.memoryManager = new MemoryManager({
llmProvider: chatProvider.connector,
embeddingProvider: embeddingProvider.connector,
chatModel: chatProvider.chatModel,
embeddingModel: embeddingProvider.embeddingModel
});
// Create LLM handler for direct LLM requests
logger.info('Initializing LLM handler...');
this.llmHandler = new LLMHandler(
chatProvider.connector,
chatProvider.chatModel
);
// Create a custom registry for passing dependencies
logger.info('Creating custom registry for chat API...');
try {
// Create a custom registry object instead of using APIRegistry
const chatRegistry = {
get: (name) => {
if (name === 'memory') return this.memoryManager;
if (name === 'llm') return this.llmHandler;
throw new Error(`API ${name} not found`);
}
};
// Register only the chat API
logger.info('Registering Chat API...');
this.chatAPI = new ChatAPI({
registry: chatRegistry,
similarityThreshold: 0.7,
contextWindow: 5
});
// Initialize the chat API
await this.chatAPI.initialize();
// Store in apiRegistry (not using register method)
this.apiRegistry.apis = this.apiRegistry.apis || new Map();
this.apiRegistry.apis.set('chat', this.chatAPI);
// Store all available providers for later use
this.availableLLMProviders = availableProviders;
logger.info('Chat features initialized successfully');
} catch (error) {
logger.error('Failed to initialize chat API:', error);
throw error;
}
} catch (error) {
logger.error('Failed to initialize chat features:', error);
throw error;
}
}
/**
* Handle listing available providers
* @param {Request} req - The Express request
* @param {Response} res - The Express response
*/
async handleListProviders(req, res) {
try {
// Ensure providers are initialized
if (!this.llmProviders || this.llmProviders.length === 0) {
logger.warn('No LLM providers available');
return res.json({ providers: [] });
}
// Define available models for each provider type
const providerModels = {
'mistral': ['mistral-medium', 'mistral-small', 'mistral-tiny'],
'claude': ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'],
'ollama': ['llama3', 'mistral', 'mixtral'],
'openai': ['gpt-4-turbo', 'gpt-4', 'gpt-3.5-turbo']
};
const providers = this.llmProviders.map((p, index) => {
const providerId = `provider-${index}`;
const implementation = p.implementation ? `-${p.implementation}` : '';
const models = providerModels[p.type] || [p.chatModel || 'default'];
return {
id: providerId, // Consistent ID format
type: p.type,
name: `${p.type}${p.implementation ? ` (${p.implementation})` : ''}`,
model: p.chatModel || 'default',
models: models, // Include available models
capabilities: p.capabilities || [],
implementation: p.implementation || 'default'
};
});
logger.info(`Returning ${providers.length} available providers`);
res.json({ providers });
} catch (error) {
logger.error('Error listing providers:', error);
res.status(500).json({
error: 'Failed to list providers',
message: error.message
});
}
}
/**
* Create a chat API instance with the specified provider
* @param {Object} provider - The provider configuration
* @param {string} model - The model to use
* @returns {Promise<Object>} Configured ChatAPI instance
*/
async createChatAPI(provider, model) {
if (!provider.connector) {
throw new Error(`Provider ${provider.type} has no connector`);
}
// Ensure the provider's connector is initialized
try {
await provider.connector.initialize();
logger.debug(`Successfully initialized ${provider.type} connector`);
} catch (error) {
logger.error(`Failed to initialize ${provider.type} connector:`, error);
throw new Error(`Failed to initialize ${provider.type} provider: ${error.message}`);
}
// Verify the connector is properly initialized
if (!provider.connector.client) {
throw new Error(`${provider.type} connector client is not initialized`);
}
try {
const memoryManager = new MemoryManager({
llmProvider: provider.connector,
chatModel: model || provider.chatModel,
embeddingProvider: this.embeddingProviders[0]?.connector,
embeddingModel: this.embeddingProviders[0]?.embeddingModel
});
const llmHandler = new LLMHandler(provider.connector, model || provider.chatModel);
const registry = {
get: (name) => {
if (name === 'memory') return memoryManager;
if (name === 'llm') return llmHandler;
throw new Error(`API ${name} not found`);
}
};
return new ChatAPI({
registry,
similarityThreshold: 0.7,
contextWindow: 5
});
} catch (error) {
logger.error(`Error creating chat API for ${provider.type}:`, error);
throw new Error(`Failed to create chat API: ${error.message}`);
}
}
async initializeLLMProvidersWithFallback() {
const availableProviders = [];
const embeddingProviders = [];
const chatProviders = [];
let providerCounter = 0;
// Try each provider and categorize them based on capabilities
const sortedProviders = [...this.llmProviders].sort((a, b) => a.priority - b.priority);
for (const provider of sortedProviders) {
try {
logger.info(`Trying LLM provider: ${provider.type}`);
// Initialize the appropriate connector based on provider type
let connector;
switch (provider.type) {
case 'mistral':
if (!provider.apiKey) {
logger.warn('Skipping Mistral provider - no API key provided');
continue;
}
// Create and attach the connector to the provider
provider.connector = new MistralConnector(
provider.apiKey,
provider.baseUrl,
provider.chatModel
);
logger.info('Mistral AI connector created and attached to provider');
connector = provider.connector; // Set the local connector variable as well
break;
case 'ollama':
// Create and attach the connector to the provider
provider.connector = new OllamaConnector({
baseUrl: provider.baseUrl,
chatModel: provider.chatModel,
embeddingModel: provider.embeddingModel
});
logger.info('Ollama connector created and attached to provider');
connector = provider.connector; // Set the local connector variable as well
break;
case 'claude':
if (!provider.apiKey) {
logger.warn('Skipping Claude provider - no API key provided');
continue;
}
// Use hyperdata-clients implementation for Claude
provider.connector = new HClaudeClientConnector(
provider.apiKey,
provider.chatModel
);
logger.info('Claude connector created and attached to provider');
connector = provider.connector; // Set the local connector variable as well
break;
case 'openai':
// Placeholder for OpenAI connector - you'll need to implement or import this
if (!provider.apiKey) {
logger.warn('Skipping OpenAI provider - no API key provided');
continue;
}
// For now, we'll skip OpenAI since we don't have the connector implementation
logger.warn('OpenAI provider not yet implemented, skipping');
continue;
default:
logger.warn(`Unknown provider type: ${provider.type}, skipping`);
continue;
}
// Test the connector for chat capabilities
try {
// Try a simple chat test if possible
if (typeof connector.generateChat === 'function') {
// Just check if the function exists, no need to actually call it
logger.info(`${provider.type} provider has chat capabilities`);
// Add to chat providers
chatProviders.push({
...provider,
connector,
capabilities: ['chat']
});
}
} catch (chatError) {
logger.warn(`Provider ${provider.type} chat capability check failed:`, chatError.message);
}
// Test the connector for embedding capabilities
try {
// Only test Ollama for embeddings
if (provider.type === 'ollama') {
await connector.generateEmbedding(
provider.embeddingModel,
'Test embedding generation'
);
logger.info(`${provider.type} provider has embedding capabilities`);
// Add to embedding providers
embeddingProviders.push({
...provider,
connector,
capabilities: ['embedding']
});
}
} catch (embeddingError) {
logger.warn(`Provider ${provider.type} embedding test failed:`, embeddingError.message);
}
// Add to available providers if it has any capabilities
if (
chatProviders.some(p => p.connector === connector) ||
embeddingProviders.some(p => p.connector === connector)