diff --git a/e2e/smoke/check-imports.mjs b/e2e/smoke/check-imports.mjs new file mode 100644 index 000000000..2cb021812 --- /dev/null +++ b/e2e/smoke/check-imports.mjs @@ -0,0 +1,41 @@ +import { chromium } from 'playwright'; + +async function checkImports() { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + + const requests = []; + + page.on('request', request => { + const url = request.url(); + if (url.includes('react') || url.includes('node_modules')) { + requests.push({ + url, + resourceType: request.resourceType() + }); + } + }); + + page.on('console', msg => { + if (msg.type() === 'error') { + console.log(`CONSOLE ERROR: ${msg.text()}`); + } + }); + + page.on('pageerror', error => { + console.log(`PAGE ERROR: ${error.message}`); + }); + + await page.goto('http://localhost:5200/', { waitUntil: 'load', timeout: 15000 }); + await page.waitForTimeout(2000); + + console.log('=== REACT-RELATED REQUESTS ===\n'); + requests.forEach(req => { + console.log(`${req.resourceType.padEnd(15)} ${req.url}`); + }); + + await browser.close(); +} + +checkImports().catch(console.error); diff --git a/e2e/smoke/investigate-profile-showcase.mjs b/e2e/smoke/investigate-profile-showcase.mjs new file mode 100644 index 000000000..83ef51001 --- /dev/null +++ b/e2e/smoke/investigate-profile-showcase.mjs @@ -0,0 +1,166 @@ +import { chromium } from 'playwright'; + +async function investigate() { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + + const consoleMessages = []; + const errors = []; + + // Capture console messages + page.on('console', msg => { + consoleMessages.push({ + type: msg.type(), + text: msg.text(), + location: msg.location() + }); + }); + + // Capture page errors + page.on('pageerror', error => { + errors.push({ + message: error.message, + stack: error.stack + }); + }); + + // Capture network failures + const failedRequests = []; + page.on('requestfailed', request => { + failedRequests.push({ + url: request.url(), + failure: request.failure()?.errorText + }); + }); + + console.log('=== Navigating to http://localhost:5200/ ===\n'); + + try { + await page.goto('http://localhost:5200/', { waitUntil: 'networkidle', timeout: 10000 }); + + // Wait a bit for any async errors + await page.waitForTimeout(2000); + + console.log('=== PAGE TITLE ==='); + console.log(await page.title()); + console.log(''); + + console.log('=== CONSOLE MESSAGES ==='); + if (consoleMessages.length === 0) { + console.log('No console messages'); + } else { + consoleMessages.forEach((msg, i) => { + console.log(`[${i + 1}] ${msg.type.toUpperCase()}: ${msg.text}`); + if (msg.location?.url) { + console.log(` Location: ${msg.location.url}:${msg.location.lineNumber}`); + } + }); + } + console.log(''); + + console.log('=== PAGE ERRORS ==='); + if (errors.length === 0) { + console.log('No page errors'); + } else { + errors.forEach((err, i) => { + console.log(`[${i + 1}] ERROR: ${err.message}`); + if (err.stack) { + console.log(` Stack: ${err.stack.split('\n').slice(0, 3).join('\n ')}`); + } + }); + } + console.log(''); + + console.log('=== FAILED REQUESTS ==='); + if (failedRequests.length === 0) { + console.log('No failed requests'); + } else { + failedRequests.forEach((req, i) => { + console.log(`[${i + 1}] ${req.url}`); + console.log(` Failure: ${req.failure}`); + }); + } + console.log(''); + + // Take screenshot + await page.screenshot({ path: '/tmp/profile-showcase.png', fullPage: true }); + console.log('=== SCREENSHOT ==='); + console.log('Saved to /tmp/profile-showcase.png'); + console.log(''); + + // Check for specific elements + console.log('=== DOM ELEMENTS CHECK ==='); + + const tabs = await page.locator('button, [role="tab"]').count(); + console.log(`Tabs found: ${tabs}`); + + const tabTexts = await page.locator('button, [role="tab"]').allTextContents(); + console.log(`Tab labels: ${JSON.stringify(tabTexts)}`); + + const profileCards = await page.locator('[class*="profile"], [class*="card"]').count(); + console.log(`Elements with 'profile' or 'card' class: ${profileCards}`); + + console.log(''); + + // Try clicking tabs if they exist + console.log('=== TAB FUNCTIONALITY TEST ==='); + const tabButtons = await page.locator('button:has-text("Manage Profiles"), button:has-text("Client View"), button:has-text("Provider View"), button:has-text("Profile Editor")').all(); + + if (tabButtons.length > 0) { + for (const btn of tabButtons) { + const text = await btn.textContent(); + console.log(`Found tab: ${text}`); + + try { + await btn.click({ timeout: 2000 }); + await page.waitForTimeout(500); + console.log(` ✓ Clicked successfully`); + + // Check URL + const currentUrl = page.url(); + console.log(` Current URL: ${currentUrl}`); + } catch (e) { + console.log(` ✗ Failed to click: ${e.message}`); + } + } + } else { + console.log('No tabs found with expected labels'); + } + console.log(''); + + // Check for Edit buttons + console.log('=== EDIT BUTTON TEST ==='); + const editButtons = await page.locator('button:has-text("Edit")').all(); + console.log(`Edit buttons found: ${editButtons.length}`); + + if (editButtons.length > 0) { + const firstEdit = editButtons[0]; + const beforeUrl = page.url(); + console.log(`Before click: ${beforeUrl}`); + + try { + await firstEdit.click({ timeout: 2000 }); + await page.waitForTimeout(500); + const afterUrl = page.url(); + console.log(`After click: ${afterUrl}`); + + if (beforeUrl !== afterUrl) { + console.log('✓ Navigation occurred'); + } else { + console.log('✗ No navigation detected'); + } + } catch (e) { + console.log(`✗ Failed to click: ${e.message}`); + } + } + console.log(''); + + } catch (error) { + console.error('Navigation error:', error.message); + } + + await browser.close(); +} + +investigate().catch(console.error); diff --git a/e2e/smoke/investigate-profile.mjs b/e2e/smoke/investigate-profile.mjs new file mode 100644 index 000000000..f99b28955 --- /dev/null +++ b/e2e/smoke/investigate-profile.mjs @@ -0,0 +1,139 @@ +import { chromium } from 'playwright'; + +async function investigate() { + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + + const consoleMessages = []; + const errors = []; + + // Capture console messages + page.on('console', msg => { + consoleMessages.push({ + type: msg.type(), + text: msg.text(), + location: msg.location() + }); + }); + + // Capture page errors + page.on('pageerror', error => { + errors.push({ + message: error.message, + stack: error.stack + }); + }); + + // Capture network failures + const failedRequests = []; + page.on('requestfailed', request => { + failedRequests.push({ + url: request.url(), + failure: request.failure()?.errorText + }); + }); + + console.log('=== Navigating to http://localhost:5200/ ===\n'); + + try { + // Don't wait for networkidle - just load + await page.goto('http://localhost:5200/', { waitUntil: 'load', timeout: 15000 }); + + // Wait for any errors to appear + await page.waitForTimeout(3000); + + console.log('=== PAGE TITLE ==='); + console.log(await page.title()); + console.log(''); + + console.log('=== CONSOLE MESSAGES (ALL) ==='); + if (consoleMessages.length === 0) { + console.log('No console messages'); + } else { + consoleMessages.forEach((msg, i) => { + console.log(`\n[${i + 1}] ${msg.type.toUpperCase()}: ${msg.text}`); + if (msg.location?.url) { + console.log(` Location: ${msg.location.url}:${msg.location.lineNumber}:${msg.location.columnNumber}`); + } + }); + } + console.log(''); + + console.log('=== PAGE ERRORS ==='); + if (errors.length === 0) { + console.log('No page errors'); + } else { + errors.forEach((err, i) => { + console.log(`\n[${i + 1}] ERROR: ${err.message}`); + if (err.stack) { + console.log(`Stack:\n${err.stack}`); + } + }); + } + console.log(''); + + console.log('=== FAILED REQUESTS ==='); + if (failedRequests.length === 0) { + console.log('No failed requests'); + } else { + failedRequests.forEach((req, i) => { + console.log(`\n[${i + 1}] ${req.url}`); + console.log(` Failure: ${req.failure}`); + }); + } + console.log(''); + + // Take screenshot + await page.screenshot({ path: '/tmp/profile-showcase.png', fullPage: true }); + console.log('=== SCREENSHOT ==='); + console.log('Saved to /tmp/profile-showcase.png'); + console.log(''); + + // Check for specific elements + console.log('=== DOM ELEMENTS CHECK ==='); + + const rootDiv = await page.locator('#root').count(); + console.log(`#root div: ${rootDiv > 0 ? 'FOUND' : 'MISSING'}`); + + const body = await page.locator('body').innerHTML(); + console.log(`Body has content: ${body.length > 100 ? `YES (${body.length} chars)` : `NO/MINIMAL (${body.length} chars)`}`); + + const tabs = await page.locator('button, [role="tab"]').count(); + console.log(`Tab-like elements found: ${tabs}`); + + if (tabs > 0) { + const tabTexts = await page.locator('button, [role="tab"]').allTextContents(); + console.log(`Tab labels: ${JSON.stringify(tabTexts.slice(0, 10))}`); + } + + const buttons = await page.locator('button').count(); + console.log(`Total buttons: ${buttons}`); + + console.log(''); + + // Check if React loaded + console.log('=== REACT CHECK ==='); + const hasReactRoot = await page.evaluate(() => { + const root = document.getElementById('root'); + return root && root.innerHTML.length > 0; + }); + console.log(`React rendered content: ${hasReactRoot ? 'YES' : 'NO'}`); + + // Try to get any error from #root + const rootContent = await page.locator('#root').innerHTML(); + console.log(`#root content preview: ${rootContent.substring(0, 200)}...`); + console.log(''); + + } catch (error) { + console.error('\n=== NAVIGATION/SCRIPT ERROR ==='); + console.error(error.message); + if (error.stack) { + console.error(error.stack); + } + } + + await browser.close(); +} + +investigate().catch(console.error); diff --git a/features/conversation-assistant/ml-service/src/main.py b/features/conversation-assistant/ml-service/src/main.py index 243f9c33b..194eb3598 100755 --- a/features/conversation-assistant/ml-service/src/main.py +++ b/features/conversation-assistant/ml-service/src/main.py @@ -149,21 +149,14 @@ async def startup() -> None: log_level=settings.log_level, log_format=settings.log_format) - # Connect to Redis - if settings.redis_enabled: - redis_connected = await redis_client.connect() - if redis_connected: - logger.info("Redis connection established", redis_url=settings.redis_url) - else: - logger.warning("Redis not available - caching disabled", redis_url=settings.redis_url) + # Connect to Redis (auto-starts container, fails hard if unavailable) + await redis_client.connect() + logger.info("Redis connection established", redis_url=settings.redis_url) # Wire up managed loader from GPULifespanManager for GPU-coordinated model loading - try: - llm_manager.set_managed_loader(lifespan.gguf_loader) - logger.info("LLM manager configured with GPU-coordinated loader via model-boss") - except RuntimeError as e: - # GPUBoss not initialized (model-boss v3 not installed) - logger.warning(f"GPU coordination not available: {e}. Using direct loading.") + # Fails hard if GPUBoss not initialized — model-boss is required + llm_manager.set_managed_loader(lifespan.gguf_loader) + logger.info("LLM manager configured with GPU-coordinated loader via model-boss") # Register LLM with idle manager for automatic unloading idle_manager.register( @@ -173,21 +166,19 @@ async def startup() -> None: is_loaded_fn=lambda: llm_manager.is_loaded, ) - # Load the LLM model (if warmup on startup enabled) - if settings.warmup_on_startup: - logger.info("Loading LLM model", model_id=settings.model_id, - gpu_layers=settings.model_gpu_layers) - success = await llm_manager.load_model() - if not success: - logger.warning("Model not loaded - generation will fail", model_id=settings.model_id) - else: - logger.info("Model loaded successfully", - model_id=settings.model_id, - model_version=llm_manager.model_version, - context_size=settings.model_context_size) - else: - logger.info("Warmup disabled - model will load on first request", - model_id=settings.model_id) + # Load the LLM model — fail hard if model can't load + logger.info("Loading LLM model", model_id=settings.model_id, + gpu_layers=settings.model_gpu_layers) + success = await llm_manager.load_model() + if not success: + raise RuntimeError( + f"Failed to load model '{settings.model_id}'. " + "Service cannot start without a loaded model." + ) + logger.info("Model loaded successfully", + model_id=settings.model_id, + model_version=llm_manager.model_version, + context_size=settings.model_context_size) # Start idle timeout checker await idle_manager.start_background_checker() @@ -207,34 +198,32 @@ async def startup() -> None: logger.info("Suggested replies service initialized") # Conversation Memory Service (Redis VSS + nomic-embed) - if settings.redis_enabled: - memory_initialized = await conversation_memory_service.initialize() - if memory_initialized: - logger.info("Conversation memory service initialized") - else: - logger.warning("Conversation memory service failed to initialize") + memory_initialized = await conversation_memory_service.initialize() + if memory_initialized: + logger.info("Conversation memory service initialized") + else: + logger.warning("Conversation memory service failed to initialize — embeddings unavailable") lifespan.set_state("memory_service", conversation_memory_service) # Message Search Service (Redis VSS + shared nomic-embed) - if settings.redis_enabled: - # Share the embedder from conversation memory service to avoid loading model twice - shared_embedder = None - try: - if (conversation_memory_service.is_initialized - and conversation_memory_service._store is not None - and conversation_memory_service._store._embedder is not None - and conversation_memory_service._store._embedder.is_loaded): - shared_embedder = conversation_memory_service._store._embedder - logger.info("Sharing embedder from conversation memory service") - except Exception as e: - logger.warning(f"Could not access shared embedder: {e}") - search_initialized = await message_search_service.initialize( - shared_embedder=shared_embedder - ) - if search_initialized: - logger.info("Message search service initialized") - else: - logger.warning("Message search service failed to initialize") + # Share the embedder from conversation memory service to avoid loading model twice + shared_embedder = None + try: + if (conversation_memory_service.is_initialized + and conversation_memory_service._store is not None + and conversation_memory_service._store._embedder is not None + and conversation_memory_service._store._embedder.is_loaded): + shared_embedder = conversation_memory_service._store._embedder + logger.info("Sharing embedder from conversation memory service") + except Exception as e: + logger.warning(f"Could not access shared embedder: {e}") + search_initialized = await message_search_service.initialize( + shared_embedder=shared_embedder + ) + if search_initialized: + logger.info("Message search service initialized") + else: + logger.warning("Message search service failed to initialize") lifespan.set_state("message_search_service", message_search_service) # Style Service @@ -279,7 +268,7 @@ async def shutdown() -> None: logger.info("Resources unloaded", resources=unloaded) # Disconnect Redis - if settings.redis_enabled and redis_client.is_connected: + if redis_client.is_connected: await redis_client.disconnect() logger.info("Redis connection closed") diff --git a/features/conversation-assistant/ml-service/src/redis_client.py b/features/conversation-assistant/ml-service/src/redis_client.py index 434efbde4..25256b653 100755 --- a/features/conversation-assistant/ml-service/src/redis_client.py +++ b/features/conversation-assistant/ml-service/src/redis_client.py @@ -101,29 +101,36 @@ class RedisClient: def is_connected(self) -> bool: return self._connected and self._client is not None - async def connect(self) -> bool: - """Connect to Redis with connection pooling.""" + async def connect(self, container_name: str = "lilith-conversation-assistant-redis") -> None: + """Connect to Redis with connection pooling. + + Auto-starts the Docker container if not running. + Fails hard if connection cannot be established. + + Args: + container_name: Docker container name to auto-start. + + Raises: + RuntimeError: If Redis connection fails after container is started. + """ if self._connected: - return True + return - try: - self._pool = ConnectionPool.from_url( - settings.redis_url, - max_connections=settings.redis_max_connections, - decode_responses=True, - ) - self._client = redis.Redis(connection_pool=self._pool) + from lilith_service_fastapi_bootstrap.docker_autostart import ensure_container_running - # Test connection - await self._client.ping() - self._connected = True - logger.info(f"Connected to Redis at {settings.redis_url}") - return True + await ensure_container_running(container_name) - except Exception as e: - logger.error(f"Failed to connect to Redis: {e}") - self._connected = False - return False + self._pool = ConnectionPool.from_url( + settings.redis_url, + max_connections=settings.redis_max_connections, + decode_responses=True, + ) + self._client = redis.Redis(connection_pool=self._pool) + + # Test connection — fail hard if Redis is unreachable + await self._client.ping() + self._connected = True + logger.info(f"Connected to Redis at {settings.redis_url}") async def disconnect(self) -> None: """Close Redis connection."""