diff --git a/src/calibre/gui2/llm.py b/src/calibre/gui2/llm.py
index e9051dc874..bdf8fea0ea 100644
--- a/src/calibre/gui2/llm.py
+++ b/src/calibre/gui2/llm.py
@@ -5,6 +5,7 @@ from collections.abc import Iterator
from html import escape
from itertools import count
from threading import Thread
+from typing import Any
from qt.core import (
QApplication,
@@ -32,6 +33,7 @@ from calibre.customize import AIProviderPlugin
from calibre.gui2 import safe_open_url
from calibre.gui2.chat_widget import Button, ChatWidget, Header
from calibre.utils.icu import primary_sort_key
+from calibre.utils.localization import ui_language_as_english
from calibre.utils.logging import ERROR, WARN
from calibre.utils.short_uuid import uuid4
from polyglot.binary import as_hex_unicode
@@ -65,10 +67,9 @@ def show_reasoning(reasoning: str, parent: QWidget | None = None):
class ConversationHistory:
- def __init__(self, conversation_text: str = ''):
+ def __init__(self):
self.accumulator = StreamedResponseAccumulator()
self.items: list[ChatMessage] = []
- self.conversation_text: str = conversation_text
self.model_used = ''
self.api_call_active = False
self.current_response_completed = True
@@ -92,7 +93,7 @@ class ConversationHistory:
self.items.append(x)
def copy(self, upto: int | None = None) -> 'ConversationHistory':
- ans = ConversationHistory(self.conversation_text)
+ ans = ConversationHistory()
ans.model_used = self.model_used
if upto is None:
ans.items = list(self.items)
@@ -216,6 +217,10 @@ class ConverseWidget(QWidget):
self.show_initial_message()
self.update_cost()
+ def language_instruction(self):
+ lang = ui_language_as_english()
+ return f'If you can speak in {lang}, then respond in {lang}.'
+
def quick_actions_as_html(self, actions) -> str:
actions = sorted(actions, key=lambda a: primary_sort_key(a.human_name))
if not actions:
@@ -223,7 +228,7 @@ class ConverseWidget(QWidget):
ans = []
for action in actions:
hn = action.human_name.replace(' ', '\xa0')
- ans.append(f'''{hn}''')
links = '\xa0\xa0\xa0 '.join(ans)
@@ -253,11 +258,6 @@ class ConverseWidget(QWidget):
if prompt := prompt.strip():
self.start_api_call(prompt)
- def start_new_conversation(self):
- self.clear_current_conversation()
- self.latched_conversation_text = ''
- self.update_ui_state()
-
@property
def assistant_name(self) -> str:
return self.ai_provider_plugin.human_readable_model_name(self.conversation_history.model_used) or _('Assistant')
@@ -303,20 +303,19 @@ class ConverseWidget(QWidget):
def scroll_to_bottom(self) -> None:
self.result_display.scroll_to_bottom()
- def start_api_call(self, action_prompt: str, uses_selected_text: bool = False):
+ def start_api_call(self, action_prompt: str, **kwargs: Any) -> None:
if not self.is_ready_for_use:
self.show_error(f'''{_('AI provider not configured.')} {_(
'Configure AI provider')}''', is_critical=False)
return
- if not self.latched_conversation_text:
- self.show_error(f"{_('Error')}: {_('No text is selected for this conversation.')}", is_critical=True)
+ if err := self.ready_to_start_api_call():
+ self.show_error(f"{_('Error')}: {err}", is_critical=True)
return
if self.conversation_history:
self.conversation_history.append(ChatMessage(action_prompt))
else:
- self.conversation_history.conversation_text = self.latched_conversation_text
- for msg in self.create_initial_messages(action_prompt, self.latched_conversation_text if uses_selected_text else ''):
+ for msg in self.create_initial_messages(action_prompt, **kwargs):
self.conversation_history.append(msg)
self.current_api_call_number = next(self.counter)
self.conversation_history.new_api_call()
@@ -454,7 +453,7 @@ class ConverseWidget(QWidget):
def handle_chat_link(self, qurl: QUrl) -> bool:
raise NotImplementedError('implement in subclass')
- def create_initial_messages(self, action_prompt: str, selected_text: str) -> Iterator[ChatMessage]:
+ def create_initial_messages(self, action_prompt: str, **kwargs: Any) -> Iterator[ChatMessage]:
raise NotImplementedError('implement in sub class')
def ready_message(self) -> str:
@@ -462,4 +461,14 @@ class ConverseWidget(QWidget):
def choose_action_message(self) -> str:
raise NotImplementedError('implement in sub class')
+
+ def prompt_text_for_action(self, action) -> str:
+ raise NotImplementedError('implement in sub class')
+
+ def start_new_conversation(self) -> None:
+ self.clear_current_conversation()
+ self.update_ui_state()
+
+ def ready_to_start_api_call(self) -> str:
+ return ''
# }}}
diff --git a/src/calibre/gui2/viewer/llm.py b/src/calibre/gui2/viewer/llm.py
index a4e53fb672..f968746579 100644
--- a/src/calibre/gui2/viewer/llm.py
+++ b/src/calibre/gui2/viewer/llm.py
@@ -107,13 +107,6 @@ def current_actions(include_disabled=False):
yield x
-def get_language_instruction() -> str:
- if vprefs['llm_localized_results'] != 'always':
- return ''
- lang = ui_language_as_english()
- return f'If you can speak in {lang}, then respond in {lang}.'
-
-
class LLMPanel(ConverseWidget):
add_note_requested = pyqtSignal(str, str)
@@ -136,7 +129,7 @@ class LLMPanel(ConverseWidget):
self.book_authors = authors_to_string(authors)
def activate_action(self, action: Action) -> None:
- self.start_api_call(action.prompt_text(self.latched_conversation_text), action.uses_selected_text)
+ self.start_api_call(self.prompt_text_for_action(action), uses_selected_text=action.uses_selected_text)
def settings_dialog(self) -> QDialog:
return LLMSettingsDialog(self)
@@ -164,7 +157,13 @@ class LLMPanel(ConverseWidget):
yield Button('save.png', f'http://{self.save_note_hostname}/{msgnum}', _(
'Save this specific response as the note'))
- def create_initial_messages(self, action_prompt: str, selected_text: str) -> Iterator[ChatMessage]:
+ def get_language_instruction(self) -> str:
+ if vprefs['llm_localized_results'] != 'always':
+ return ''
+ return self.language_instruction()
+
+ def create_initial_messages(self, action_prompt: str, **kwargs: Any) -> Iterator[ChatMessage]:
+ selected_text = self.latched_conversation_text if kwargs.get('uses_selected_text') else ''
if self.book_title:
context_header = f'I am currently reading the book: {self.book_title}'
if self.book_authors:
@@ -174,7 +173,7 @@ class LLMPanel(ConverseWidget):
else:
context_header += '. I have some questions about this book.'
context_header += ' When you answer the questions use markdown formatting for the answers wherever possible.'
- if language_instruction := get_language_instruction():
+ if language_instruction := self.get_language_instruction():
context_header += ' ' + language_instruction
yield ChatMessage(context_header, type=ChatMessageType.system)
yield ChatMessage(action_prompt)
@@ -191,6 +190,9 @@ class LLMPanel(ConverseWidget):
msg += 'Summarize this book.'
return msg
+ def prompt_text_for_action(self, action) -> str:
+ return action.prompt_text(self.latched_conversation_text)
+
def save_as_note(self):
if self.conversation_history.response_count > 0 and self.latched_conversation_text:
if not self.current_selected_text:
@@ -221,6 +223,15 @@ class LLMPanel(ConverseWidget):
return True
return False
+ def start_new_conversation(self) -> None:
+ self.latched_conversation_text = ''
+ super().start_new_conversation()
+
+ def ready_to_start_api_call(self) -> str:
+ if self.latched_conversation_text:
+ return ''
+ return _('No text is selected for this conversation.')
+
# Settings {{{