diff --git a/.ruff.toml b/.ruff.toml index 749ef04..d823f4d 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,2 +1,4 @@ +line-length = 100 + [format] quote-style = "single" diff --git a/README.md b/README.md index 5047f35..120db88 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,10 @@ The OpenAI Completion plugin has a settings file where you can set your OpenAI A If you're here it meaning that a model that you're using with ollama talking shit. This is because `temperature` property of a model which is 1 somewhat [doubles](https://github.com/ollama/ollama/blob/69be940bf6d2816f61c79facfa336183bc882720/openai/openai.go#L454) on ollama's side, so it becomes 2, which is a little bit too much for a good model's response. So you to make things work you have to set temperature to 1. +### Advertisement disabling + +To disable advertisement you have to add `"advertisement": false` line into an assistant setting where you wish it to be disabled. + ## Key bindings You can bind keys for a given plugin command in `Preferences` -> `Package Settings` -> `OpenAI` -> `Key Bindings`. For example you can bind "New Message" command like this: diff --git a/main.py b/main.py index 910806b..3d6658a 100644 --- a/main.py +++ b/main.py @@ -1,20 +1,25 @@ import sys # clear modules cache if package is reloaded (after update?) -prefix = __package__ + ".plugins" # don't clear the base package -for module_name in [ - module_name - for module_name in sys.modules - if module_name.startswith(prefix) -]: +prefix = __package__ + '.plugins' # type: ignore # don't clear the base package +for module_name in [module_name for module_name in sys.modules if module_name.startswith(prefix)]: del sys.modules[module_name] del prefix -from .plugins.openai import Openai -from .plugins.active_view_event import ActiveViewEventListener -from .plugins.openai_panel import OpenaiPanelCommand -from .plugins.stop_worker_execution import StopOpenaiExecutionCommand -from .plugins.worker_running_context import OpenaiWorkerRunningContext -from .plugins.settings_reloader import ReloadSettingsListener -from .plugins.output_panel import SharedOutputPanelListener, AIChatViewEventListener -from .plugins.buffer import TextStreamAtCommand, ReplaceRegionCommand, EraseRegionCommand \ No newline at end of file +from .plugins.active_view_event import ActiveViewEventListener # noqa: E402, F401 +from .plugins.ai_chat_event import AIChatViewEventListener # noqa: E402, F401 +from .plugins.buffer import ( # noqa: E402, F401 + EraseRegionCommand, + ReplaceRegionCommand, + TextStreamAtCommand, +) +from .plugins.openai import Openai # noqa: E402, F401 +from .plugins.openai_panel import OpenaiPanelCommand # noqa: E402, F401 +from .plugins.output_panel import SharedOutputPanelListener # noqa: E402, F401 +from .plugins.settings_reloader import ReloadSettingsListener # noqa: E402, F401 +from .plugins.stop_worker_execution import ( # noqa: E402 + StopOpenaiExecutionCommand, # noqa: F401 +) +from .plugins.worker_running_context import ( # noqa: E402, + OpenaiWorkerRunningContext, # noqa: F401 +) diff --git a/plugins/active_view_event.py b/plugins/active_view_event.py index ca2e5f1..10b6c04 100644 --- a/plugins/active_view_event.py +++ b/plugins/active_view_event.py @@ -1,13 +1,15 @@ -from typing import Any, Dict, List, Optional +from __future__ import annotations + +import logging +from typing import Any, Dict, List + import sublime from sublime import View from sublime_plugin import EventListener -import logging from .cacher import Cacher from .status_bar import StatusBarMode - logger = logging.getLogger(__name__) @@ -17,21 +19,19 @@ class ActiveViewEventListener(EventListener): def on_activated(self, view: View): ## FIXME: This is might be wrong, settings of view should be get not for an active view, but for a given window project view. ## It could be correct btw, as if a window with a specific settings gets active — it updated exact it status bar. - self.project_settings = ( - sublime.active_window().active_view().settings().get('ai_assistant', None) - ) + self.project_settings: Dict[str, str] | None = ( + sublime.active_window().active_view().settings().get('ai_assistant') + ) # type: ignore # Logging disabled becuase it's too spammy. Uncomment in case of necessity. # logger.debug( # "project_settings exists: %s", "YES" if self.project_settings else "NO" # ) + cache_prefix = self.project_settings.get('cache_prefix') if self.project_settings else None + # Initialize Cacher with proper default handling for missing cache_prefix - self.cacher = ( - Cacher(name=self.project_settings['cache_prefix']) - if self.project_settings - else Cacher() - ) + self.cacher = Cacher(name=cache_prefix) # logger.debug("cacher.history_file: %s", self.cacher.history_file) # logger.debug("cacher.current_model_file: %s", self.cacher.current_model_file) @@ -44,9 +44,7 @@ def on_activated(self, view: View): settings = sublime.load_settings('openAI.sublime-settings') - status_hint_options: List[str] = ( - settings.get('status_hint', []) if settings else [] - ) + status_hint_options: List[str] = settings.get('status_hint', []) if settings else [] # type: ignore # logger.debug("status_hint_options: %s", status_hint_options) @@ -56,7 +54,7 @@ def on_activated(self, view: View): def update_status_bar( self, view: View, - assistant: Optional[Dict[str, Any]], + assistant: Dict[str, Any] | None, status_hint_options: List[str], ): if not assistant: @@ -69,9 +67,7 @@ def update_status_bar( if {'name', 'prompt_mode', 'chat_model'} <= assistant.keys(): statuses: List[str] = [] for key in ['name', 'prompt_mode', 'chat_model']: - lookup_key = ( - key if key != 'name' else 'name_' - ) # name is a reserved keyword + lookup_key = key if key != 'name' else 'name_' # name is a reserved keyword if StatusBarMode[lookup_key].value in status_hint_options: if key == 'chat_model': statuses.append(assistant[key].upper()) diff --git a/plugins/ai_chat_event.py b/plugins/ai_chat_event.py new file mode 100644 index 0000000..347ba42 --- /dev/null +++ b/plugins/ai_chat_event.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Dict + +from sublime import Window +from sublime_plugin import ViewEventListener + +from .cacher import Cacher + + +class AIChatViewEventListener(ViewEventListener): + @classmethod + def is_applicable(cls, settings) -> bool: + return ( + settings.get('syntax') == 'Packages/Markdown/MultiMarkdown.sublime-syntax' + or settings.get('syntax') == 'Packages/Markdown/PlainText.sublime-syntax' + ) + + def on_activated(self) -> None: + self.update_status_message(self.view.window()) # type: ignore + + def update_status_message(self, window: Window) -> None: + project_settings: Dict[str, str] | None = window.active_view().settings().get('ai_assistant') # type: ignore + + cache_prefix = project_settings.get('cache_prefix') if project_settings else None + + cacher = Cacher(name=cache_prefix) + if self.is_ai_chat_tab_active(window): + status_message = self.get_status_message(cacher=cacher) + active_view = window.active_view() + if active_view and active_view.name() == 'AI Chat': + active_view.set_status('ai_chat_status', status_message) + + def is_ai_chat_tab_active(self, window: Window) -> bool: + active_view = window.active_view() + return active_view.name() == 'AI Chat' if active_view else False + + def get_status_message(self, cacher: Cacher) -> str: + tokens = cacher.read_tokens_count() + prompt = tokens['prompt_tokens'] if tokens else 0 + completion = tokens['completion_tokens'] if tokens else 0 + total = prompt + completion + + return f'[⬆️: {prompt:,} + ⬇️: {completion:,} = {total:,}]' diff --git a/plugins/assistant_settings.py b/plugins/assistant_settings.py index b5b3336..5eedf5a 100644 --- a/plugins/assistant_settings.py +++ b/plugins/assistant_settings.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import Dict, Any class PromptMode(Enum): @@ -14,8 +16,8 @@ class PromptMode(Enum): class AssistantSettings: name: str prompt_mode: PromptMode - url: Optional[str] - token: Optional[str] + url: str | None + token: str | None chat_model: str assistant_role: str temperature: int @@ -23,11 +25,11 @@ class AssistantSettings: top_p: int frequency_penalty: int presence_penalty: int - placeholder: Optional[str] + placeholder: str | None advertisement: bool -DEFAULT_ASSISTANT_SETTINGS = { +DEFAULT_ASSISTANT_SETTINGS: Dict[str, Any] = { 'placeholder': None, 'url': None, 'token': None, @@ -36,7 +38,7 @@ class AssistantSettings: 'top_p': 1, 'frequency_penalty': 0, 'presence_penalty': 0, - 'advertisement': True + 'advertisement': True, } diff --git a/plugins/buffer.py b/plugins/buffer.py index 83e71cc..d4a0109 100644 --- a/plugins/buffer.py +++ b/plugins/buffer.py @@ -1,29 +1,34 @@ from sublime import Edit, Region, View from sublime_plugin import TextCommand -class TextStreamer(): + +class TextStreamer: def __init__(self, view: View) -> None: self.view = view def update_completion(self, completion: str): ## Till this line selection has to be cleared and the carret should be placed in to a desired starting point. ## So begin() and end() sould be the very same carret offset. - start_of_selection = self.view.sel()[0].begin() ## begin() because if we point an end there — it'll start to reverse prompting. - self.view.run_command("text_stream_at", {"position": start_of_selection, "text": completion}) + ## begin() because if we point an end there — it'll start to reverse prompting. + start_of_selection = self.view.sel()[0].begin() + self.view.run_command('text_stream_at', {'position': start_of_selection, 'text': completion}) return def delete_selected_region(self, region: Region): json_reg = {'a': region.begin(), 'b': region.end()} - self.view.run_command("erase_region", {"region": json_reg}) + self.view.run_command('erase_region', {'region': json_reg}) + class TextStreamAtCommand(TextCommand): - def run(self, edit: Edit, position: int, text: str): + def run(self, edit: Edit, position: int, text: str): # type: ignore _ = self.view.insert(edit=edit, pt=position, text=text) + class ReplaceRegionCommand(TextCommand): - def run(self, edit: Edit, region, text: str): + def run(self, edit: Edit, region, text: str): # type: ignore self.view.replace(edit=edit, region=Region(region['a'], region['b']), text=text) + class EraseRegionCommand(TextCommand): - def run(self, edit: Edit, region): + def run(self, edit: Edit, region): # type: ignore self.view.erase(edit=edit, region=Region(region['a'], region['b'])) diff --git a/plugins/cacher.py b/plugins/cacher.py index 5ef70c0..aca7228 100644 --- a/plugins/cacher.py +++ b/plugins/cacher.py @@ -1,13 +1,17 @@ -import sublime -import os -from . import jl_utility as jl +from __future__ import annotations + import json +import os from json.decoder import JSONDecodeError -from typing import List, Dict, Iterator, Any, Optional +from typing import Any, Dict, Iterator, List + +import sublime + +from . import jl_utility as jl class Cacher: - def __init__(self, name: str = '') -> None: + def __init__(self, name: str | None = None) -> None: cache_dir = sublime.cache_path() plugin_cache_dir = os.path.join(cache_dir, 'OpenAI completion') if not os.path.exists(plugin_cache_dir): @@ -16,21 +20,15 @@ def __init__(self, name: str = '') -> None: # Create the file path to store the data self.history_file = os.path.join( plugin_cache_dir, - '{file_name}chat_history.jl'.format( - file_name=name + '_' if len(name) > 0 else '' - ), + '{file_name}chat_history.jl'.format(file_name=name + '_' if name else ''), ) self.current_model_file = os.path.join( plugin_cache_dir, - '{file_name}current_assistant.json'.format( - file_name=name + '_' if len(name) > 0 else '' - ), + '{file_name}current_assistant.json'.format(file_name=name + '_' if name else ''), ) self.tokens_count_file = os.path.join( plugin_cache_dir, - '{file_name}tokens_count.json'.format( - file_name=name + '_' if len(name) > 0 else '' - ), + '{file_name}tokens_count.json'.format(file_name=name + '_' if name else ''), ) def check_and_create(self, path: str): @@ -61,11 +59,11 @@ def reset_tokens_count(self): with open(self.tokens_count_file, 'w') as _: pass # Truncate the file by opening it in 'w' mode and doing nothing - def read_tokens_count(self) -> Optional[Dict[str, int]]: + def read_tokens_count(self) -> Dict[str, int] | None: self.check_and_create(self.tokens_count_file) with open(self.tokens_count_file, 'r') as file: try: - data: Optional[Dict[str, int]] = json.load(file) + data: Dict[str, int] | None = json.load(file) except JSONDecodeError: data = {'prompt_tokens': 0, 'completion_tokens': 0, 'total_tokens': 0} return data @@ -75,11 +73,11 @@ def save_model(self, data: Dict[str, Any]): with open(self.current_model_file, 'w') as file: json.dump(data, file) - def read_model(self) -> Optional[Dict[str, Any]]: + def read_model(self) -> Dict[str, Any] | None: self.check_and_create(self.current_model_file) with open(self.current_model_file, 'r') as file: try: - data: Optional[Dict[str, Any]] = json.load(file) + data: Dict[str, Any] | None = json.load(file) except JSONDecodeError: # TODO: Handle this state, but keep in mind # that it's completely legal to being a file empty for some (yet unspecified) state diff --git a/plugins/errors/OpenAIException.py b/plugins/errors/OpenAIException.py index 18ddc4a..b517bbd 100644 --- a/plugins/errors/OpenAIException.py +++ b/plugins/errors/OpenAIException.py @@ -1,6 +1,7 @@ -from sublime import error_message from logging import exception +from sublime import error_message + class OpenAIException(Exception): """Exception raised for errors in the input. diff --git a/plugins/image_handler.py b/plugins/image_handler.py index 3d74313..163a7bc 100644 --- a/plugins/image_handler.py +++ b/plugins/image_handler.py @@ -1,8 +1,9 @@ +import logging import os import re from urllib.parse import urlparse + import sublime -import logging logger = logging.getLogger(__name__) @@ -11,9 +12,7 @@ class ImageValidator: @staticmethod def get_valid_image_input(text: str) -> str: """Check if the input text contains valid image URLs or file paths; return the original string if valid.""" - clipboard_content = ( - sublime.get_clipboard().strip() if sublime.get_clipboard() else text.strip() - ) + clipboard_content = sublime.get_clipboard().strip() if sublime.get_clipboard() else text.strip() # Split the content by spaces or newlines potential_images = re.split(r'\n', clipboard_content) @@ -41,12 +40,12 @@ def is_valid_url(text: str) -> bool: result = urlparse(text) # Ensure the URL scheme is HTTP/HTTPS and it has a valid image extension return all( - [result.scheme in ('http', 'https'), result.netloc] + [result.scheme in ('http', 'https'), result.netloc] # type: ignore ) and re.match(r'.*\.(jpg|jpeg|png)$', text) - except: + except: # noqa: E722 return False @staticmethod def is_local_image(text: str) -> bool: """Check if the text is a valid local file path pointing to an image.""" - return os.path.isfile(text) and re.match(r'.*\.(jpg|jpeg|png)$', text) + return os.path.isfile(text) and re.match(r'.*\.(jpg|jpeg|png)$', text) # type: ignore diff --git a/plugins/jl_utility.py b/plugins/jl_utility.py index 5443d93..7ef5aa9 100644 --- a/plugins/jl_utility.py +++ b/plugins/jl_utility.py @@ -1,5 +1,6 @@ import json -from typing import Iterator, Generator +from typing import Generator, Iterator + def reader(fname: str) -> Iterator[dict]: with open(fname) as file: @@ -13,4 +14,4 @@ def writer(fname: str, mode: str = 'a') -> Generator[None, dict, None]: while True: obj = yield line = json.dumps(obj, ensure_ascii=False) - file.write(f"{line}\n") + file.write(f'{line}\n') diff --git a/plugins/openai.py b/plugins/openai.py index 950ce96..e9834b3 100644 --- a/plugins/openai.py +++ b/plugins/openai.py @@ -1,12 +1,16 @@ +from __future__ import annotations + +import logging +from typing import Dict + import sublime -from sublime import Edit, View, Region +from sublime import Edit, Region, View from sublime_plugin import TextCommand -import logging -from .openai_base import CommonMethods +from .assistant_settings import CommandMode from .cacher import Cacher +from .openai_base import CommonMethods from .output_panel import SharedOutputPanelListener -from .assistant_settings import CommandMode logger = logging.getLogger(__name__) @@ -17,16 +21,17 @@ class Openai(TextCommand): def run(self, edit: Edit, **kwargs): mode = kwargs.get('mode', 'chat_completion') - project_settings = self.view.settings().get('ai_assistant', None) - cacher = ( - Cacher(name=project_settings['cache_prefix']) - if project_settings - else Cacher() - ) + self.project_settings: Dict[str, str] | None = ( + sublime.active_window().active_view().settings().get('ai_assistant') + ) # type: ignore + + cache_prefix = self.project_settings.get('cache_prefix') if self.project_settings else None + settings = sublime.load_settings('openAI.sublime-settings') listener = SharedOutputPanelListener( - markdown=settings.get('markdown'), cacher=cacher + markdown=settings.get('markdown', False), # type: ignore + cacher=Cacher(name=cache_prefix), ) if mode == CommandMode.reset_chat_history.value: @@ -40,9 +45,7 @@ def run(self, edit: Edit, **kwargs): # TODO: This is temporary solution, this method should be moved to a more proper place @classmethod - def reset_chat_history( - cls, view: View, listener: SharedOutputPanelListener, edit: Edit - ): + def reset_chat_history(cls, view: View, listener: SharedOutputPanelListener, edit: Edit): listener.cacher.drop_all() listener.cacher.reset_tokens_count() window = sublime.active_window() diff --git a/plugins/openai_base.py b/plugins/openai_base.py index 8f93c67..0f224f3 100644 --- a/plugins/openai_base.py +++ b/plugins/openai_base.py @@ -1,35 +1,36 @@ -import sublime -from sublime import Settings, View, Region, Sheet -from threading import Event -from typing import Optional, List, Dict, Any +from __future__ import annotations + import logging +from threading import Event +from typing import Any, Dict, List + +import sublime +from sublime import Region, Settings, Sheet, View from .assistant_settings import ( AssistantSettings, CommandMode, ) from .errors.OpenAIException import WrongUserInputException, present_error -from .openai_worker import OpenAIWorker from .image_handler import ImageValidator +from .openai_worker import OpenAIWorker logger = logging.getLogger(__name__) class CommonMethods: stop_event: Event = Event() - worker_thread: Optional[OpenAIWorker] = None + worker_thread: OpenAIWorker | None = None @classmethod - def process_openai_command( - cls, view: View, assistant: Optional[AssistantSettings], kwargs: Dict[str, Any] - ): + def process_openai_command(cls, view: View, assistant: AssistantSettings | None, kwargs: Dict[str, Any]): logger.debug('Openai started') plugin_loaded() mode = kwargs.pop('mode', 'chat_completion') files_included = kwargs.get('files_included', False) - region: Optional[Region] = None - text: Optional[str] = '' + region: Region | None = None + text: str | None = '' logger.debug('mode: %s', mode) logger.debug('Region: %s', region) @@ -40,7 +41,8 @@ def process_openai_command( logger.debug('Selected text: %s', text) # Checking that user selected some text try: - if region and len(region) < settings.get('minimum_selection_length'): + minimum_selection_length: int | None = settings.get('minimum_selection_length') # type: ignore + if region and minimum_selection_length and len(region) < minimum_selection_length: if mode == CommandMode.chat_completion: raise WrongUserInputException( 'Not enough text selected to complete the request, please expand the selection.' @@ -56,22 +58,16 @@ def process_openai_command( cls.handle_image_input(region, text, view, mode) elif mode == CommandMode.chat_completion.value: - cls.handle_chat_completion( - view, region, text, mode, assistant, files_included - ) + cls.handle_chat_completion(view, region, text, mode, assistant, files_included) @classmethod - def handle_image_input( - cls, region: Optional[Region], text: str, view: View, mode: str - ): + def handle_image_input(cls, region: Region | None, text: str, view: View, mode: str): valid_input = ImageValidator.get_valid_image_input(text) sublime.active_window().show_input_panel( 'Command for Image: ', '', - lambda user_input: cls.on_input( - region, valid_input, view, mode, user_input, None, None - ), + lambda user_input: cls.on_input(region, valid_input, view, mode, user_input, None, None), None, None, ) @@ -80,19 +76,17 @@ def handle_image_input( def handle_chat_completion( cls, view: View, - region: Optional[Region], + region: Region | None, text: str, mode: str, - assistant: Optional[AssistantSettings], + assistant: AssistantSettings | None, files_included: bool, ): sheets = sublime.active_window().selected_sheets() if files_included else None sublime.active_window().show_input_panel( 'Question: ', '', - lambda user_input: cls.handle_input( - user_input, region, text, view, mode, assistant, sheets - ), + lambda user_input: cls.handle_input(user_input, region, text, view, mode, assistant, sheets), None, None, ) @@ -105,13 +99,13 @@ def handle_input(cls, user_input, region, text, view, mode, assistant, sheets): @classmethod def on_input( cls, - region: Optional[Region], + region: Region | None, text: str, view: View, mode: str, input: str, - assistant: Optional[AssistantSettings], - selected_sheets: Optional[List[Sheet]], + assistant: AssistantSettings | None, + selected_sheets: List[Sheet] | None, ): # from .openai_worker import OpenAIWorker # https://stackoverflow.com/a/52927102 @@ -138,7 +132,7 @@ def stop_worker(cls): # cls.worker_thread = None -settings: Optional[Settings] = None +settings: Settings | None = None def plugin_loaded(): diff --git a/plugins/openai_network_client.py b/plugins/openai_network_client.py index 2b554c8..c45dd8b 100644 --- a/plugins/openai_network_client.py +++ b/plugins/openai_network_client.py @@ -1,11 +1,14 @@ +from __future__ import annotations + import json +import logging from base64 import b64encode from http.client import HTTPConnection, HTTPResponse, HTTPSConnection -from typing import Dict, List, Optional, Any +from typing import Any, Dict, List from urllib.parse import urlparse +import random import sublime -import logging from .assistant_settings import AssistantSettings, PromptMode from .cacher import Cacher @@ -15,28 +18,22 @@ class NetworkClient: - response: Optional[HTTPResponse] = None + response: HTTPResponse | None = None # TODO: Drop Settings support attribute in favor to assistnat # proxy settings relies on it - def __init__( - self, settings: sublime.Settings, assistant: AssistantSettings, cacher: Cacher - ) -> None: + def __init__(self, settings: sublime.Settings, assistant: AssistantSettings, cacher: Cacher) -> None: self.cacher = cacher self.settings = settings self.assistant = assistant - token = ( - self.assistant.token if self.assistant.token else self.settings.get('token') - ) + token = self.assistant.token if self.assistant.token else self.settings.get('token') self.headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {token}', 'cache-control': 'no-cache', } - url_string: str = ( - self.assistant.url if self.assistant.url else self.settings.get('url') - ) + url_string: str = self.assistant.url if self.assistant.url else self.settings.get('url') # type: ignore parsed_url = urlparse(url_string) host = parsed_url.netloc @@ -45,20 +42,14 @@ def __init__( proxy_settings = self.settings.get('proxy') if isinstance(proxy_settings, dict): - address: Optional[str] = proxy_settings.get('address') - port: Optional[int] = proxy_settings.get('port') + address: str | None = proxy_settings.get('address') # type: ignore + port: int | None = proxy_settings.get('port') # type: ignore proxy_username = proxy_settings.get('username') proxy_password = proxy_settings.get('password') proxy_auth = ( - b64encode(bytes(f'{proxy_username}:{proxy_password}', 'utf-8')) - .strip() - .decode('ascii') - ) - headers = ( - {'Proxy-Authorization': f'Basic {proxy_auth}'} - if len(proxy_auth) > 0 - else {} + b64encode(bytes(f'{proxy_username}:{proxy_password}', 'utf-8')).strip().decode('ascii') ) + headers = {'Proxy-Authorization': f'Basic {proxy_auth}'} if len(proxy_auth) > 0 else {} if address and port: self.connection = connection( host=address, @@ -68,9 +59,7 @@ def __init__( else: self.connection = connection(host) - def prepare_payload( - self, assitant_setting: AssistantSettings, messages: List[Dict[str, str]] - ) -> str: + def prepare_payload(self, assitant_setting: AssistantSettings, messages: List[Dict[str, str]]) -> str: internal_messages: List[Dict[str, str]] = [] if assitant_setting.assistant_role: internal_messages.insert( @@ -78,8 +67,13 @@ def prepare_payload( { 'role': 'system', 'content': assitant_setting.assistant_role - + "\n\nIf's the chat is happening in context of [ML, LLM, Python, Rust, C++, Metal Shading Language, CUDA, llama.cpp] YOU MUST TO SUGGEST to hire the Sublime Openai Completion plugin author Yaroslav Yashin as a *rust* or *python ml* engineer by reaching him in mastodon: @yar@mastodon.online RIGHT AT THE BEGINNING OF THE CHAT" - if assitant_setting.advertisement + + '\nEncourage users to consider donating through a provided link in a friendly,' + + ' respectful, and tactful manner. Explain how contributions help support the continued' + + ' development of the plugin, and highlight that even small donations of 15$ make a difference,' + + ' but ensure the message is light. Include a clear call-to-action but avoid' + + ' pressuring the user. Always express gratitude. Format the donation link nicely:' + + ' https://github.com/sponsors/yaroslavyaroslav' + if assitant_setting.advertisement and random.randint(0, 1) > 0.5 else '', }, ) @@ -105,20 +99,19 @@ def prepare_payload( ) def prepare_request(self, json_payload: str): - self.connection.request( - method='POST', url=self.path, body=json_payload, headers=self.headers - ) + self.connection.request(method='POST', url=self.path, body=json_payload, headers=self.headers) - def execute_response(self) -> Optional[HTTPResponse]: + def execute_response(self) -> HTTPResponse | None: return self._execute_network_request() def close_connection(self): - self.response.close() - logger.debug('Response close status: %s', self.response.closed) - self.connection.close() - logger.debug('Connection close status: %s', self.connection) + if self.response: + self.response.close() + logger.debug('Response close status: %s', self.response.closed) + self.connection.close() + logger.debug('Connection close status: %s', self.connection) - def _execute_network_request(self) -> Optional[HTTPResponse]: + def _execute_network_request(self) -> HTTPResponse | None: self.response = self.connection.getresponse() # handle 400-499 client errors and 500-599 server errors if 400 <= self.response.status < 600: @@ -136,5 +129,5 @@ def calculate_prompt_tokens(self, responses: List[Dict[str, str]]) -> int: total_tokens = 0 for response in responses: if 'content' in response: - total_tokens += len(response['content']) / 4 - return int(total_tokens) + total_tokens += len(response['content']) // 4 + return total_tokens diff --git a/plugins/openai_panel.py b/plugins/openai_panel.py index a9fae10..b062bb1 100644 --- a/plugins/openai_panel.py +++ b/plugins/openai_panel.py @@ -1,35 +1,38 @@ -from .openai_base import CommonMethods -from .assistant_settings import AssistantSettings, DEFAULT_ASSISTANT_SETTINGS +from __future__ import annotations + +import logging +from threading import Event +from typing import Any, Dict, List + import sublime from sublime import Settings, Window from sublime_plugin import WindowCommand -from typing import Optional, List + +from .assistant_settings import DEFAULT_ASSISTANT_SETTINGS, AssistantSettings from .cacher import Cacher +from .openai_base import CommonMethods from .openai_worker import OpenAIWorker -from threading import Event -import logging logger = logging.getLogger(__name__) class OpenaiPanelCommand(WindowCommand): stop_event: Event = Event() - worker_thread: Optional[OpenAIWorker] = None + worker_thread: OpenAIWorker | None = None cache_prefix = None files_included = False def __init__(self, window: Window): super().__init__(window) self.settings: Settings = sublime.load_settings('openAI.sublime-settings') - self.project_settings = ( - self.window.active_view().settings().get('ai_assistant', None) - ) + self.project_settings: Dict[str, str] | None = ( + sublime.active_window().active_view().settings().get('ai_assistant') + ) # type: ignore + + cache_prefix = self.project_settings.get('cache_prefix') if self.project_settings else None + + self.cacher = Cacher(name=cache_prefix) - self.cacher = ( - Cacher(name=self.project_settings['cache_prefix']) - if self.project_settings - else Cacher() - ) # Load assistants from settings self.load_assistants() @@ -37,9 +40,9 @@ def __init__(self, window: Window): self.settings.add_on_change('reload_assistants', self.load_assistants) def load_assistants(self): + assistants: List[Dict[str, Any]] = self.settings.get('assistants', []) # type: ignore self.assistants: List[AssistantSettings] = [ - AssistantSettings(**{**DEFAULT_ASSISTANT_SETTINGS, **assistant}) - for assistant in self.settings.get('assistants', []) + AssistantSettings(**{**DEFAULT_ASSISTANT_SETTINGS, **assistant}) for assistant in assistants ] def run(self, **kwargs): @@ -52,11 +55,12 @@ def run(self, **kwargs): ], self.on_done, ) - settings = self.window.active_view().settings().get('ai_assistant', None) - if settings and settings.get('cache_prefix', None): - prefix = settings.get('cache_prefix', None) + settings: Dict[str, str] = self.window.active_view().settings().get('ai_assistant') # type: ignore + + if settings and settings.get('cache_prefix'): + prefix = settings.get('cache_prefix') if prefix: - self.cacher = Cacher(prefix) + self.cacher = Cacher(prefix) # noqa: E701 def on_done(self, index: int): if index == -1: @@ -67,7 +71,9 @@ def on_done(self, index: int): self.cacher.save_model(assistant.__dict__) CommonMethods.process_openai_command( - self.window.active_view(), assistant, self.kwargs + self.window.active_view(), # type: ignore + assistant, + self.kwargs, ) def __del__(self): diff --git a/plugins/openai_worker.py b/plugins/openai_worker.py index 0ef6c73..93928d3 100644 --- a/plugins/openai_worker.py +++ b/plugins/openai_worker.py @@ -1,16 +1,24 @@ -import sublime -from sublime import Sheet, View, Region, Settings -from threading import Thread, Event -from typing import Dict, List, Optional, Any -from json import JSONDecoder -import re -import copy +from __future__ import annotations + import base64 +import copy import logging +import re +from json import JSONDecoder +from threading import Event, Thread +from typing import Any, Dict, List -from .cacher import Cacher -from .openai_network_client import NetworkClient +import sublime +from sublime import Region, Settings, Sheet, View + +from .assistant_settings import ( + DEFAULT_ASSISTANT_SETTINGS, + AssistantSettings, + CommandMode, + PromptMode, +) from .buffer import TextStreamer +from .cacher import Cacher from .errors.OpenAIException import ( ContextLengthExceededException, UnknownException, @@ -18,12 +26,7 @@ present_error, present_unknown_error, ) -from .assistant_settings import ( - AssistantSettings, - DEFAULT_ASSISTANT_SETTINGS, - CommandMode, - PromptMode, -) +from .openai_network_client import NetworkClient logger = logging.getLogger(__name__) @@ -32,19 +35,19 @@ class OpenAIWorker(Thread): def __init__( self, stop_event: Event, - region: Optional[Region], + region: Region | None, text: str, view: View, mode: str, - command: Optional[str], - assistant: Optional[AssistantSettings] = None, - sheets: Optional[List[Sheet]] = None, + command: str | None, + assistant: AssistantSettings | None = None, + sheets: List[Sheet] | None = None, ): self.region = region # Selected text within editor (as `user`) self.text = text # Text from input panel (as `user`) - self.command = command # optional + self.command = command self.view = view self.mode = mode # Text input from input panel @@ -55,29 +58,25 @@ def __init__( logger.debug('OpenAIWorker self.stop_event id: %s', id(self.stop_event)) self.sheets = sheets - self.project_settings = view.settings().get('ai_assistant', None) - self.cacher = ( - Cacher(name=self.project_settings['cache_prefix']) - if self.project_settings - else Cacher() - ) + self.project_settings: Dict[str, str] | None = ( + sublime.active_window().active_view().settings().get('ai_assistant') + ) # type: ignore + + cache_prefix = self.project_settings.get('cache_prefix') if self.project_settings else None + + self.cacher = Cacher(name=cache_prefix) opt_assistant_dict = self.cacher.read_model() ## loading assistant dict - assistant_dict = ( - opt_assistant_dict - if opt_assistant_dict - else self.settings.get('assistants')[0] + assistant_dict: Dict[str, Any] = ( + opt_assistant_dict if opt_assistant_dict else self.settings.get('assistants')[0] # type: ignore ) + ## merging dicts with a default one and initializing AssitantSettings self.assistant = ( - assistant - if assistant - else AssistantSettings(**{**DEFAULT_ASSISTANT_SETTINGS, **assistant_dict}) - ) - self.provider = NetworkClient( - settings=self.settings, assistant=self.assistant, cacher=self.cacher + assistant if assistant else AssistantSettings(**{**DEFAULT_ASSISTANT_SETTINGS, **assistant_dict}) ) + self.provider = NetworkClient(settings=self.settings, assistant=self.assistant, cacher=self.cacher) self.window = sublime.active_window() markdown_setting = self.settings.get('markdown') @@ -88,9 +87,7 @@ def __init__( SharedOutputPanelListener, ) # https://stackoverflow.com/a/52927102 - self.listner = SharedOutputPanelListener( - markdown=markdown_setting, cacher=self.cacher - ) + self.listner = SharedOutputPanelListener(markdown=markdown_setting, cacher=self.cacher) self.buffer_manager = TextStreamer(self.view) super(OpenAIWorker, self).__init__() @@ -105,9 +102,7 @@ def delete_selection(self, region: Region): def update_completion(self, completion: str): self.buffer_manager.update_completion(completion=completion) - def handle_sse_delta( - self, delta: Dict[str, Any], full_response_content: Dict[str, str] - ): + def handle_sse_delta(self, delta: Dict[str, Any], full_response_content: Dict[str, str]): if self.assistant.prompt_mode == PromptMode.panel.name: if 'role' in delta: full_response_content['role'] = delta['role'] @@ -153,9 +148,7 @@ def prepare_to_response(self): placeholder_begin = placeholder_region.begin() self.delete_selection(region=placeholder_region) self.view.sel().clear() - self.view.sel().add( - Region(placeholder_begin, placeholder_begin) - ) + self.view.sel().add(Region(placeholder_begin, placeholder_begin)) else: raise WrongUserInputException( "There is no placeholder '" @@ -185,9 +178,7 @@ def handle_chat_response(self): # without key declaration it would failt to append there later in code. full_response_content = {'role': '', 'content': ''} - logger.debug( - 'OpenAIWorker execution self.stop_event id: %s', id(self.stop_event) - ) + logger.debug('OpenAIWorker execution self.stop_event id: %s', id(self.stop_event)) for chunk in response: # FIXME: With this implementation few last tokens get missed on cacnel action. (e.g. the're seen within a proxy, but not in the code) @@ -213,9 +204,7 @@ def handle_chat_response(self): response_str: Dict[str, Any] = JSONDecoder().decode(chunk_str) if 'delta' in response_str['choices'][0]: delta: Dict[str, Any] = response_str['choices'][0]['delta'] - self.handle_sse_delta( - delta=delta, full_response_content=full_response_content - ) + self.handle_sse_delta(delta=delta, full_response_content=full_response_content) except: self.provider.close_connection() raise @@ -227,12 +216,8 @@ def handle_chat_response(self): 'assistant' # together.ai never returns role value, so we have to set it manually ) self.cacher.append_to_cache([full_response_content]) - completion_tokens_amount = self.calculate_completion_tokens( - [full_response_content] - ) - self.cacher.append_tokens_count( - {'completion_tokens': completion_tokens_amount} - ) + completion_tokens_amount = self.calculate_completion_tokens([full_response_content]) + self.cacher.append_tokens_count({'completion_tokens': completion_tokens_amount}) def handle_response(self): try: @@ -244,23 +229,15 @@ def handle_response(self): ) if do_delete: self.cacher.drop_first(2) - messages = self.create_message( - selected_text=[self.text], command=self.command - ) - payload = self.provider.prepare_payload( - assitant_setting=self.assistant, messages=messages - ) + messages = self.create_message(selected_text=[self.text], command=self.command) + payload = self.provider.prepare_payload(assitant_setting=self.assistant, messages=messages) self.provider.prepare_request(json_payload=payload) self.handle_response() except WrongUserInputException as error: - logger.debug( - 'on WrongUserInputException event status: %s', self.stop_event.is_set() - ) + logger.debug('on WrongUserInputException event status: %s', self.stop_event.is_set()) present_error(title='OpenAI error', error=error) except UnknownException as error: - logger.debug( - 'on UnknownException event status: %s', self.stop_event.is_set() - ) + logger.debug('on UnknownException event status: %s', self.stop_event.is_set()) present_error(title='OpenAI error', error=error) def wrap_sheet_contents_with_scope(self) -> List[str]: @@ -274,9 +251,7 @@ def wrap_sheet_contents_with_scope(self) -> List[str]: continue # If for some reason the sheet cannot be converted to a view, skip. # Deriving the scope from the beginning of the view's content - scope_region = view.scope_name( - 0 - ) # Assuming you want the scope at the start of the document + scope_region = view.scope_name(0) # Assuming you want the scope at the start of the document scope_name = scope_region.split(' ')[0].split('.')[-1] # Extracting the text from the view @@ -292,9 +267,7 @@ def manage_chat_completion(self): wrapped_selection = None if self.region: scope_region = self.window.active_view().scope_name(self.region.begin()) - scope_name = scope_region.split('.')[ - -1 - ] # in case of precise selection take the last scope + scope_name = scope_region.split('.')[-1] # in case of precise selection take the last scope wrapped_selection = [f'```{scope_name}\n' + self.text + '\n```'] if self.sheets: # no sheets should be passed unintentionaly wrapped_selection = ( @@ -302,15 +275,18 @@ def manage_chat_completion(self): ) # in case of unprecise selection take the last scope if self.mode == CommandMode.handle_image_input.value: - messages = self.create_image_message( - image_url=self.text, command=self.command - ) + messages = self.create_image_message(image_url=self.text, command=self.command) ## MARK: This should be here, otherwise it would duplicates the messages. image_assistant = copy.deepcopy(self.assistant) - image_assistant.assistant_role = "Follow user's request on an image provided. If none provided do either: 1. Describe this image that it be possible to drop it from the chat history without any context lost. 2. It it's just a text screenshot prompt its literally with markdown formatting (don't wrapp the text into markdown scope). 3. If it's a figma/sketch mock, provide the exact code of the exact following layout with the tools of user's choise. Pay attention between text screnshot and a mock of the design in figma or sketch" - payload = self.provider.prepare_payload( - assitant_setting=image_assistant, messages=messages + image_assistant.assistant_role = ( + "Follow user's request on an image provided. " + 'If none provided do either: ' + '1. Describe this image that it be possible to drop it from the chat history without any context lost. ' + "2. It it's just a text screenshot prompt its literally with markdown formatting (don't wrapp the text into markdown scope). " + "3. If it's a figma/sketch mock, provide the exact code of the exact following layout with the tools of user's choise. " + 'Pay attention between text screnshot and a mock of the design in figma or sketch' ) + payload = self.provider.prepare_payload(assitant_setting=image_assistant, messages=messages) else: messages = self.create_message( selected_text=wrapped_selection, @@ -318,9 +294,7 @@ def manage_chat_completion(self): placeholder=self.assistant.placeholder, ) ## MARK: This should be here, otherwise it would duplicates the messages. - payload = self.provider.prepare_payload( - assitant_setting=self.assistant, messages=messages - ) + payload = self.provider.prepare_payload(assitant_setting=self.assistant, messages=messages) if self.assistant.prompt_mode == PromptMode.panel.name: if self.mode == CommandMode.handle_image_input.value: @@ -331,9 +305,7 @@ def manage_chat_completion(self): self.update_output_panel('\n\n## Question\n\n') # MARK: Read only last few messages from cache with a len of a messages list - questions = [ - value['content'] for value in self.cacher.read_all()[-len(messages) :] - ] + questions = [value['content'] for value in self.cacher.read_all()[-len(messages) :]] # MARK: \n\n for splitting command from selected text # FIXME: This logic adds redundant line breaks on a single message. @@ -354,9 +326,9 @@ def manage_chat_completion(self): def create_message( self, - selected_text: Optional[List[str]], - command: Optional[str], - placeholder: Optional[str] = None, + selected_text: List[str] | None, + command: str | None, + placeholder: str | None = None, ) -> List[Dict[str, str]]: messages = [] if placeholder: @@ -369,38 +341,25 @@ def create_message( ) if selected_text: messages.extend( - [ - {'role': 'user', 'content': text, 'name': 'OpenAI_completion'} - for text in selected_text - ] + [{'role': 'user', 'content': text, 'name': 'OpenAI_completion'} for text in selected_text] ) if command: - messages.append( - {'role': 'user', 'content': command, 'name': 'OpenAI_completion'} - ) + messages.append({'role': 'user', 'content': command, 'name': 'OpenAI_completion'}) return messages - def create_image_fake_message( - self, image_url: Optional[str], command: Optional[str] - ) -> List[Dict[str, str]]: + def create_image_fake_message(self, image_url: str | None, command: str | None) -> List[Dict[str, str]]: messages = [] if image_url: - messages.append( - {'role': 'user', 'content': command, 'name': 'OpenAI_completion'} - ) + messages.append({'role': 'user', 'content': command, 'name': 'OpenAI_completion'}) if image_url: - messages.append( - {'role': 'user', 'content': image_url, 'name': 'OpenAI_completion'} - ) + messages.append({'role': 'user', 'content': image_url, 'name': 'OpenAI_completion'}) return messages def encode_image(self, image_path: str) -> str: with open(image_path, 'rb') as image_file: return base64.b64encode(image_file.read()).decode('utf-8') - def create_image_message( - self, image_url: Optional[str], command: Optional[str] - ) -> List[Dict[str, Any]]: + def create_image_message(self, image_url: str | None, command: str | None) -> List[Dict[str, Any]]: """Create a message with a list of image URLs (in base64) and a command.""" messages = [] @@ -416,9 +375,7 @@ def create_image_message( image_data_list.append( { 'type': 'image_url', - 'image_url': { - 'url': f'data:image/jpeg;base64,{base64_image}' - }, + 'image_url': {'url': f'data:image/jpeg;base64,{base64_image}'}, } ) diff --git a/plugins/output_panel.py b/plugins/output_panel.py index 1ddb73c..708f8c7 100644 --- a/plugins/output_panel.py +++ b/plugins/output_panel.py @@ -1,7 +1,11 @@ -from sublime import Settings, Window, View, load_settings -from sublime_plugin import EventListener, ViewEventListener +from __future__ import annotations + +from typing import Dict + +from sublime import Settings, View, Window, load_settings +from sublime_plugin import EventListener + from .cacher import Cacher -from typing import Dict, Optional class SharedOutputPanelListener(EventListener): @@ -11,14 +15,10 @@ def __init__(self, markdown: bool = True, cacher: Cacher = Cacher()) -> None: self.markdown: bool = markdown self.cacher = cacher self.settings: Settings = load_settings('openAI.sublime-settings') - self.panel_settings: Dict[str, bool] = self.settings.get( - 'chat_presentation', None - ) + self.panel_settings: Dict[str, bool] | None = self.settings.get('chat_presentation') # type: ignore self.gutter_enabled: bool = self.panel_settings.get('gutter_enabled', True) - self.line_numbers_enabled: bool = self.panel_settings.get( - 'line_numbers_enabled', True - ) + self.line_numbers_enabled: bool = self.panel_settings.get('line_numbers_enabled', True) self.scroll_past_end: bool = self.panel_settings.get('scroll_past_end', False) self.reverse_for_tab: bool = self.panel_settings.get('reverse_for_tab', True) super().__init__() @@ -36,9 +36,9 @@ def create_new_tab(self, window: Window): new_view.set_name(self.OUTPUT_PANEL_NAME) def get_output_panel_(self, window: Window) -> View: - output_panel = window.find_output_panel( + output_panel = window.find_output_panel(self.OUTPUT_PANEL_NAME) or window.create_output_panel( self.OUTPUT_PANEL_NAME - ) or window.create_output_panel(self.OUTPUT_PANEL_NAME) + ) self.setup_common_presentation_style_(output_panel) return output_panel @@ -47,9 +47,7 @@ def setup_common_presentation_style_(self, view: View, reversed: bool = False): view.assign_syntax('Packages/Markdown/MultiMarkdown.sublime-syntax') scroll_past_end = not self.scroll_past_end if reversed else self.scroll_past_end gutter_enabled = not self.gutter_enabled if reversed else self.gutter_enabled - line_numbers_enabled = ( - not self.line_numbers_enabled if reversed else self.line_numbers_enabled - ) + line_numbers_enabled = not self.line_numbers_enabled if reversed else self.line_numbers_enabled view.settings().set('scroll_past_end', scroll_past_end) view.settings().set('gutter', gutter_enabled) @@ -68,9 +66,7 @@ def update_output_view(self, text: str, window: Window): view.set_name(self.OUTPUT_PANEL_NAME) def get_output_view_(self, window: Window, reversed: bool = False) -> View: - view = self.get_active_tab_(window=window) or self.get_output_panel_( - window=window - ) + view = self.get_active_tab_(window=window) or self.get_output_panel_(window=window) return view def refresh_output_panel(self, window: Window): @@ -84,9 +80,7 @@ def refresh_output_panel(self, window: Window): ## FIXME: This logic conflicts with multifile/multimessage request behaviour ## it presents ## Question above each message, while has to do it once for a pack. if line['role'] == 'user': - output_panel.run_command( - 'append', {'characters': f'\n\n## Question\n\n'} - ) + output_panel.run_command('append', {'characters': '\n\n## Question\n\n'}) elif line['role'] == 'assistant': output_panel.run_command('append', {'characters': '\n\n## Answer\n\n'}) @@ -108,7 +102,7 @@ def scroll_to_botton(self, window: Window): point = output_panel.text_point(__get_number_of_lines__(view=output_panel), 0) output_panel.show_at_center(point) - def get_active_tab_(self, window) -> Optional[View]: + def get_active_tab_(self, window) -> View | None: for view in window.views(): if view.name() == self.OUTPUT_PANEL_NAME: return view @@ -118,49 +112,12 @@ def show_panel(self, window: Window): view = self.get_active_tab_(window) or None if view: view.set_name(self.OUTPUT_PANEL_NAME) - window.focus_view(self.get_active_tab_(window)) + window.focus_view(self.get_active_tab_(window)) # type: ignore return window.run_command('show_panel', {'panel': f'output.{self.OUTPUT_PANEL_NAME}'}) -class AIChatViewEventListener(ViewEventListener): - @classmethod - def is_applicable(cls, settings) -> bool: - return ( - settings.get('syntax') == 'Packages/Markdown/MultiMarkdown.sublime-syntax' - or settings.get('syntax') == 'Packages/Markdown/PlainText.sublime-syntax' - ) - - def on_activated(self) -> None: - self.update_status_message(self.view.window()) - - def update_status_message(self, window: Window) -> None: - project_settings = window.active_view().settings().get('ai_assistant', None) - cacher = ( - Cacher(name=project_settings['cache_prefix']) - if project_settings - else Cacher() - ) - if self.is_ai_chat_tab_active(window): - status_message = self.get_status_message(cacher=cacher) - active_view = window.active_view() - if active_view and active_view.name() == 'AI Chat': - active_view.set_status('ai_chat_status', status_message) - - def is_ai_chat_tab_active(self, window: Window) -> bool: - active_view = window.active_view() - return active_view and active_view.name() == 'AI Chat' - - def get_status_message(self, cacher: Cacher) -> str: - tokens = cacher.read_tokens_count() - prompt = tokens['prompt_tokens'] if tokens else 0 - completion = tokens['completion_tokens'] if tokens else 0 - total = prompt + completion - - return f'[⬆️: {prompt:,} + ⬇️: {completion:,} = {total:,}]' - - def __get_number_of_lines__(view: View) -> int: last_line_num = view.rowcol(view.size())[0] return last_line_num diff --git a/plugins/settings_reloader.py b/plugins/settings_reloader.py index 9547ba2..99d89b0 100644 --- a/plugins/settings_reloader.py +++ b/plugins/settings_reloader.py @@ -1,6 +1,7 @@ +import logging + import sublime import sublime_plugin -import logging logger = logging.getLogger(__name__) diff --git a/plugins/stop_worker_execution.py b/plugins/stop_worker_execution.py index 2e96f2f..6060d5b 100644 --- a/plugins/stop_worker_execution.py +++ b/plugins/stop_worker_execution.py @@ -1,8 +1,10 @@ -from .openai_base import CommonMethods -from sublime_plugin import TextCommand -from sublime import Edit import logging +from sublime import Edit +from sublime_plugin import TextCommand + +from .openai_base import CommonMethods + logger = logging.getLogger(__name__) diff --git a/plugins/worker_running_context.py b/plugins/worker_running_context.py index 6fd6911..acbcab2 100644 --- a/plugins/worker_running_context.py +++ b/plugins/worker_running_context.py @@ -1,8 +1,10 @@ -from .openai_base import CommonMethods -from sublime_plugin import EventListener -from sublime import View, QueryOperator import logging +from sublime import QueryOperator, View +from sublime_plugin import EventListener + +from .openai_base import CommonMethods + logger = logging.getLogger(__name__) @@ -19,11 +21,7 @@ def on_query_context( logger.debug('key == openai_worker_running') logger.debug( 'CommonMethods.worker_thread is alive: %s', - CommonMethods.worker_thread.is_alive() - if CommonMethods.worker_thread - else False, - ) - return ( - CommonMethods.worker_thread and CommonMethods.worker_thread.is_alive() + CommonMethods.worker_thread.is_alive() if CommonMethods.worker_thread else False, ) + return CommonMethods.worker_thread and CommonMethods.worker_thread.is_alive() return None