Centralise checking ready for use

This commit is contained in:
Kovid Goyal
2025-09-02 14:41:25 +05:30
parent 7c595c932b
commit 035ef2b351
5 changed files with 58 additions and 24 deletions
+2 -3
View File
@@ -4,8 +4,7 @@
from qt.core import QComboBox, QDialog, QGroupBox, QHBoxLayout, QLabel, QStackedLayout, QVBoxLayout, QWidget
from calibre.ai import AICapabilities
from calibre.ai.prefs import prefs
from calibre.customize.ui import available_ai_provider_plugins
from calibre.ai.prefs import plugins_for_purpose, prefs
from calibre.gui2 import Application, error_dialog
@@ -13,7 +12,7 @@ class ConfigureAI(QWidget):
def __init__(self, purpose: AICapabilities = AICapabilities.text_to_text, parent: QWidget | None = None):
super().__init__(parent)
plugins = tuple(p for p in available_ai_provider_plugins() if p.capabilities & purpose == purpose)
plugins = tuple(plugins_for_purpose(purpose))
self.available_plugins = plugins
self.purpose = purpose
self.plugin_config_widgets: tuple[QWidget, ...] = tuple(p.config_widget() for p in plugins)
+15 -1
View File
@@ -8,16 +8,22 @@ import tempfile
from contextlib import closing, suppress
from functools import lru_cache
from threading import Thread
from typing import NamedTuple
from typing import Any, NamedTuple
from calibre import browser
from calibre.ai import AICapabilities
from calibre.ai.open_router import OpenRouterAI
from calibre.ai.prefs import pref_for_provider
from calibre.constants import __version__, cache_dir
from calibre.utils.lock import SingleInstance
module_version = 1 # needed for live updates
def pref(key: str, defval: Any = None) -> Any:
return pref_for_provider(OpenRouterAI.name, key, defval)
def get_browser():
ans = browser(user_agent=f'calibre {__version__}')
return ans
@@ -151,6 +157,14 @@ def save_settings(config_widget):
config_widget.save_settings()
def api_key() -> str:
return pref('api_key')
def is_ready_for_use() -> bool:
return bool(api_key())
if __name__ == '__main__':
from pprint import pprint
for m in get_available_models().values():
+20
View File
@@ -1,10 +1,14 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2025, Kovid Goyal <kovid at kovidgoyal.net>
from collections.abc import Iterator
from copy import deepcopy
from functools import lru_cache
from typing import Any
from calibre.ai import AICapabilities
from calibre.customize import AIProviderPlugin
from calibre.customize.ui import available_ai_provider_plugins
from calibre.utils.config import JSONConfig
@@ -24,3 +28,19 @@ def set_prefs_for_provider(name: str, pref_map: dict[str, Any]) -> None:
p = prefs()
p['providers'][name] = deepcopy(pref_map)
p.set('providers', p['providers'])
def plugins_for_purpose(purpose: AICapabilities) -> Iterator[AIProviderPlugin]:
for p in sorted(available_ai_provider_plugins(), key=lambda p: (p.priority, p.name.lower())):
if p.capabilities & purpose == purpose:
yield p
def plugin_for_purpose(purpose: AICapabilities) -> AIProviderPlugin | None:
compatible_plugins = {p.name: p for p in plugins_for_purpose(purpose)}
q = prefs()['purpose_map'].get(str(purpose), '')
if ans := compatible_plugins.get(q):
return ans
if compatible_plugins:
return next(iter(compatible_plugins.values()))
return None
+6
View File
@@ -846,6 +846,12 @@ class AIProviderPlugin(Plugin): # {{{
ans = self._builtin_live_module = load_module(self.builtin_live_module_name, strategy=Strategy.fast)
return ans
@property
def is_ready_for_use(self) -> bool:
if not self.builtin_live_module_name:
return False
return self.builtin_live_module.is_ready_for_use()
def initialize(self):
self._builtin_live_module = None
+15 -20
View File
@@ -34,7 +34,9 @@ from qt.core import (
pyqtSignal,
)
from calibre.ai import AICapabilities
from calibre.ai.config import ConfigureAI
from calibre.ai.prefs import plugin_for_purpose
from calibre.ebooks.metadata import authors_to_string
from calibre.gui2 import Application, error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
@@ -42,7 +44,6 @@ from calibre.gui2.viewer.config import vprefs
from calibre.gui2.viewer.highlights import HighlightColorCombo
from calibre.gui2.widgets2 import Dialog
from calibre.utils.icu import primary_sort_key
from polyglot.binary import from_hex_unicode
# --- Backend Abstraction & Cost Data ---
MODEL_COSTS = {
@@ -115,14 +116,10 @@ API_PROVIDERS = {
class LLMAPICall(Thread):
def __init__(self, conversation_history, api_key, model_id, signal_emitter, provider_config):
super().__init__()
def __init__(self, conversation_history, signal_emitter):
super().__init__(daemon=True)
self.conversation_history = conversation_history
self.api_key = api_key
self.model_id = model_id
self.signal_emitter = signal_emitter
self.provider_config = provider_config
self.daemon = True
def run(self):
try:
@@ -299,13 +296,16 @@ class LLMPanel(QWidget):
dialog.actions_updated.connect(self.rebuild_actions_ui)
dialog.exec()
@property
def is_ready_for_use(self) -> bool:
p = plugin_for_purpose(AICapabilities.text_to_text)
return p is not None and p.is_ready_for_use
def show_initial_message(self):
self.save_note_button.setEnabled(False)
api_key_hex = vprefs.get('llm_api_key', '') or ''
api_key = from_hex_unicode(api_key_hex)
if not api_key:
if not self.is_ready_for_use:
self.show_response('<p>' + _(
'Please add your API key for an AI service by clicking the <b>Settings</b> button below.'), {})
'Please configure an AI provider by clicking the <b>Settings</b> button below.'), {})
else:
self.show_response(_('Select text in the book to begin.'), {})
@@ -399,10 +399,9 @@ class LLMPanel(QWidget):
return html_output
def start_api_call(self, action_prompt):
api_key_hex = vprefs.get('llm_api_key', '') or ''
api_key = from_hex_unicode(api_key_hex)
if not api_key:
self.show_response(f"<p style='color:orange;'><b>{_('API Key Missing.')}</b> Click the <b>{_('Settings')}</b> button to add your key.</p>", {})
if not self.is_ready_for_use:
self.show_response(f"<p style='color:orange;'><b>{_('AI provider not configured')}</b> Click the <b>{_(
'Settings')}</b> button to configure an AI service provider.</p>", {})
return
if not self.latched_conversation_text:
self.show_response(f"<p style='color:red;'><b>{_('Error')}:</b> {_('No text is selected for this conversation.')}</p>", {})
@@ -438,11 +437,7 @@ class LLMPanel(QWidget):
self.result_display.verticalScrollBar().setValue(self.result_display.verticalScrollBar().maximum())
self.set_all_inputs_enabled(False)
model_id = vprefs.get('llm_model_id', 'google/gemini-1.5-flash')
provider_config = API_PROVIDERS['openrouter']
api_call_thread = LLMAPICall(
api_call_history, api_key, model_id, self.response_received, provider_config
)
api_call_thread = LLMAPICall(api_call_history, self.response_received)
api_call_thread.start()
def show_response(self, response_text, usage_data):