2 Commits

Author SHA1 Message Date
Oleksandr Kozachuk 9a957a89ac Switch to current version of OpenAI. 2024-03-30 14:17:38 +01:00
Oleksandr Kozachuk 5d1bb1f9e4 Fix some of the commands. 2023-11-10 10:42:46 +01:00
6 changed files with 92 additions and 201 deletions
+14 -18
View File
@@ -47,12 +47,12 @@ class OpenAIAnswer:
self.finished = True self.finished = True
if not self.finished: if not self.finished:
found_choice = False found_choice = False
for choice in chunk['choices']: for choice in chunk.choices:
if not choice['finish_reason']: if not choice.finish_reason:
self.streams[choice['index']].data.append(choice['delta']['content']) self.streams[choice.index].data.append(choice.delta.content)
self.tokens.completion += len(self.encoding.encode(choice['delta']['content'])) self.tokens.completion += len(self.encoding.encode(choice.delta.content))
self.tokens.total = self.tokens.prompt + self.tokens.completion self.tokens.total = self.tokens.prompt + self.tokens.completion
if choice['index'] == self.idx: if choice.index == self.idx:
found_choice = True found_choice = True
if not found_choice: if not found_choice:
return False return False
@@ -68,6 +68,10 @@ class OpenAI(AI):
self.ID = config.ID self.ID = config.ID
self.name = config.name self.name = config.name
self.config = config self.config = config
self.client = openai.OpenAI(api_key=self.config.api_key)
def _completions(self, *args, **kw): # type: ignore
return self.client.chat.completions.create(*args, **kw)
def request(self, def request(self,
question: Message, question: Message,
@@ -80,10 +84,9 @@ class OpenAI(AI):
nr. of messages in the 'AIResponse'. nr. of messages in the 'AIResponse'.
""" """
self.encoding = tiktoken.encoding_for_model(self.config.model) self.encoding = tiktoken.encoding_for_model(self.config.model)
openai.api_key = self.config.api_key
oai_chat, prompt_tokens = self.openai_chat(chat, self.config.system, question) oai_chat, prompt_tokens = self.openai_chat(chat, self.config.system, question)
tokens: Tokens = Tokens(prompt_tokens, 0, prompt_tokens) tokens: Tokens = Tokens(prompt_tokens, 0, prompt_tokens)
response = openai.ChatCompletion.create( response = self._completions(
model=self.config.model, model=self.config.model,
messages=oai_chat, messages=oai_chat,
temperature=self.config.temperature, temperature=self.config.temperature,
@@ -114,9 +117,8 @@ class OpenAI(AI):
Return all models supported by this AI. Return all models supported by this AI.
""" """
ret = [] ret = []
for engine in sorted(openai.Engine.list()['data'], key=lambda x: x['id']): for engine in sorted(self.client.models.list().data, key=lambda x: x.id):
if engine['ready']: ret.append(engine.id)
ret.append(engine['id'])
ret.sort() ret.sort()
return ret return ret
@@ -124,14 +126,8 @@ class OpenAI(AI):
""" """
Print all models supported by the current AI. Print all models supported by the current AI.
""" """
not_ready = [] for model in self.models():
for engine in sorted(openai.Engine.list()['data'], key=lambda x: x['id']): print(model)
if engine['ready']:
print(engine['id'])
else:
not_ready.append(engine['id'])
if len(not_ready) > 0:
print('\nNot ready: ' + ', '.join(not_ready))
def openai_chat(self, chat: Chat, system: str, def openai_chat(self, chat: Chat, system: str,
question: Optional[Message] = None) -> tuple[ChatType, int]: question: Optional[Message] = None) -> tuple[ChatType, int]:
-64
View File
@@ -1,64 +0,0 @@
"""
Contains shared functions for the various CMM subcommands.
"""
import argparse
from pathlib import Path
from ..message import Message, MessageError, source_code
def add_file_as_text(question_parts: list[str], file: str) -> None:
"""
Add the given file as plain text to the question part list.
If the file is a Message, add the answer.
"""
file_path = Path(file)
content: str
try:
message = Message.from_file(file_path)
if message and message.answer:
content = message.answer
except MessageError:
with open(file) as r:
content = r.read().strip()
if len(content) > 0:
question_parts.append(content)
def add_file_as_code(question_parts: list[str], file: str) -> None:
"""
Add all source code from the given file. If no code segments can be extracted,
the whole content is added as source code segment. If the file is a Message,
extract the source code from the answer.
"""
file_path = Path(file)
content: str
try:
message = Message.from_file(file_path)
if message and message.answer:
content = message.answer
except MessageError:
with open(file) as r:
content = r.read().strip()
# extract and add source code
code_parts = source_code(content, include_delims=True)
if len(code_parts) > 0:
question_parts += code_parts
else:
question_parts.append(f"```\n{content}\n```")
def invert_input_tag_args(args: argparse.Namespace) -> None:
"""
Changes the semantics of the INPUT tags for this command:
* not tags specified on the CLI -> no tags are selected
* empty tags specified on the CLI -> all tags are selected
"""
if args.or_tags is None:
args.or_tags = set()
elif len(args.or_tags) == 0:
args.or_tags = None
if args.and_tags is None:
args.and_tags = set()
elif len(args.and_tags) == 0:
args.and_tags = None
+58 -2
View File
@@ -3,10 +3,9 @@ import argparse
from pathlib import Path from pathlib import Path
from itertools import zip_longest from itertools import zip_longest
from copy import deepcopy from copy import deepcopy
from .common import invert_input_tag_args, add_file_as_code, add_file_as_text
from ..configuration import Config from ..configuration import Config
from ..chat import ChatDB, msg_location from ..chat import ChatDB, msg_location
from ..message import Message, MessageFilter, Question from ..message import Message, MessageFilter, MessageError, Question, source_code
from ..ai_factory import create_ai from ..ai_factory import create_ai
from ..ai import AI, AIResponse from ..ai import AI, AIResponse
@@ -15,6 +14,47 @@ class QuestionCmdError(Exception):
pass pass
def add_file_as_text(question_parts: list[str], file: str) -> None:
"""
Add the given file as plain text to the question part list.
If the file is a Message, add the answer.
"""
file_path = Path(file)
content: str
try:
message = Message.from_file(file_path)
if message and message.answer:
content = message.answer
except MessageError:
with open(file) as r:
content = r.read().strip()
if len(content) > 0:
question_parts.append(content)
def add_file_as_code(question_parts: list[str], file: str) -> None:
"""
Add all source code from the given file. If no code segments can be extracted,
the whole content is added as source code segment. If the file is a Message,
extract the source code from the answer.
"""
file_path = Path(file)
content: str
try:
message = Message.from_file(file_path)
if message and message.answer:
content = message.answer
except MessageError:
with open(file) as r:
content = r.read().strip()
# extract and add source code
code_parts = source_code(content, include_delims=True)
if len(code_parts) > 0:
question_parts += code_parts
else:
question_parts.append(f"```\n{content}\n```")
def create_msg_args(msg: Message, args: argparse.Namespace) -> argparse.Namespace: def create_msg_args(msg: Message, args: argparse.Namespace) -> argparse.Namespace:
""" """
Takes an existing message and CLI arguments, and returns modified args based Takes an existing message and CLI arguments, and returns modified args based
@@ -123,6 +163,22 @@ def repeat_messages(messages: list[Message], chat: ChatDB, args: argparse.Namesp
make_request(ai, chat, message, msg_args) make_request(ai, chat, message, msg_args)
def invert_input_tag_args(args: argparse.Namespace) -> None:
"""
Changes the semantics of the INPUT tags for this command:
* not tags specified on the CLI -> no tags are selected
* empty tags specified on the CLI -> all tags are selected
"""
if args.or_tags is None:
args.or_tags = set()
elif len(args.or_tags) == 0:
args.or_tags = None
if args.and_tags is None:
args.and_tags = set()
elif len(args.and_tags) == 0:
args.and_tags = None
def question_cmd(args: argparse.Namespace, config: Config) -> None: def question_cmd(args: argparse.Namespace, config: Config) -> None:
""" """
Handler for the 'question' command. Handler for the 'question' command.
-69
View File
@@ -1,69 +0,0 @@
import argparse
from pathlib import Path
from itertools import zip_longest
from .common import invert_input_tag_args, add_file_as_text
from ..configuration import Config
from ..message import MessageFilter, Message, Question
from ..chat import ChatDB, msg_location
class TranslationCmdError(Exception):
pass
def create_message(chat: ChatDB, args: argparse.Namespace) -> Message:
"""
Create a new message from the given arguments and write it
to the cache directory.
"""
text_parts = []
if args.create is not None:
text_list = args.create
elif args.ask is not None:
text_list = args.ask
else:
raise TranslationCmdError("No input text found")
# NOTE: we currently support only one input document
text_files = args.input_document if args.input_document is not None else []
# create the full text to be translated by combining all text parts
# from the arguments with the content of the document
for text, text_file in zip_longest(text_list, text_files, fillvalue=None):
if text is not None and len(text.strip()) > 0:
text_parts.append(text)
if text_file is not None and len(text_file) > 0:
add_file_as_text(text_parts, text_file)
full_text = '\n\n'.join([str(s) for s in text_parts])
# FIXME: prepend translation prompt and glossaries (if given)
message = Message(question=Question(full_text),
tags=args.output_tags,
ai=args.AI,
model=args.model)
# only write the new message to the cache,
# don't add it to the internal list
chat.cache_write([message])
return message
def translation_cmd(args: argparse.Namespace, config: Config) -> None:
"""
Handler for the 'translation' command. Creates and executes translation
requests based on the input and selected AI. Depending on the AI, the
whole process may be significantly different (e.g. DeepL vs OpenAI).
"""
invert_input_tag_args(args)
mfilter = MessageFilter(tags_or=args.or_tags,
tags_and=args.and_tags,
tags_not=args.exclude_tags)
chat = ChatDB.from_dir(cache_path=Path(config.cache),
db_path=Path(config.db),
mfilter=mfilter,
glob=args.glob,
loc=msg_location(args.location))
# if it's a new translation, create and store it immediately
if args.ask or args.create:
# message = create_message(chat, args)
create_message(chat, args)
if args.create:
return
+1 -17
View File
@@ -14,7 +14,6 @@ from .commands.tags import tags_cmd
from .commands.config import config_cmd from .commands.config import config_cmd
from .commands.hist import hist_cmd from .commands.hist import hist_cmd
from .commands.print import print_cmd from .commands.print import print_cmd
from .commands.translation import translation_cmd
from .chat import msg_location from .chat import msg_location
@@ -103,7 +102,7 @@ def create_parser() -> argparse.ArgumentParser:
# 'tags' command parser # 'tags' command parser
tags_cmd_parser = cmdparser.add_parser('tags', tags_cmd_parser = cmdparser.add_parser('tags',
help="Manage tags.", help="Manage tags.",
aliases=['T']) aliases=['t'])
tags_cmd_parser.set_defaults(func=tags_cmd) tags_cmd_parser.set_defaults(func=tags_cmd)
tags_group = tags_cmd_parser.add_mutually_exclusive_group(required=True) tags_group = tags_cmd_parser.add_mutually_exclusive_group(required=True)
tags_group.add_argument('-l', '--list', help="List all tags and their frequency", tags_group.add_argument('-l', '--list', help="List all tags and their frequency",
@@ -137,21 +136,6 @@ def create_parser() -> argparse.ArgumentParser:
print_cmd_modes.add_argument('-a', '--answer', help='Only print the answer', action='store_true') print_cmd_modes.add_argument('-a', '--answer', help='Only print the answer', action='store_true')
print_cmd_modes.add_argument('-S', '--only-source-code', help='Only print embedded source code', action='store_true') print_cmd_modes.add_argument('-S', '--only-source-code', help='Only print embedded source code', action='store_true')
# 'translation' command parser
translation_cmd_parser = cmdparser.add_parser('translation', parents=[ai_parser, tag_parser],
help="ask, create and repeat translations.",
aliases=['t'])
translation_cmd_parser.set_defaults(func=translation_cmd)
translation_group = translation_cmd_parser.add_mutually_exclusive_group(required=True)
translation_group.add_argument('-a', '--ask', nargs='+', help='Ask to translate the given text', metavar='TEXT')
translation_group.add_argument('-c', '--create', nargs='+', help='Create a translation', metavar='TEXT')
translation_group.add_argument('-r', '--repeat', nargs='*', help='Repeat a translation', metavar='MESSAGE')
translation_cmd_parser.add_argument('-S', '--source-lang', help="Source language", metavar="LANGUAGE", required=True)
translation_cmd_parser.add_argument('-T', '--target-lang', help="Target language", metavar="LANGUAGE", required=True)
translation_cmd_parser.add_argument('-G', '--glossaries', nargs='+', help="List of glossaries", metavar="GLOSSARY")
translation_cmd_parser.add_argument('-d', '--input-document', help="Document to translate", metavar="FILE")
translation_cmd_parser.add_argument('-D', '--output-document', help="Path for the translated document", metavar="FILE")
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
return parser return parser
+19 -31
View File
@@ -9,43 +9,31 @@ from chatmastermind.configuration import OpenAIConfig
class OpenAITest(unittest.TestCase): class OpenAITest(unittest.TestCase):
@mock.patch('openai.ChatCompletion.create') @mock.patch('chatmastermind.ais.openai.OpenAI._completions')
def test_request(self, mock_create: mock.MagicMock) -> None: def test_request(self, mock_create: mock.MagicMock) -> None:
# Create a test instance of OpenAI # Create a test instance of OpenAI
config = OpenAIConfig() config = OpenAIConfig()
openai = OpenAI(config) openai = OpenAI(config)
# Set up the mock response from openai.ChatCompletion.create # Set up the mock response from openai.ChatCompletion.create
mock_chunk1 = { class mock_obj:
'choices': [ pass
{ mock_chunk1 = mock_obj()
'index': 0, mock_chunk1.choices = [mock_obj(), mock_obj()] # type: ignore
'delta': { mock_chunk1.choices[0].index = 0 # type: ignore
'content': 'Answer 1' mock_chunk1.choices[0].delta = mock_obj() # type: ignore
}, mock_chunk1.choices[0].delta.content = 'Answer 1' # type: ignore
'finish_reason': None mock_chunk1.choices[0].finish_reason = None # type: ignore
}, mock_chunk1.choices[1].index = 1 # type: ignore
{ mock_chunk1.choices[1].delta = mock_obj() # type: ignore
'index': 1, mock_chunk1.choices[1].delta.content = 'Answer 2' # type: ignore
'delta': { mock_chunk1.choices[1].finish_reason = None # type: ignore
'content': 'Answer 2' mock_chunk2 = mock_obj()
}, mock_chunk2.choices = [mock_obj(), mock_obj()] # type: ignore
'finish_reason': None mock_chunk2.choices[0].index = 0 # type: ignore
} mock_chunk2.choices[0].finish_reason = 'stop' # type: ignore
], mock_chunk2.choices[1].index = 1 # type: ignore
} mock_chunk2.choices[1].finish_reason = 'stop' # type: ignore
mock_chunk2 = {
'choices': [
{
'index': 0,
'finish_reason': 'stop'
},
{
'index': 1,
'finish_reason': 'stop'
}
],
}
mock_create.return_value = iter([mock_chunk1, mock_chunk2]) mock_create.return_value = iter([mock_chunk1, mock_chunk2])
# Create test data # Create test data