diff --git a/main.py b/main.py index a824a25..4f98e4a 100755 --- a/main.py +++ b/main.py @@ -63,6 +63,8 @@ check_config_value_exists(config_data['openai'], 'api_key') command_prefixes = {} for k, v in config_data['command'].items(): + check_config_value_exists(v, 'model') + check_config_value_exists(v, 'mode', default='default') if 'allowed_to_chat' not in v.keys(): # Set default value v['allowed_to_chat'] = 'all' diff --git a/matrix_gpt/bot/chat_functions.py b/matrix_gpt/bot/chat_functions.py index 5415716..70e74b4 100644 --- a/matrix_gpt/bot/chat_functions.py +++ b/matrix_gpt/bot/chat_functions.py @@ -161,87 +161,94 @@ async def process_chat( log_full_response: bool = False, injected_system_prompt: str = False ): - if not store.check_seen_event(event.event_id): - await client.room_typing(room.room_id, typing_state=True, timeout=90000) - # if self.reply_in_thread: - # thread_content = await get_thread_content(self.client, self.room, self.event) + try: + if not store.check_seen_event(event.event_id): + await client.room_typing(room.room_id, typing_state=True, timeout=90000) + # if self.reply_in_thread: + # thread_content = await get_thread_content(self.client, self.room, self.event) - if isinstance(command, list): - messages = command - else: - messages = [{'role': 'user', 'content': command}] + if isinstance(command, list): + messages = command + else: + messages = [{'role': 'user', 'content': command}] - if system_prompt: - messages.insert(0, {"role": "system", "content": system_prompt}) - if injected_system_prompt: - if messages[-1]['role'] == 'system': - del messages[-1] - index = -9999 - if len(messages) >= 3: # only inject the system prompt if this isn't the first reply - index = -1 - elif not system_prompt: - index = 0 - if index != -9999: - messages.insert(index, {"role": "system", "content": injected_system_prompt}) + if system_prompt: + messages.insert(0, {"role": "system", "content": system_prompt}) + if injected_system_prompt: + if messages[-1]['role'] == 'system': + del messages[-1] + index = -9999 + if len(messages) >= 3: # only inject the system prompt if this isn't the first reply + index = -1 + elif not system_prompt: + index = 0 + if index != -9999: + messages.insert(index, {"role": "system", "content": injected_system_prompt}) - logger.debug(f'Generating reply to event {event.event_id}') + logger.debug(f'Generating reply to event {event.event_id}') - loop = asyncio.get_running_loop() + loop = asyncio.get_running_loop() - # I don't think the OpenAI py api has a built-in timeout - @stopit.threading_timeoutable(default=(None, None)) - async def generate(): - if openai_model in ['gpt-3', 'gpt-4']: - return await loop.run_in_executor(None, functools.partial(openai_obj.ChatCompletion.create, model=openai_model, messages=messages, temperature=openai_temperature, timeout=20)) - elif openai_model in ['text-davinci-003', 'davinci-instruct-beta', 'text-davinci-001', 'text-davinci-002', 'text-curie-001', 'text-babbage-001']: - return await loop.run_in_executor(None, functools.partial(openai_obj.Completion.create, model=openai_model, temperature=openai_temperature, timeout=20)) + # I don't think the OpenAI py api has a built-in timeout + @stopit.threading_timeoutable(default=(None, None)) + async def generate(): + if openai_model.startswith('gpt-3') or openai_model.startswith('gpt-4'): + r = await loop.run_in_executor(None, functools.partial(openai_obj.ChatCompletion.create, model=openai_model, messages=messages, temperature=openai_temperature, timeout=20)) + return r.choices[0].message.content + elif openai_model in ['text-davinci-003', 'davinci-instruct-beta', 'text-davinci-001', 'text-davinci-002', 'text-curie-001', 'text-babbage-001']: + r = await loop.run_in_executor(None, functools.partial(openai_obj.Completion.create, model=openai_model, temperature=openai_temperature, timeout=20, max_tokens=4096)) + return r.choices[0].text + else: + raise Exception(f'Model {openai_model} not found!') - response = None - for i in range(openai_retries): - try: - task = asyncio.create_task(generate(timeout=20)) - asyncio.as_completed(task) - response = await task - if response is not None: - break - except Exception as e: # (stopit.utils.TimeoutException, openai.error.APIConnectionError) - logger.warning(f'Got exception when generating response to event {event.event_id}, retrying: {e}') - await client.room_typing(room.room_id, typing_state=True, timeout=15000) - time.sleep(2) - continue + response = None + for i in range(openai_retries): + try: + task = asyncio.create_task(generate(timeout=20)) + asyncio.as_completed(task) + response = await task + if response is not None: + break + else: + logger.warning(f'Response to event {event.event_id} was null, retrying.') + time.sleep(2) + except Exception as e: # (stopit.utils.TimeoutException, openai.error.APIConnectionError) + logger.warning(f'Got exception when generating response to event {event.event_id}, retrying: {e}') + await client.room_typing(room.room_id, typing_state=True, timeout=15000) + time.sleep(2) + continue - if response is None: - logger.critical(f'Could not generate response to event {event.event_id} in room {room.room_id}.') - await client.room_typing(room.room_id, typing_state=False, timeout=15000) - await react_to_event(client, room.room_id, event.event_id, '❌') - return - if openai_model in ['gpt-3', 'gpt-4']: - text_response = response["choices"][0]["message"]["content"].strip().strip('\n') - elif openai_model in ['text-davinci-003', 'davinci-instruct-beta', 'text-davinci-001', 'text-davinci-002', 'text-curie-001', 'text-babbage-001']: - text_response = response["choices"][0]["text"].strip().strip('\n') + if response is None: + logger.critical(f'Response to event {event.event_id} in room {room.room_id} was null.') + await client.room_typing(room.room_id, typing_state=False, timeout=15000) + await react_to_event(client, room.room_id, event.event_id, '❌') + return + text_response = response.strip().strip('\n') - # Logging stuff - if log_full_response: - logger.debug({'event_id': event.event_id, 'room': room.room_id, 'messages': messages, 'response': response}) - z = text_response.replace("\n", "\\n") - if isinstance(command, str): - x = command.replace("\n", "\\n") - elif isinstance(command, list): - x = command[-1]['content'].replace("\n", "\\n") - else: - x = command - logger.info(f'Reply to {event.event_id} --> "{x}" and bot ({openai_model}) responded with "{z}"') + # Logging stuff + if log_full_response: + logger.debug({'event_id': event.event_id, 'room': room.room_id, 'messages': messages, 'response': response}) + z = text_response.replace("\n", "\\n") + if isinstance(command, str): + x = command.replace("\n", "\\n") + elif isinstance(command, list): + x = command[-1]['content'].replace("\n", "\\n") + else: + x = command + logger.info(f'Reply to {event.event_id} --> "{x}" and bot ({openai_model}) responded with "{z}"') - resp = await send_text_to_room(client, room.room_id, text_response, reply_to_event_id=event.event_id, thread=True, thread_root_id=thread_root_id if thread_root_id else event.event_id) - await client.room_typing(room.room_id, typing_state=False, timeout=3000) - - store.add_event_id(event.event_id) - if not isinstance(resp, RoomSendResponse): - logger.critical(f'Failed to respond to event {event.event_id} in room {room.room_id}:\n{vars(resp)}') - await react_to_event(client, room.room_id, event.event_id, '❌') - else: - store.add_event_id(resp.event_id) + resp = await send_text_to_room(client, room.room_id, text_response, reply_to_event_id=event.event_id, thread=True, thread_root_id=thread_root_id if thread_root_id else event.event_id) + await client.room_typing(room.room_id, typing_state=False, timeout=3000) + store.add_event_id(event.event_id) + if not isinstance(resp, RoomSendResponse): + logger.critical(f'Failed to respond to event {event.event_id} in room {room.room_id}:\n{vars(resp)}') + await react_to_event(client, room.room_id, event.event_id, '❌') + else: + store.add_event_id(resp.event_id) + except Exception: + await react_to_event(client, room.room_id, event.event_id, '❌') + raise def check_authorized(string, to_check): def check_str(s, c): diff --git a/matrix_gpt/config.py b/matrix_gpt/config.py index 447c1fd..857b8ca 100644 --- a/matrix_gpt/config.py +++ b/matrix_gpt/config.py @@ -1,14 +1,20 @@ import sys -def check_config_value_exists(config_part, key, check_type=None, allow_empty=False) -> bool: - if key not in config_part.keys(): - print(f'Config key not found: "{key}"') - sys.exit(1) - if not allow_empty and config_part[key] is None or config_part[key] == '': - print(f'Config key "{key}" must not be empty.') - sys.exit(1) - if check_type and not isinstance(config_part[key], check_type): - print(f'Config key "{key}" must be type "{check_type}", not "{type(config_part[key])}".') - sys.exit(1) - return True +def check_config_value_exists(config_part, key, check_type=None, allow_empty=False, choices: list = None, default=None) -> bool: + if default and key not in config_part.keys(): + return default + else: + if key not in config_part.keys(): + print(f'Config key not found: "{key}"') + sys.exit(1) + if not allow_empty and config_part[key] is None or config_part[key] == '': + print(f'Config key "{key}" must not be empty.') + sys.exit(1) + if check_type and not isinstance(config_part[key], check_type): + print(f'Config key "{key}" must be type "{check_type}", not "{type(config_part[key])}".') + sys.exit(1) + if choices and config_part[key] not in choices: + print(f'Invalid choice for config key "{key}". Choices: {choices}') + sys.exit(1) + return True diff --git a/matrix_gpt/matrix.py b/matrix_gpt/matrix.py index 4902cf2..e67b4a5 100644 --- a/matrix_gpt/matrix.py +++ b/matrix_gpt/matrix.py @@ -1,4 +1,5 @@ import json +import logging import os from pathlib import Path from typing import Union @@ -6,6 +7,7 @@ from typing import Union from nio import AsyncClient, AsyncClientConfig, LoginError from nio import LoginResponse +logger = logging.getLogger('MatrixGPT') class MatrixNioGPTHelper: """ @@ -34,6 +36,7 @@ class MatrixNioGPTHelper: try: # If there are no previously-saved credentials, we'll use the password if not os.path.exists(self.auth_file): + logger.info('Using username/password.') resp = await self.client.login(self.passwd, device_name=self.device_name) # check that we logged in succesfully @@ -44,6 +47,7 @@ class MatrixNioGPTHelper: return False, resp else: # Otherwise the config file exists, so we'll use the stored credentials + logger.info('Using cached credentials.') with open(self.auth_file, "r") as f: config = json.load(f) client = AsyncClient(config["homeserver"])