mirror of
https://github.com/kovidgoyal/calibre.git
synced 2026-04-03 15:51:58 -04:00
Implement basic settings for llm book
This commit is contained in:
parent
5dadf4a732
commit
a3ade0dfd2
@ -19,6 +19,7 @@ def prefs() -> JSONConfig:
|
||||
ans = JSONConfig('ai', permissions=0o600) # make readable only by user as it stores secrets
|
||||
ans.defaults['providers'] = {}
|
||||
ans.defaults['purpose_map'] = {}
|
||||
ans.defaults['llm_localized_results'] = 'never'
|
||||
return ans
|
||||
|
||||
|
||||
|
||||
@ -3,15 +3,14 @@
|
||||
|
||||
from collections.abc import Iterator
|
||||
from functools import lru_cache
|
||||
from typing import Any, NamedTuple
|
||||
from typing import Any
|
||||
|
||||
from qt.core import QDialog, QUrl, QVBoxLayout, QWidget
|
||||
|
||||
from calibre.ai import ChatMessage, ChatMessageType
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.gui2 import Application, gprefs
|
||||
from calibre.gui2.llm import ConverseWidget
|
||||
from calibre.gui2.viewer.config import vprefs
|
||||
from calibre.gui2.llm import ActionData, ConverseWidget, LLMActionsSettingsWidget, LLMSettingsDialogBase, LocalisedResults
|
||||
from polyglot.binary import from_hex_unicode
|
||||
|
||||
|
||||
@ -55,51 +54,62 @@ def get_allowed_fields() -> set[str]:
|
||||
return set()
|
||||
|
||||
|
||||
class Action(NamedTuple):
|
||||
name: str
|
||||
human_name: str
|
||||
prompt_template: str
|
||||
is_builtin: bool = True
|
||||
is_disabled: bool = False
|
||||
|
||||
@property
|
||||
def as_custom_action_dict(self) -> dict[str, Any]:
|
||||
return {'disabled': self.is_disabled, 'title': self.human_name, 'prompt_template': self.prompt_template}
|
||||
class Action(ActionData):
|
||||
|
||||
def prompt_text(self, books: list[Metadata]) -> str:
|
||||
pt = self.prompt_template
|
||||
return pt.format(
|
||||
books=format_books_for_query(books),
|
||||
books_word='book' if len(books) < 2 else 'books',
|
||||
plural_word='is' if len(books) < 2 else 'are',
|
||||
is_are='is' if len(books) < 2 else 'are',
|
||||
title=books[0].title, authors=books[0].format_authors(), series=books[0].series or '',
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(2)
|
||||
def default_actions() -> tuple[Action, ...]:
|
||||
return (
|
||||
Action('summarize', _('Summarize'), '{books} Provide a concise summary of the previously described {books_word}.'),
|
||||
Action('chapters', _('Chapters'), '{books} Provide a chapter by chapter summary of the previously described {books_word}.'),
|
||||
Action('summarize', _('Summarize'), 'Provide a concise summary of the previously described {books_word}.'),
|
||||
Action('chapters', _('Chapters'), 'Provide a chapter by chapter summary of the previously described {books_word}.'),
|
||||
Action('read_next', _('Read next'), 'Suggest some good books to read after the previously described {books_word}.'),
|
||||
Action('universe', _('Universe'), 'Describe the fictional universe the previously described {books_word} {plural_word} set in.'
|
||||
Action('universe', _('Universe'), 'Describe the fictional universe the previously described {books_word} {is_are} set in.'
|
||||
' Outline major plots, themes and characters in the universe.'),
|
||||
Action('series', _('Series'), 'Give the series the previously described {books_word} {plural_word} in.'
|
||||
Action('series', _('Series'), 'Give the series the previously described {books_word} {is_are} in.'
|
||||
' List all the books in the series, in both published and internal chronological order.'
|
||||
' Also describe any prominent spin-off series.')
|
||||
)
|
||||
|
||||
|
||||
def current_actions(include_disabled=False):
|
||||
p = gprefs.get('llm_converse_quick_actions') or {}
|
||||
dd = p.get('disabled_default_actions', ())
|
||||
for x in default_actions():
|
||||
x = x._replace(is_disabled=x.name in dd)
|
||||
if include_disabled or not x.is_disabled:
|
||||
yield x
|
||||
for title, c in p.get('custom_actions', {}).items():
|
||||
x = Action(f'custom-{title}', title, c['prompt_template'], is_builtin=False, is_disabled=c['disabled'])
|
||||
if include_disabled or not x.is_disabled:
|
||||
yield x
|
||||
def current_actions(include_disabled=False) -> Iterator[Action]:
|
||||
p = gprefs.get('llm_book_quick_actions') or {}
|
||||
return Action.unserialize(p, default_actions(), include_disabled)
|
||||
|
||||
|
||||
class LLMSettingsWidget(LLMActionsSettingsWidget):
|
||||
|
||||
action_edit_help_text = '<p>' + _(
|
||||
'The prompt is a template. The expression {0} will be replaced by "book"'
|
||||
' when there is only a single book being discussed and "books" otherwise.'
|
||||
' Similarly {1} becomes "is" or "are", as needed. {2}, {3}, {4} are replaced '
|
||||
' by the title, authors and series of the first book, respectively.'
|
||||
).format('{books_word}', '{is_are}', '{title}', '{authors}', '{series}')
|
||||
|
||||
def get_actions_from_prefs(self) -> Iterator[ActionData]:
|
||||
yield from current_actions(include_disabled=True)
|
||||
|
||||
def set_actions_in_prefs(self, s: dict[str, Any]) -> None:
|
||||
gprefs.set('llm_book_quick_actions', s)
|
||||
|
||||
def create_custom_widgets(self) -> Iterator[str, QWidget]:
|
||||
yield '', LocalisedResults()
|
||||
|
||||
|
||||
class LLMSettingsDialog(LLMSettingsDialogBase):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(title=_('AI Settings'), name='llm-book-settings-dialog', prefs=gprefs, parent=parent)
|
||||
|
||||
def custom_tabs(self) -> Iterator[str, str, QWidget]:
|
||||
yield 'config.png', _('&Actions'), LLMSettingsWidget(self)
|
||||
|
||||
|
||||
class LLMPanel(ConverseWidget):
|
||||
@ -109,6 +119,9 @@ class LLMPanel(ConverseWidget):
|
||||
self.books = books
|
||||
super().__init__(parent)
|
||||
|
||||
def settings_dialog(self) -> QDialog:
|
||||
return LLMSettingsDialog(self)
|
||||
|
||||
def handle_chat_link(self, qurl: QUrl) -> bool:
|
||||
match qurl.host():
|
||||
case self.quick_action_hostname:
|
||||
@ -135,11 +148,6 @@ class LLMPanel(ConverseWidget):
|
||||
return msg
|
||||
ready_message = choose_action_message
|
||||
|
||||
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]:
|
||||
context_header = format_books_for_query(self.books)
|
||||
context_header += ' When you answer the questions use markdown formatting for the answers wherever possible.'
|
||||
|
||||
@ -41,6 +41,7 @@ from qt.core import (
|
||||
from calibre.ai import AICapabilities, ChatMessage, ChatMessageType, ChatResponse
|
||||
from calibre.ai.config import ConfigureAI
|
||||
from calibre.ai.prefs import plugin_for_purpose
|
||||
from calibre.ai.prefs import prefs as aiprefs
|
||||
from calibre.ai.utils import ContentType, StreamedResponseAccumulator, response_to_html
|
||||
from calibre.customize import AIProviderPlugin
|
||||
from calibre.gui2 import error_dialog, safe_open_url
|
||||
@ -315,6 +316,11 @@ class ConverseWidget(QWidget):
|
||||
self.result_display.re_render()
|
||||
self.scroll_to_bottom()
|
||||
|
||||
def get_language_instruction(self) -> str:
|
||||
if aiprefs['llm_localized_results'] != 'always':
|
||||
return ''
|
||||
return self.language_instruction()
|
||||
|
||||
def scroll_to_bottom(self) -> None:
|
||||
self.result_display.scroll_to_bottom()
|
||||
|
||||
@ -569,8 +575,7 @@ class ActionEditDialog(QDialog):
|
||||
|
||||
class LocalisedResults(QCheckBox):
|
||||
|
||||
def __init__(self, prefs):
|
||||
self.prefs = prefs
|
||||
def __init__(self):
|
||||
super().__init__(_('Ask the AI to respond in the current language'))
|
||||
self.setToolTip('<p>' + _(
|
||||
'Ask the AI to respond in the current calibre user interface language. Note that how well'
|
||||
@ -578,10 +583,10 @@ class LocalisedResults(QCheckBox):
|
||||
' different languages.'))
|
||||
|
||||
def load_settings(self):
|
||||
self.setChecked(self.prefs['llm_localized_results'] == 'always')
|
||||
self.setChecked(aiprefs['llm_localized_results'] == 'always')
|
||||
|
||||
def commit(self) -> bool:
|
||||
self.prefs.set('llm_localized_results', 'always' if self.isChecked() else 'never')
|
||||
aiprefs['llm_localized_results'] = 'always' if self.isChecked() else 'never'
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ vprefs.defaults['old_prefs_migrated'] = False
|
||||
vprefs.defaults['bookmarks_sort'] = 'title'
|
||||
vprefs.defaults['highlight_export_format'] = 'txt'
|
||||
vprefs.defaults['auto_update_lookup'] = True
|
||||
vprefs.defaults['llm_localized_results'] = 'never'
|
||||
|
||||
|
||||
def get_session_pref(name, default=None, group='standalone_misc_settings'):
|
||||
|
||||
@ -121,11 +121,6 @@ class LLMPanel(ConverseWidget):
|
||||
yield Button('save.png', f'http://{self.save_note_hostname}/{msgnum}', _(
|
||||
'Save this specific response as the note'))
|
||||
|
||||
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:
|
||||
@ -214,10 +209,9 @@ class HighlightWidget(HighlightColorCombo):
|
||||
class LLMSettingsWidget(LLMActionsSettingsWidget):
|
||||
|
||||
action_edit_help_text = '<p>' + _(
|
||||
'The prompt is a template. If you want the prompt to operate on the currently selected'
|
||||
' text, add <b>{0}</b> to the end of the prompt. Similarly, use <b>{1}</b>'
|
||||
' when you want the AI to respond in the current language (not all AIs work well with all languages).'
|
||||
).format('{selected}', 'Respond in {language}')
|
||||
'The prompt is a template. If you want the prompt to operate on the currently selected'
|
||||
' text, add <b>{0}</b> to the end of the prompt.'
|
||||
).format('{selected}')
|
||||
|
||||
def get_actions_from_prefs(self) -> Iterator[ActionData]:
|
||||
yield from current_actions(include_disabled=True)
|
||||
@ -227,7 +221,7 @@ class LLMSettingsWidget(LLMActionsSettingsWidget):
|
||||
|
||||
def create_custom_widgets(self) -> Iterator[str, QWidget]:
|
||||
yield _('&Highlight style:'), HighlightWidget(self)
|
||||
yield '', LocalisedResults(vprefs)
|
||||
yield '', LocalisedResults()
|
||||
# }}}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user