Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
yaroslavyaroslav committed Jul 7, 2023
2 parents 524d5ef + cb1bc05 commit 7f6202a
Show file tree
Hide file tree
Showing 12 changed files with 400 additions and 218 deletions.
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/static/**/image*.png export-ignore
/static/**/image*.png export-ignore
/.github/FUNDING.yml export-ignore
13 changes: 13 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These are supported funding model platforms

github: [yaroslavyaroslav] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
# patreon: # Replace with a single Patreon username
# open_collective: # Replace with a single Open Collective username
# ko_fi: # Replace with a single Ko-fi username
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
# liberapay: # Replace with a single Liberapay username
# issuehunt: # Replace with a single IssueHunt username
# otechie: # Replace with a single Otechie username
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
35 changes: 35 additions & 0 deletions buffer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Optional

class SublimeBuffer():

def __init__(self, view) -> None:
self.view = view

def prompt_completion(self, mode: str, completion: str, placeholder: Optional[str] = None):
completion = completion.replace("$", "\$")
if mode == 'insertion':
result = self.view.find(placeholder, 0, 1)
if result:
self.view.sel().clear()
self.view.sel().add(result)
# Replace the placeholder with the specified replacement text
self.view.run_command("insert_snippet", {"contents": completion})
return

elif mode == 'completion':
region = self.view.sel()[0]
if region.a <= region.b:
region.a = region.b
else:
region.b = region.a

self.view.sel().clear()
self.view.sel().add(region)
# Replace the placeholder with the specified replacement text
self.view.run_command("insert_snippet", {"contents": completion})
return

elif mode == 'edition': # it's just replacing all given text for now.
region = self.view.sel()[0]
self.view.run_command("insert_snippet", {"contents": completion})
return
22 changes: 22 additions & 0 deletions errors/OpenAIException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from sublime import error_message
from logging import exception

class OpenAIException(Exception):
"""Exception raised for errors in the input.
Attributes:
message -- explanation of the error
"""

def __init__(self, message: str):
self.message = message
super().__init__(self.message)

class ContextLengthExceededException(OpenAIException): ...

class UnknownException(OpenAIException): ...


def present_error(title: str, error: OpenAIException):
exception(f"{title}: {error.message}")
error_message(f"{title}\n{error.message}")
3 changes: 2 additions & 1 deletion messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"install": "README.md",
"2.0.0": "messages/2.0.0.txt",
"2.0.3": "messages/2.0.3.txt",
"2.0.4": "messages/2.0.4.txt"
"2.0.4": "messages/2.0.4.txt",
"2.1.0": "messages/2.1.0.txt"
}
2 changes: 1 addition & 1 deletion messages/2.0.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ChatGPT mode works the following way:
4. If you would like to fetch chat history to another window manually, you can do that by running the `OpenAI: Refresh Chat` command.
5. When you're done or want to start all over you should run the `OpenAI: Reset Chat History` command, which deletes the chat cache.

> You can bind both of the most usable commands `OpenAI: New Message` and `OpenAI: Show output panel`, to do that please follow `Settings`->`Package Control`->`OpenAI completion`->`Key Bindings`.
> You can bind both of the most usable commands `OpenAI: New Message` and `OpenAI: Show output panel`, to do that please follow `Settings` -> `Package Control` -> `OpenAI completion` -> `Key Bindings`.

> As for now there's just a single history instance. I guess this limitation would disappear sometime, but highly likely it wouldn't be soon.

Expand Down
17 changes: 17 additions & 0 deletions messages/2.1.0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
=> 2.1.0

## Features

- Completion streaming support.
- Drop the 2 farthest replies from the plugin cache dialogue.

### Completion streaming support.

Yep, you've heard it right. That new cool shiny way that you see in the original OpenAI Chat now comes to Sublime. Embrace, behold and all that stuff. Jokes aside — this thing only makes GPT-4 completion workable, by releasing the most significant tradeoff it has — long answering time. I mean GPT-4 answering time is still the same, but now you starting to see it up to 20 seconds earlier which is matters in terms of UX.

