Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 4 additions & 36 deletions distiller_cm5_python/client/mid_layer/llm_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,10 @@ def __init__(
if (
not self._check_cloud_api_connection_sync()
): # Use sync check during init
# logger.error(f"LLMClient.__init__: Could not connect to API at {self.server_url}")
raise UserVisibleError(
f"Could not connect to API at {self.server_url}. Check URL and API key."
# Log a warning, but don't raise an error immediately.
# Let the first actual request fail if connection is terrible.
logger.warning(
f"LLMClient.__init__: Initial check failed for {self.provider_type} API at {self.server_url}. Client will proceed, but requests may fail."
)
else:
logger.error(
Expand Down Expand Up @@ -448,39 +449,6 @@ def _check_cloud_api_connection_sync(self) -> bool:
logger.error(f"Unexpected error during sync cloud connection check: {e}")
return False

# Async version for internal use during API calls if needed later (currently unused)
async def _check_cloud_api_connection_async(self) -> bool:
"""Asynchronously check connection for cloud-based APIs (e.g., OpenRouter)."""
endpoint = self._get_endpoint(self.models_url)
headers = self._get_headers()
if not headers.get("Authorization"):
logger.error(
f"Cannot check connection async to {self.server_url}: API key is missing."
)
return False

try:
async with aiohttp.ClientSession() as session:
async with session.get(
endpoint, timeout=10, headers=headers
) as response:
if response.status == 200:
return True
else:
response_text = await response.text()
logger.warning(
f"Async cloud API connection check failed at {endpoint}. Status: {response.status}, Response: {response_text[:100]}..."
)
return False
except aiohttp.ClientError as e:
logger.warning(
f"Async cloud API connection check failed at {endpoint}. Error: {e}"
)
return False
except Exception as e:
logger.error(f"Unexpected error during async cloud connection check: {e}")
return False

def _get_headers(self) -> Dict[str, str]:
"""Get headers for API requests, including Authorization if api_key is set."""
headers = {"Content-Type": "application/json"}
Expand Down
36 changes: 33 additions & 3 deletions distiller_cm5_python/client/mid_layer/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,39 @@ async def connect_to_server(self, server_script_path: str) -> bool:
)
return False
else:
# For other provider types, just set connected
self._is_connected = True
return True
# For other provider types, explicitly check connection
logger.info(
f"Verifying connection for LLM provider: {self.llm_provider.provider_type} at {self.llm_provider.server_url}"
)
if self.llm_provider.check_connection():
logger.info(
f"Successfully connected to LLM provider: {self.llm_provider.provider_type}"
)
self._is_connected = True
if self.dispatcher:
self.dispatcher.dispatch(
StatusEvent(
type=EventType.INFO,
content=f"Successfully connected to LLM provider: {self.llm_provider.provider_type}",
status=StatusType.SUCCESS,
component="llm_connection",
)
)
return True
else:
error_msg = f"Failed to connect to LLM provider: {self.llm_provider.provider_type} at {self.llm_provider.server_url}. Please check configuration and network."
logger.error(error_msg)
if self.dispatcher:
self.dispatcher.dispatch(
StatusEvent(
type=EventType.ERROR,
content=error_msg,
status=StatusType.FAILED,
component="llm_connection",
)
)
self._is_connected = False
return False

except Exception as e:
logger.error(f"Failed to connect to server: {e}")
Expand Down
44 changes: 20 additions & 24 deletions distiller_cm5_python/client/ui/App.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,18 @@ async def initialize(self):
self._apply_window_constraints()
# E-Ink Initialization Call
self._init_eink_renderer()
self._eink_initialized = True

# Add a small delay to allow QML/FocusManager to potentially settle
await asyncio.sleep(0.2) # Wait 200ms
logger.info("Proceeding to start input monitor after short delay.")
if self._eink_initialized: # Check if E-Ink initialized successfully
# Add a small delay to allow QML/FocusManager to potentially settle
await asyncio.sleep(0.2) # Wait 200ms
logger.info("Proceeding to start input monitor after short delay.")

# Set the target window for the input monitor
self.input_monitor.set_target_window(self.main_window)
# Start the input monitor with default device name
self.input_monitor.start()
# Set the target window for the input monitor
self.input_monitor.set_target_window(self.main_window)
# Start the input monitor with default device name
self.input_monitor.start()
else:
logger.warning("E-Ink initialization failed (self._eink_initialized is False). Input monitor will not be started.")

logger.info("Application initialized successfully")

Expand Down Expand Up @@ -527,13 +529,9 @@ def _init_eink_renderer(self):
# Set adaptive capture mode
self.eink_renderer.set_adaptive_capture(adaptive_capture)

# Connect the signal to an async lambda that schedules the handler
# Use asyncio.create_task to run the async handler without blocking the signal emission
self.eink_renderer.frameReady.connect(
lambda data, w, h: asyncio.create_task(
self._handle_eink_frame(data, w, h)
)
)
# Connect the signal to a synchronous handler that uses Qt's thread pool
# This avoids threading issues with asyncio.create_task
self.eink_renderer.frameReady.connect(self._handle_eink_frame_sync)

# Start capturing frames
self.eink_renderer.start()
Expand All @@ -558,10 +556,10 @@ def _init_eink_renderer(self):
self.eink_renderer = None
return False

async def _handle_eink_frame(self, frame_data, width, height):
def _handle_eink_frame_sync(self, frame_data, width, height):
"""
Handle a new frame from the E-Ink renderer asynchronously.
This method forwards the frame to the e-ink bridge for display in a separate thread.
Handle a new frame from the E-Ink renderer synchronously.
This method forwards the frame to the e-ink bridge for display in the main thread.

Args:
frame_data: The binary data for the frame
Expand All @@ -572,17 +570,15 @@ async def _handle_eink_frame(self, frame_data, width, height):
f"E-Ink frame received: {width}x{height}, {len(frame_data)} bytes. Offloading to bridge."
)

# Forward the frame to the e-ink bridge if available, using a separate thread
# Forward the frame to the e-ink bridge if available, using the main thread
if self.eink_bridge and self.eink_bridge.initialized:
try:
# Run the potentially blocking bridge call in a separate thread
await asyncio.to_thread(
self.eink_bridge.handle_frame, frame_data, width, height
)
# Run the potentially blocking bridge call in the main thread
self.eink_bridge.handle_frame(frame_data, width, height)
logger.debug("E-Ink frame successfully handled by bridge.")
except Exception as e:
logger.error(
f"Error calling eink_bridge.handle_frame in thread: {e}",
f"Error calling eink_bridge.handle_frame in main thread: {e}",
exc_info=True,
)
else:
Expand Down
5 changes: 4 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import sys
import os
import logging
import time
from typing import Optional

# Add project root to sys.path if necessary, although cli.py should handle it
Expand Down Expand Up @@ -68,7 +69,9 @@ async def main():
else:
logger.info(f"Existing llama-cpp server detected at {SERVER_URL}. Proceeding.")
# --- End Llama.cpp Server Management ---

else:
logger.info("Configuration specifies non-llama-cpp provider. Skipping server management.")
time.sleep(5) # Wait for 5 seconds to ensure the server is ready

if hasattr(args, 'gui') and args.gui:
logger.info("Starting GUI mode...")
Expand Down