4 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
Oleksandr Kozachuk 75a123eb72 Fix usage of the dynamic answer is some cases. 2023-10-24 12:59:13 +02:00
juk0de 7c1c67f8ff Merge pull request 'Dynamic Answer class and OpenAI streaming API' (#19) from dynamic_answer into main
Introduces several changes with the main objective of enabling OpenAI's streaming API in the chatmastermind application. This allows for the retrieval of AI responses gradually as a stream, which can significantly improve the user experience in interactions that involve large result sets.

* Added tiktoken import in 'openai.py' and modifications to the OpenAI class to support streaming. This includes the addition of a new class OpenAIAnswer to handle streaming API responses.
* Modified request function in the OpenAI class: the stream=True flag is added to the openai.ChatCompletion.create method to enable streaming API.
* Modified 'question.py' to print the answer parts as they are streamed.
* Replaced the Answer class's string data type with a generator which supports str and Generator[str, None, None] data types. Modifications are made to the Answer class methods to handle both data types accordingly.
* Updated the tests in 'test_ais_openai.py' and 'test_message.py' to reflect and validate these changes.
2023-10-21 15:50:45 +02:00
2 changed files with 34 additions and 50 deletions
+15 -19
View File
@@ -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,6 +68,10 @@ 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,
@@ -80,10 +84,9 @@ 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 = openai.ChatCompletion.create(
response = self._completions(
model=self.config.model,
messages=oai_chat,
temperature=self.config.temperature,
@@ -114,9 +117,8 @@ class OpenAI(AI):
Return all models supported by this AI.
"""
ret = []
for engine in sorted(openai.Engine.list()['data'], key=lambda x: x['id']):
if engine['ready']:
ret.append(engine['id'])
for engine in sorted(self.client.models.list().data, key=lambda x: x.id):
ret.append(engine.id)
ret.sort()
return ret
@@ -124,14 +126,8 @@ class OpenAI(AI):
"""
Print all models supported by the current AI.
"""
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))
for model in self.models():
print(model)
def openai_chat(self, chat: Chat, system: str,
question: Optional[Message] = None) -> tuple[ChatType, int]:
@@ -150,7 +146,7 @@ class OpenAI(AI):
for message in chat.messages:
if message.answer:
prompt_tokens += append('user', message.question)
prompt_tokens += append('assistant', message.answer)
prompt_tokens += append('assistant', str(message.answer))
if question:
prompt_tokens += append('user', question.question)
return oai_chat, prompt_tokens
+19 -31
View File
@@ -9,43 +9,31 @@ from chatmastermind.configuration import OpenAIConfig
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:
# Create a test instance of OpenAI
config = OpenAIConfig()
openai = OpenAI(config)
# Set up the mock response from openai.ChatCompletion.create
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'
}
],
}
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_create.return_value = iter([mock_chunk1, mock_chunk2])
# Create test data