mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-05-30 18:45:20 -04:00
Centralise checking ready for use
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user