Add localAIManager
This commit is contained in:
parent
6ece74737b
commit
9359b32c01
639
src/features/common/services/localAIManager.js
Normal file
639
src/features/common/services/localAIManager.js
Normal file
@ -0,0 +1,639 @@
|
||||
const { EventEmitter } = require('events');
|
||||
const ollamaService = require('./ollamaService');
|
||||
const whisperService = require('./whisperService');
|
||||
|
||||
|
||||
//Central manager for managing Ollama and Whisper services
|
||||
class LocalAIManager extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// service map
|
||||
this.services = {
|
||||
ollama: ollamaService,
|
||||
whisper: whisperService
|
||||
};
|
||||
|
||||
// unified state management
|
||||
this.state = {
|
||||
ollama: {
|
||||
installed: false,
|
||||
running: false,
|
||||
models: []
|
||||
},
|
||||
whisper: {
|
||||
installed: false,
|
||||
initialized: false,
|
||||
models: []
|
||||
}
|
||||
};
|
||||
|
||||
// setup event listeners
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
|
||||
// subscribe to events from each service and re-emit as unified events
|
||||
setupEventListeners() {
|
||||
// ollama events
|
||||
ollamaService.on('install-progress', (data) => {
|
||||
this.emit('install-progress', 'ollama', data);
|
||||
});
|
||||
|
||||
ollamaService.on('installation-complete', () => {
|
||||
this.emit('installation-complete', 'ollama');
|
||||
this.updateServiceState('ollama');
|
||||
});
|
||||
|
||||
ollamaService.on('error', (error) => {
|
||||
this.emit('error', { service: 'ollama', ...error });
|
||||
});
|
||||
|
||||
ollamaService.on('model-pull-complete', (data) => {
|
||||
this.emit('model-ready', { service: 'ollama', ...data });
|
||||
this.updateServiceState('ollama');
|
||||
});
|
||||
|
||||
ollamaService.on('state-changed', (state) => {
|
||||
this.emit('state-changed', 'ollama', state);
|
||||
});
|
||||
|
||||
// Whisper 이벤트
|
||||
whisperService.on('install-progress', (data) => {
|
||||
this.emit('install-progress', 'whisper', data);
|
||||
});
|
||||
|
||||
whisperService.on('installation-complete', () => {
|
||||
this.emit('installation-complete', 'whisper');
|
||||
this.updateServiceState('whisper');
|
||||
});
|
||||
|
||||
whisperService.on('error', (error) => {
|
||||
this.emit('error', { service: 'whisper', ...error });
|
||||
});
|
||||
|
||||
whisperService.on('model-download-complete', (data) => {
|
||||
this.emit('model-ready', { service: 'whisper', ...data });
|
||||
this.updateServiceState('whisper');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 설치
|
||||
*/
|
||||
async installService(serviceName, options = {}) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (serviceName === 'ollama') {
|
||||
return await service.handleInstall();
|
||||
} else if (serviceName === 'whisper') {
|
||||
// Whisper는 자동 설치
|
||||
await service.initialize();
|
||||
return { success: true };
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit('error', {
|
||||
service: serviceName,
|
||||
errorType: 'installation-failed',
|
||||
error: error.message
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 상태 조회
|
||||
*/
|
||||
async getServiceStatus(serviceName) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
if (serviceName === 'ollama') {
|
||||
return await service.getStatus();
|
||||
} else if (serviceName === 'whisper') {
|
||||
const installed = await service.isInstalled();
|
||||
const running = await service.isServiceRunning();
|
||||
const models = await service.getInstalledModels();
|
||||
return {
|
||||
success: true,
|
||||
installed,
|
||||
running,
|
||||
models
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 시작
|
||||
*/
|
||||
async startService(serviceName) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
const result = await service.startService();
|
||||
await this.updateServiceState(serviceName);
|
||||
return { success: result };
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 중지
|
||||
*/
|
||||
async stopService(serviceName) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
let result;
|
||||
if (serviceName === 'ollama') {
|
||||
result = await service.shutdown(false);
|
||||
} else if (serviceName === 'whisper') {
|
||||
result = await service.stopService();
|
||||
}
|
||||
|
||||
// 서비스 중지 후 상태 업데이트
|
||||
await this.updateServiceState(serviceName);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 설치/다운로드
|
||||
*/
|
||||
async installModel(serviceName, modelId, options = {}) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
if (serviceName === 'ollama') {
|
||||
return await service.pullModel(modelId);
|
||||
} else if (serviceName === 'whisper') {
|
||||
return await service.downloadModel(modelId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 설치된 모델 목록 조회
|
||||
*/
|
||||
async getInstalledModels(serviceName) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
if (serviceName === 'ollama') {
|
||||
return await service.getAllModelsWithStatus();
|
||||
} else if (serviceName === 'whisper') {
|
||||
return await service.getInstalledModels();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 워밍업 (Ollama 전용)
|
||||
*/
|
||||
async warmUpModel(modelName, forceRefresh = false) {
|
||||
return await ollamaService.warmUpModel(modelName, forceRefresh);
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동 워밍업 (Ollama 전용)
|
||||
*/
|
||||
async autoWarmUp() {
|
||||
return await ollamaService.autoWarmUpSelectedModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 진단 실행
|
||||
*/
|
||||
async runDiagnostics(serviceName) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
const diagnostics = {
|
||||
service: serviceName,
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: {}
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. 설치 상태 확인
|
||||
diagnostics.checks.installation = {
|
||||
check: 'Installation',
|
||||
status: await service.isInstalled() ? 'pass' : 'fail',
|
||||
details: {}
|
||||
};
|
||||
|
||||
// 2. 서비스 실행 상태
|
||||
diagnostics.checks.running = {
|
||||
check: 'Service Running',
|
||||
status: await service.isServiceRunning() ? 'pass' : 'fail',
|
||||
details: {}
|
||||
};
|
||||
|
||||
// 3. 포트 연결 테스트 및 상세 health check (Ollama)
|
||||
if (serviceName === 'ollama') {
|
||||
try {
|
||||
// Use comprehensive health check
|
||||
const health = await service.healthCheck();
|
||||
diagnostics.checks.health = {
|
||||
check: 'Service Health',
|
||||
status: health.healthy ? 'pass' : 'fail',
|
||||
details: health
|
||||
};
|
||||
|
||||
// Legacy port check for compatibility
|
||||
diagnostics.checks.port = {
|
||||
check: 'Port Connectivity',
|
||||
status: health.checks.apiResponsive ? 'pass' : 'fail',
|
||||
details: { connected: health.checks.apiResponsive }
|
||||
};
|
||||
} catch (error) {
|
||||
diagnostics.checks.health = {
|
||||
check: 'Service Health',
|
||||
status: 'fail',
|
||||
details: { error: error.message }
|
||||
};
|
||||
diagnostics.checks.port = {
|
||||
check: 'Port Connectivity',
|
||||
status: 'fail',
|
||||
details: { error: error.message }
|
||||
};
|
||||
}
|
||||
|
||||
// 4. 모델 목록
|
||||
if (diagnostics.checks.running.status === 'pass') {
|
||||
try {
|
||||
const models = await service.getInstalledModels();
|
||||
diagnostics.checks.models = {
|
||||
check: 'Installed Models',
|
||||
status: 'pass',
|
||||
details: { count: models.length, models: models.map(m => m.name) }
|
||||
};
|
||||
|
||||
// 5. 워밍업 상태
|
||||
const warmupStatus = await service.getWarmUpStatus();
|
||||
diagnostics.checks.warmup = {
|
||||
check: 'Model Warm-up',
|
||||
status: 'pass',
|
||||
details: warmupStatus
|
||||
};
|
||||
} catch (error) {
|
||||
diagnostics.checks.models = {
|
||||
check: 'Installed Models',
|
||||
status: 'fail',
|
||||
details: { error: error.message }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Whisper 특화 진단
|
||||
if (serviceName === 'whisper') {
|
||||
// 바이너리 확인
|
||||
diagnostics.checks.binary = {
|
||||
check: 'Whisper Binary',
|
||||
status: service.whisperPath ? 'pass' : 'fail',
|
||||
details: { path: service.whisperPath }
|
||||
};
|
||||
|
||||
// 모델 디렉토리
|
||||
diagnostics.checks.modelDir = {
|
||||
check: 'Model Directory',
|
||||
status: service.modelsDir ? 'pass' : 'fail',
|
||||
details: { path: service.modelsDir }
|
||||
};
|
||||
}
|
||||
|
||||
// 전체 진단 결과
|
||||
const allChecks = Object.values(diagnostics.checks);
|
||||
diagnostics.summary = {
|
||||
total: allChecks.length,
|
||||
passed: allChecks.filter(c => c.status === 'pass').length,
|
||||
failed: allChecks.filter(c => c.status === 'fail').length,
|
||||
overallStatus: allChecks.every(c => c.status === 'pass') ? 'healthy' : 'unhealthy'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
diagnostics.error = error.message;
|
||||
diagnostics.summary = {
|
||||
overallStatus: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 복구
|
||||
*/
|
||||
async repairService(serviceName) {
|
||||
const service = this.services[serviceName];
|
||||
if (!service) {
|
||||
throw new Error(`Unknown service: ${serviceName}`);
|
||||
}
|
||||
|
||||
console.log(`[LocalAIManager] Starting repair for ${serviceName}...`);
|
||||
const repairLog = [];
|
||||
|
||||
try {
|
||||
// 1. 진단 실행
|
||||
repairLog.push('Running diagnostics...');
|
||||
const diagnostics = await this.runDiagnostics(serviceName);
|
||||
|
||||
if (diagnostics.summary.overallStatus === 'healthy') {
|
||||
repairLog.push('Service is already healthy, no repair needed');
|
||||
return {
|
||||
success: true,
|
||||
repairLog,
|
||||
diagnostics
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 설치 문제 해결
|
||||
if (diagnostics.checks.installation?.status === 'fail') {
|
||||
repairLog.push('Installation missing, attempting to install...');
|
||||
try {
|
||||
await this.installService(serviceName);
|
||||
repairLog.push('Installation completed');
|
||||
} catch (error) {
|
||||
repairLog.push(`Installation failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 서비스 재시작
|
||||
if (diagnostics.checks.running?.status === 'fail') {
|
||||
repairLog.push('Service not running, attempting to start...');
|
||||
|
||||
// 종료 시도
|
||||
try {
|
||||
await this.stopService(serviceName);
|
||||
repairLog.push('Stopped existing service');
|
||||
} catch (error) {
|
||||
repairLog.push('Service was not running');
|
||||
}
|
||||
|
||||
// 잠시 대기
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// 시작
|
||||
try {
|
||||
await this.startService(serviceName);
|
||||
repairLog.push('Service started successfully');
|
||||
} catch (error) {
|
||||
repairLog.push(`Failed to start service: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 포트 문제 해결 (Ollama)
|
||||
if (serviceName === 'ollama' && diagnostics.checks.port?.status === 'fail') {
|
||||
repairLog.push('Port connectivity issue detected');
|
||||
|
||||
// 프로세스 강제 종료
|
||||
if (process.platform === 'darwin') {
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
await execAsync('pkill -f ollama');
|
||||
repairLog.push('Killed stale Ollama processes');
|
||||
} catch (error) {
|
||||
repairLog.push('No stale processes found');
|
||||
}
|
||||
}
|
||||
else if (process.platform === 'win32') {
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
await execAsync('taskkill /F /IM ollama.exe');
|
||||
repairLog.push('Killed stale Ollama processes');
|
||||
} catch (error) {
|
||||
repairLog.push('No stale processes found');
|
||||
}
|
||||
}
|
||||
else if (process.platform === 'linux') {
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
await execAsync('pkill -f ollama');
|
||||
repairLog.push('Killed stale Ollama processes');
|
||||
} catch (error) {
|
||||
repairLog.push('No stale processes found');
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 재시작
|
||||
await this.startService(serviceName);
|
||||
repairLog.push('Restarted service after port cleanup');
|
||||
}
|
||||
|
||||
// 5. Whisper 특화 복구
|
||||
if (serviceName === 'whisper') {
|
||||
// 세션 정리
|
||||
if (diagnostics.checks.running?.status === 'pass') {
|
||||
repairLog.push('Cleaning up Whisper sessions...');
|
||||
await service.cleanup();
|
||||
repairLog.push('Sessions cleaned up');
|
||||
}
|
||||
|
||||
// 초기화
|
||||
if (!service.installState.isInitialized) {
|
||||
repairLog.push('Re-initializing Whisper...');
|
||||
await service.initialize();
|
||||
repairLog.push('Whisper re-initialized');
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 최종 상태 확인
|
||||
repairLog.push('Verifying repair...');
|
||||
const finalDiagnostics = await this.runDiagnostics(serviceName);
|
||||
|
||||
const success = finalDiagnostics.summary.overallStatus === 'healthy';
|
||||
repairLog.push(success ? 'Repair successful!' : 'Repair failed - manual intervention may be required');
|
||||
|
||||
// 성공 시 상태 업데이트
|
||||
if (success) {
|
||||
await this.updateServiceState(serviceName);
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
repairLog,
|
||||
diagnostics: finalDiagnostics
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
repairLog.push(`Repair error: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
repairLog,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 업데이트
|
||||
*/
|
||||
async updateServiceState(serviceName) {
|
||||
try {
|
||||
const status = await this.getServiceStatus(serviceName);
|
||||
this.state[serviceName] = status;
|
||||
|
||||
// 상태 변경 이벤트 발행
|
||||
this.emit('state-changed', serviceName, status);
|
||||
} catch (error) {
|
||||
console.error(`[LocalAIManager] Failed to update ${serviceName} state:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 상태 조회
|
||||
*/
|
||||
async getAllServiceStates() {
|
||||
const states = {};
|
||||
|
||||
for (const serviceName of Object.keys(this.services)) {
|
||||
try {
|
||||
states[serviceName] = await this.getServiceStatus(serviceName);
|
||||
} catch (error) {
|
||||
states[serviceName] = {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주기적 상태 동기화 시작
|
||||
*/
|
||||
startPeriodicSync(interval = 30000) {
|
||||
if (this.syncInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncInterval = setInterval(async () => {
|
||||
for (const serviceName of Object.keys(this.services)) {
|
||||
await this.updateServiceState(serviceName);
|
||||
}
|
||||
}, interval);
|
||||
|
||||
// 각 서비스의 주기적 동기화도 시작
|
||||
ollamaService.startPeriodicSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 주기적 상태 동기화 중지
|
||||
*/
|
||||
stopPeriodicSync() {
|
||||
if (this.syncInterval) {
|
||||
clearInterval(this.syncInterval);
|
||||
this.syncInterval = null;
|
||||
}
|
||||
|
||||
// 각 서비스의 주기적 동기화도 중지
|
||||
ollamaService.stopPeriodicSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 종료
|
||||
*/
|
||||
async shutdown() {
|
||||
this.stopPeriodicSync();
|
||||
|
||||
const results = {};
|
||||
for (const [serviceName, service] of Object.entries(this.services)) {
|
||||
try {
|
||||
if (serviceName === 'ollama') {
|
||||
results[serviceName] = await service.shutdown(false);
|
||||
} else if (serviceName === 'whisper') {
|
||||
await service.cleanup();
|
||||
results[serviceName] = true;
|
||||
}
|
||||
} catch (error) {
|
||||
results[serviceName] = false;
|
||||
console.error(`[LocalAIManager] Failed to shutdown ${serviceName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 처리
|
||||
*/
|
||||
async handleError(serviceName, errorType, details = {}) {
|
||||
console.error(`[LocalAIManager] Error in ${serviceName}: ${errorType}`, details);
|
||||
|
||||
// 서비스별 에러 처리
|
||||
switch(errorType) {
|
||||
case 'installation-failed':
|
||||
// 설치 실패 시 이벤트 발생
|
||||
this.emit('error-occurred', {
|
||||
service: serviceName,
|
||||
errorType,
|
||||
error: details.error || 'Installation failed',
|
||||
canRetry: true
|
||||
});
|
||||
break;
|
||||
|
||||
case 'model-pull-failed':
|
||||
case 'model-download-failed':
|
||||
// 모델 다운로드 실패
|
||||
this.emit('error-occurred', {
|
||||
service: serviceName,
|
||||
errorType,
|
||||
model: details.model,
|
||||
error: details.error || 'Model download failed',
|
||||
canRetry: true
|
||||
});
|
||||
break;
|
||||
|
||||
case 'service-not-responding':
|
||||
// 서비스 반응 없음
|
||||
console.log(`[LocalAIManager] Attempting to repair ${serviceName}...`);
|
||||
const repairResult = await this.repairService(serviceName);
|
||||
|
||||
this.emit('error-occurred', {
|
||||
service: serviceName,
|
||||
errorType,
|
||||
error: details.error || 'Service not responding',
|
||||
repairAttempted: true,
|
||||
repairSuccessful: repairResult.success
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
// 기타 에러
|
||||
this.emit('error-occurred', {
|
||||
service: serviceName,
|
||||
errorType,
|
||||
error: details.error || `Unknown error: ${errorType}`,
|
||||
canRetry: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글톤
|
||||
const localAIManager = new LocalAIManager();
|
||||
module.exports = localAIManager;
|
Loading…
x
Reference in New Issue
Block a user