### Drop the 2 farthest replies from the plugin cache dialogue.

Now if you reach the context window limit, you're getting asked whether you or not wish to delete the 2 farthest messages (1 yours and 1 from the assistant) to shorter the chat history. If yes, the plugin would drop them and resend all the other chat history to OpenAI servers once again. This thing is recursive and will spit the popup in your face until the chat history would fit within a given model context window again. On cancel it will do nothing, as expected.

PS: As usual, if you have any issues feel free to open an issue [here](https://github.com/yaroslavyaroslav/OpenAI-sublime-text/issues).
PS2: If you feel happy with this plugin you can drop me some coins for paying my OpenAI bills on Ethereum here (including L2 chains): 0x60843b4026Ff630b36835a8b78561eDD559ab208.
34 changes: 25 additions & 9 deletions openAI.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,41 @@
// ____Affects only chat completion mode___
"chat_model": "gpt-3.5-turbo",

// Controls randomness: Lowering results in less random completions.
// As the temperature approaches zero, the model will become deterministic and repetitive.
// ChatGPT model knows how to role, lol
// It can act as a different kind of person. Recently in this plugin it was acting
// like as a code assistant. With this setting you're able to set it up more precisely.
// E.g. "You are (rust|python|js|whatewer) developer assistant", "You are an english tutor" and so on.
"assistant_role": "You are a senior code assitant",

// What sampling temperature to use, between 0 and 2.
// Higher values like 0.8 will make the output more random,
// while lower values like 0.2 will make it more focused and deterministic.
//
// OpenAI generally recommend altering this or top_p but not both.
"temperature": 0.7,

// The maximum number of tokens to generate.
// Requests can use up to 2,048 or 4,000 tokens shared between prompt and completion.
// The exact limit varies by model.
// The maximum number of tokens to generate in the completion.
// The token count of your prompt plus `max_tokens` cannot exceed the model's context length.
// (One token is roughly 4 characters for normal English text)
// Does not affect editing mode.
"max_tokens": 256,

// Controls diversity via nucleus sampling:
// 0.5 means half of all likelihood-weighted options are considered.
// An alternative to sampling with temperature, called nucleus sampling,
// where the model considers the results of the tokens with `top_p` probability mass.
// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
// OpenAI generally recommend altering this or temperature but not both.
"top_p": 1,

// Controls the minimum height of the debugger output panels in lines.
// Number between -2.0 and 2.0.
// Positive values penalize new tokens based on their existing frequency in the text so far,
// decreasing the model's likelihood to repeat the same line verbatim.
// docs: https://platform.openai.com/docs/api-reference/parameter-details
"frequency_penalty": 0,

// Some new features are locked behind this flag.
// Number between -2.0 and 2.0.
/// Positive values penalize new tokens based on whether they appear in the text so far,
// increasing the model's likelihood to talk about new topics.
// docs: https://platform.openai.com/docs/api-reference/parameter-details
"presence_penalty": 0,

// Placeholder for insert mode. You should to put it where you want the suggestion to be inserted.
Expand Down
9 changes: 3 additions & 6 deletions openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def run(self, edit, **kwargs):
text = self.view.substr(region)


# Cheching that user select some text
# Checking that user select some text
try:
if region.__len__() < settings.get("minimum_selection_length"):
if mode != 'chat_completion' and mode != 'reset_chat_history' and mode != 'refresh_output_panel':
Expand Down Expand Up @@ -57,11 +57,8 @@ def run(self, edit, **kwargs):
elif mode == 'refresh_output_panel':
from .outputpanel import SharedOutputPanelListener
window = sublime.active_window()
listner = SharedOutputPanelListener()
listner.refresh_output_panel(
window=window,
markdown=settings.get('markdown'),
)
listner = SharedOutputPanelListener(markdown=settings.get('markdown'))
listner.refresh_output_panel(window=window)
listner.show_panel(window=window)
else: # mode 'chat_completion', always in panel
sublime.active_window().show_input_panel("Question: ", "", functools.partial(self.on_input, "region", "text", self.view, mode), None, None)
Expand Down
104 changes: 104 additions & 0 deletions openai_network_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from http.client import HTTPSConnection, HTTPResponse
from os import error
from urllib.error import HTTPError, URLError
from typing import Optional, List
import logging
import sublime
import json
from .errors.OpenAIException import ContextLengthExceededException, UnknownException, present_error
from .cacher import Cacher

class NetworkClient():
mode = ""
def __init__(self, settings: sublime.Settings) -> None:
self.settings = settings
self.headers = {
'Content-Type': "application/json",
'Authorization': f'Bearer {self.settings.get("token")}',
'cache-control': "no-cache",
}

proxy_settings = self.settings.get('proxy')
if isinstance(proxy_settings, dict):
address = proxy_settings.get('address')
port = proxy_settings.get('port')
if address and len(address) > 0 and port:
self.connection = HTTPSConnection(
host=address,
port=port
)
self.connection.set_tunnel("api.openai.com")
else:
self.connection = HTTPSConnection("api.openai.com")

def prepare_payload(self, mode: str, text: Optional[str] = None, command: Optional[str] = None, role: Optional[str] = None, parts: Optional[List[str]] = None) -> str:
self.mode = mode
if mode == 'insertion':
prompt, suffix = (parts[0], parts[1]) if parts and len(parts) >= 2 else ("Print out that input text is wrong", "Print out that input text is wrong")
return json.dumps({
"model": self.settings.get("model"),
"prompt": prompt,
"suffix": suffix,
"temperature": self.settings.get("temperature"),
"max_tokens": self.settings.get("max_tokens"),
"top_p": self.settings.get("top_p"),
"frequency_penalty": self.settings.get("frequency_penalty"),
"presence_penalty": self.settings.get("presence_penalty")
})

elif mode == 'edition':
return json.dumps({
"model": self.settings.get('edit_model'),
"input": text,
"instruction": command,
"temperature": self.settings.get("temperature"),
"top_p": self.settings.get("top_p"),
})

elif mode == 'completion':
return json.dumps({
"prompt": text,
"model": self.settings.get("model"),
"temperature": self.settings.get("temperature"),
"max_tokens": self.settings.get("max_tokens"),
"top_p": self.settings.get("top_p"),
"frequency_penalty": self.settings.get("frequency_penalty"),
"presence_penalty": self.settings.get("presence_penalty")
})

elif mode == 'chat_completion':
return json.dumps({
# Todo add uniq name for each output panel (e.g. each window)
"messages": [
{"role": "system", "content": role},
*Cacher().read_all()
],
"model": self.settings.get('chat_model'),
"temperature": self.settings.get("temperature"),
"max_tokens": self.settings.get("max_tokens"),
"top_p": self.settings.get("top_p"),
"stream": True
})
else: raise Exception("Undefined mode")

def prepare_request(self, gateway, json_payload):
self.connection.request(method="POST", url=gateway, body=json_payload, headers=self.headers)

def execute_response(self) -> Optional[HTTPResponse]:
return self._execute_network_request()

def _execute_network_request(self) -> Optional[HTTPResponse]:
response = self.connection.getresponse()
# handle 400-499 client errors and 500-599 server errors
if 400 <= response.status < 600:
error_object = response.read().decode('utf-8')
error_data = json.loads(error_object)
if error_data.get('error', {}).get('code') == 'context_length_exceeded':
raise ContextLengthExceededException(error_data['error']['message'])
# raise custom exception for 'context_length_exceeded' error
# if error_data.get('error', {}).get('code') == 'context_length_exceeded':
# raise ContextLengthExceeded(error_data['error']['message'])
code = error_data.get('error', {}).get('code') or error_data.get('error', {}).get('type')
unknown_error = UnknownException(error_data.get('error', {}).get('message'))
present_error(title=code, error=unknown_error)
return response
Loading

0 comments on commit 7f6202a

Please sign in to comment.