Compare commits
2 Commits
main
..
44f7ebe365
| Author | SHA1 | Date | |
|---|---|---|---|
| 44f7ebe365 | |||
| 7b8ee56230 |
@@ -47,12 +47,12 @@ class OpenAIAnswer:
|
||||
self.finished = True
|
||||
if not self.finished:
|
||||
found_choice = False
|
||||
for choice in chunk.choices:
|
||||
if not choice.finish_reason:
|
||||
self.streams[choice.index].data.append(choice.delta.content)
|
||||
self.tokens.completion += len(self.encoding.encode(choice.delta.content))
|
||||
for choice in chunk['choices']:
|
||||
if not choice['finish_reason']:
|
||||
self.streams[choice['index']].data.append(choice['delta']['content'])
|
||||
self.tokens.completion += len(self.encoding.encode(choice['delta']['content']))
|
||||
self.tokens.total = self.tokens.prompt + self.tokens.completion
|
||||
if choice.index == self.idx:
|
||||
if choice['index'] == self.idx:
|
||||
found_choice = True
|
||||
if not found_choice:
|
||||
return False
|
||||
@@ -68,10 +68,6 @@ class OpenAI(AI):
|
||||
self.ID = config.ID
|
||||
self.name = config.name
|
||||
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,
|
||||
question: Message,
|
||||
@@ -84,9 +80,10 @@ class OpenAI(AI):
|
||||
nr. of messages in the 'AIResponse'.
|
||||
"""
|
||||
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)
|
||||
tokens: Tokens = Tokens(prompt_tokens, 0, prompt_tokens)
|
||||
response = self._completions(
|
||||
response = openai.ChatCompletion.create(
|
||||
model=self.config.model,
|
||||
messages=oai_chat,
|
||||
temperature=self.config.temperature,
|
||||
@@ -117,8 +114,9 @@ class OpenAI(AI):
|
||||
Return all models supported by this AI.
|
||||
"""
|
||||
ret = []
|
||||
for engine in sorted(self.client.models.list().data, key=lambda x: x.id):
|
||||
ret.append(engine.id)
|
||||
for engine in sorted(openai.Engine.list()['data'], key=lambda x: x['id']):
|
||||
if engine['ready']:
|
||||
ret.append(engine['id'])
|
||||
ret.sort()
|
||||
return ret
|
||||
|
||||
@@ -126,8 +124,14 @@ class OpenAI(AI):
|
||||
"""
|
||||
Print all models supported by the current AI.
|
||||
"""
|
||||
for model in self.models():
|
||||
print(model)
|
||||
not_ready = []
|
||||
for engine in sorted(openai.Engine.list()['data'], key=lambda x: x['id']):
|
||||
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,
|
||||
question: Optional[Message] = None) -> tuple[ChatType, int]:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
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
|
||||
@@ -3,9 +3,10 @@ import argparse
|
||||
from pathlib import Path
|
||||
from itertools import zip_longest
|
||||
from copy import deepcopy
|
||||
from .common import invert_input_tag_args, add_file_as_code, add_file_as_text
|
||||
from ..configuration import Config
|
||||
from ..chat import ChatDB, msg_location
|
||||
from ..message import Message, MessageFilter, MessageError, Question, source_code
|
||||
from ..message import Message, MessageFilter, Question
|
||||
from ..ai_factory import create_ai
|
||||
from ..ai import AI, AIResponse
|
||||
|
||||
@@ -14,47 +15,6 @@ class QuestionCmdError(Exception):
|
||||
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:
|
||||
"""
|
||||
Takes an existing message and CLI arguments, and returns modified args based
|
||||
@@ -163,22 +123,6 @@ def repeat_messages(messages: list[Message], chat: ChatDB, args: argparse.Namesp
|
||||
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:
|
||||
"""
|
||||
Handler for the 'question' command.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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
|
||||
+17
-1
@@ -14,6 +14,7 @@ from .commands.tags import tags_cmd
|
||||
from .commands.config import config_cmd
|
||||
from .commands.hist import hist_cmd
|
||||
from .commands.print import print_cmd
|
||||
from .commands.translation import translation_cmd
|
||||
from .chat import msg_location
|
||||
|
||||
|
||||
@@ -102,7 +103,7 @@ def create_parser() -> argparse.ArgumentParser:
|
||||
# 'tags' command parser
|
||||
tags_cmd_parser = cmdparser.add_parser('tags',
|
||||
help="Manage tags.",
|
||||
aliases=['t'])
|
||||
aliases=['T'])
|
||||
tags_cmd_parser.set_defaults(func=tags_cmd)
|
||||
tags_group = tags_cmd_parser.add_mutually_exclusive_group(required=True)
|
||||
tags_group.add_argument('-l', '--list', help="List all tags and their frequency",
|
||||
@@ -136,6 +137,21 @@ 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('-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)
|
||||
return parser
|
||||
|
||||
|
||||
+31
-19
@@ -9,31 +9,43 @@ from chatmastermind.configuration import OpenAIConfig
|
||||
|
||||
class OpenAITest(unittest.TestCase):
|
||||
|
||||
@mock.patch('chatmastermind.ais.openai.OpenAI._completions')
|
||||
@mock.patch('openai.ChatCompletion.create')
|
||||
def test_request(self, mock_create: mock.MagicMock) -> None:
|
||||
# Create a test instance of OpenAI
|
||||
config = OpenAIConfig()
|
||||
openai = OpenAI(config)
|
||||
|
||||
# Set up the mock response from openai.ChatCompletion.create
|
||||
class mock_obj:
|
||||
pass
|
||||
mock_chunk1 = mock_obj()
|
||||
mock_chunk1.choices = [mock_obj(), mock_obj()] # type: ignore
|
||||
mock_chunk1.choices[0].index = 0 # type: ignore
|
||||
mock_chunk1.choices[0].delta = mock_obj() # type: ignore
|
||||
mock_chunk1.choices[0].delta.content = 'Answer 1' # type: ignore
|
||||
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
|
||||
mock_chunk1.choices[1].delta.content = 'Answer 2' # type: ignore
|
||||
mock_chunk1.choices[1].finish_reason = None # type: ignore
|
||||
mock_chunk2 = mock_obj()
|
||||
mock_chunk2.choices = [mock_obj(), mock_obj()] # type: ignore
|
||||
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_chunk1 = {
|
||||
'choices': [
|
||||
{
|
||||
'index': 0,
|
||||
'delta': {
|
||||
'content': 'Answer 1'
|
||||
},
|
||||
'finish_reason': None
|
||||
},
|
||||
{
|
||||
'index': 1,
|
||||
'delta': {
|
||||
'content': 'Answer 2'
|
||||
},
|
||||
'finish_reason': None
|
||||
}
|
||||
],
|
||||
}
|
||||
mock_chunk2 = {
|
||||
'choices': [
|
||||
{
|
||||
'index': 0,
|
||||
'finish_reason': 'stop'
|
||||
},
|
||||
{
|
||||
'index': 1,
|
||||
'finish_reason': 'stop'
|
||||
}
|
||||
],
|
||||
}
|
||||
mock_create.return_value = iter([mock_chunk1, mock_chunk2])
|
||||
|
||||
# Create test data
|
||||
|
||||
Reference in New Issue
Block a user