From c50b369f2ffbbe0944044624900790de96dfc0b7 Mon Sep 17 00:00:00 2001 From: Cyberes Date: Wed, 10 Apr 2024 22:47:15 -0600 Subject: [PATCH] implement context into copilot, various minor changes --- README.md | 7 +- config.sample.yaml | 7 ++ docs/Config.md | 5 +- docs/Copilot.md | 15 +++ matrix_gpt/api_client_manager.py | 26 +++-- matrix_gpt/callbacks.py | 4 +- matrix_gpt/chat_functions.py | 12 +- matrix_gpt/config.py | 4 + matrix_gpt/generate.py | 30 +++-- matrix_gpt/generate_clients/anthropic.py | 21 ++-- matrix_gpt/generate_clients/api_client.py | 15 ++- matrix_gpt/generate_clients/copilot.py | 128 +++++++++++++++------- matrix_gpt/generate_clients/openai.py | 29 ++--- matrix_gpt/handle_actions.py | 20 ++-- matrix_gpt/matrix_helper.py | 4 +- new-fernet-key.py | 4 + requirements.txt | 1 + 17 files changed, 217 insertions(+), 115 deletions(-) create mode 100644 docs/Copilot.md create mode 100644 new-fernet-key.py diff --git a/README.md b/README.md index 686aa50..07fb98d 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,9 @@ multiple different models using different triggers, such as `!c4` for GPT4 and ` and/or Anthropic API keys. 3. Start the bot with `python3 main.py` -[Pantalaimon](https://github.com/matrix-org/pantalaimon) is **required** for the bot to be able to talk in encrypted -rooms. +[Pantalaimon](https://github.com/matrix-org/pantalaimon) is **required** for the bot to be able to talk in encrypted rooms. + +If you are using Copilot, please read the extra documentation: [docs/Copilot.md](docs/Copilot.md) I included a sample Systemd service (`matrixgpt.service`). @@ -55,8 +56,8 @@ The bot can give helpful reactions: ## TODO +- [ ] Add our own context mechanism to Copilot - [ ] Dalle bot -- [ ] Add our own context mechanism to Copilot? - [ ] Improve error messages sent with reactions to narrow down where the issue occurred. - [ ] Allow replying to an image post which will give a vision model an image + text on the first message. - [ ] Fix the typing indicator being removed when two responses are generating. diff --git a/config.sample.yaml b/config.sample.yaml index 98b5021..92ea1ee 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -90,6 +90,13 @@ openai: anthropic: api_key: sk-ant-qwerty12345 +copilot: + api_key: '_C_Auth=; MC1=GUID=....' + + # The key to encrypt metadata attached to events in the room. + # Generated using `new-fernet-key.py` + event_encryption_key: abc123= + # When an error occurs, send additional metadata with the reaction event. send_extra_messages: true diff --git a/docs/Config.md b/docs/Config.md index 9bcd0ab..e3268fa 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -69,6 +69,8 @@ command: ### Bing Copilot +See [Copilot.md](Copilot.md). + ```yaml command: - trigger: '!cp' @@ -76,8 +78,9 @@ command: api_type: copilot copilot: api_key: '_C_Auth=; MC1=GUID=....' + event_encryption_key: abc123= ``` ### Dalle-3 -TODO \ No newline at end of file +TODO diff --git a/docs/Copilot.md b/docs/Copilot.md new file mode 100644 index 0000000..583dbb1 --- /dev/null +++ b/docs/Copilot.md @@ -0,0 +1,15 @@ +# Copilot Setup + +Copilot doesn't have a concept of "context". But the server does keep track of conversations. + +The bot will store conversation metadata in the Matrix room attached to its initial response to a query. This metadata +is encrypted and contains the nessesary information needed to load the conversation and continue talking to the user. + +You need to generate your encryption key first: + +```bash +python3 new-fernet-key.py +``` + +This will print a string. Copy this to your `config.yaml` and enter it in the `event_encryption_key` field in the `copilot` +section. \ No newline at end of file diff --git a/matrix_gpt/api_client_manager.py b/matrix_gpt/api_client_manager.py index c9af7a3..5edbdef 100644 --- a/matrix_gpt/api_client_manager.py +++ b/matrix_gpt/api_client_manager.py @@ -1,5 +1,7 @@ import logging +from nio import MatrixRoom, Event + from matrix_gpt import MatrixClientHelper from matrix_gpt.config import global_config from matrix_gpt.generate_clients.anthropic import AnthropicApiClient @@ -26,37 +28,41 @@ class ApiClientManager: self._anth_api_key = global_config['anthropic'].get('api_key') self._copilot_cookie = global_config['copilot'].get('api_key') - def get_client(self, mode: str, client_helper: MatrixClientHelper): + def get_client(self, mode: str, client_helper: MatrixClientHelper, room: MatrixRoom, event: Event): if mode == 'openai': - return self.openai_client(client_helper) + return self.openai_client(client_helper, room, event) elif mode == 'anthropic': - return self.anth_client(client_helper) + return self.anth_client(client_helper, room, event) elif mode == 'copilot': - return self.copilot_client(client_helper) + return self.copilot_client(client_helper, room, event) else: raise Exception - def openai_client(self, client_helper: MatrixClientHelper): + def openai_client(self, client_helper: MatrixClientHelper, room: MatrixRoom, event: Event): self._set_from_config() if not self._openai_api_key: self.logger.error('Missing an OpenAI API key!') return None return OpenAIClient( api_key=self._openai_api_key, - client_helper=client_helper + client_helper=client_helper, + room=room, + event=event ) - def anth_client(self, client_helper: MatrixClientHelper): + def anth_client(self, client_helper: MatrixClientHelper, room: MatrixRoom, event: Event): self._set_from_config() if not self._anth_api_key: self.logger.error('Missing an Anthropic API key!') return None return AnthropicApiClient( api_key=self._anth_api_key, - client_helper=client_helper + client_helper=client_helper, + room=room, + event=event ) - def copilot_client(self, client_helper): + def copilot_client(self, client_helper, room: MatrixRoom, event: Event): self._set_from_config() if not self._copilot_cookie: self.logger.error('Missing a Copilot API key!') @@ -64,6 +70,8 @@ class ApiClientManager: return CopilotClient( api_key=self._copilot_cookie, client_helper=client_helper, + room=room, + event=event ) diff --git a/matrix_gpt/callbacks.py b/matrix_gpt/callbacks.py index de1f451..8396279 100644 --- a/matrix_gpt/callbacks.py +++ b/matrix_gpt/callbacks.py @@ -40,7 +40,7 @@ class MatrixBotCallbacks: # Need to track messages manually because the sync background thread may trigger the callback. return self.seen_messages.add(requestor_event.event_id) - command_activated, sent_command_prefix, command_info = check_command_prefix(msg) + command_activated, command_info = check_command_prefix(msg) if not command_activated and is_thread(requestor_event): # Threaded messages @@ -54,7 +54,7 @@ class MatrixBotCallbacks: if not check_authorized(requestor_event.sender, allowed_to_chat): await self.client_helper.react_to_event(room.room_id, requestor_event.event_id, '🚫', extra_error='Not allowed to chat.' if global_config['send_extra_messages'] else None) return - task = asyncio.create_task(do_reply_msg(self.client_helper, room, requestor_event, command_info, command_activated)) + task = asyncio.create_task(do_reply_msg(self.client_helper, room, requestor_event, command_info)) async def handle_invite(self, room: MatrixRoom, event: InviteMemberEvent) -> None: """Callback for when an invite is received. Join the room specified in the invite. diff --git a/matrix_gpt/chat_functions.py b/matrix_gpt/chat_functions.py index 9c0eeb0..be02baf 100644 --- a/matrix_gpt/chat_functions.py +++ b/matrix_gpt/chat_functions.py @@ -14,25 +14,25 @@ def is_thread(event: RoomMessageText): return event.source['content'].get('m.relates_to', {}).get('rel_type') == 'm.thread' -def check_command_prefix(string: str) -> Tuple[bool, str | None, CommandInfo | None]: +def check_command_prefix(string: str) -> Tuple[bool, CommandInfo | None]: for k, v in global_config.command_prefixes.items(): if string.startswith(f'{k} '): command_info = CommandInfo(**v) - return True, k, command_info - return False, None, None + return True, command_info + return False, None -async def is_this_our_thread(client: AsyncClient, room: MatrixRoom, event: RoomMessageText) -> Tuple[bool, str | None, CommandInfo | None]: +async def is_this_our_thread(client: AsyncClient, room: MatrixRoom, event: RoomMessageText) -> Tuple[bool, CommandInfo | None]: base_event_id = event.source['content'].get('m.relates_to', {}).get('event_id') if base_event_id: e = await client.room_get_event(room.room_id, base_event_id) if not isinstance(e, RoomGetEventResponse): logger.critical(f'Failed to get event in is_this_our_thread(): {vars(e)}') - return False, None, None + return False, None else: return check_command_prefix(e.event.body) else: - return False, None, None + return False, None async def get_thread_content(client: AsyncClient, room: MatrixRoom, base_event: RoomMessageText) -> List[Event]: diff --git a/matrix_gpt/config.py b/matrix_gpt/config.py index 04bcf77..3f4ed14 100644 --- a/matrix_gpt/config.py +++ b/matrix_gpt/config.py @@ -44,6 +44,7 @@ config_scheme = bison.Scheme( )), bison.DictOption('copilot', scheme=bison.Scheme( bison.Option('api_key', field_type=[str, NoneType], required=False, default=None), + bison.Option('event_encryption_key', field_type=[str, NoneType], required=False, default=None), )), bison.DictOption('logging', scheme=bison.Scheme( bison.Option('log_level', field_type=str, default='info'), @@ -107,6 +108,9 @@ class ConfigManager: raise SchemeValidationError(f'Duplicate trigger {trigger}') existing_triggers.append(trigger) + if self._config.config.get('copilot') and not self._config.config['copilot'].get('event_encryption_key'): + raise SchemeValidationError('You must set `event_encryption_key` when using copilot') + self._command_prefixes = self._generate_command_prefixes() def _merge_in_list_defaults(self): diff --git a/matrix_gpt/generate.py b/matrix_gpt/generate.py index 2eb8591..dd65027 100644 --- a/matrix_gpt/generate.py +++ b/matrix_gpt/generate.py @@ -1,4 +1,5 @@ import asyncio +import json import logging import traceback from typing import Union @@ -21,16 +22,17 @@ async def generate_ai_response( client_helper: MatrixClientHelper, room: MatrixRoom, event: RoomMessageText, - msg: Union[str, list], + context: Union[str, list], command_info: CommandInfo, thread_root_id: str = None, + matrix_gpt_data: str = None ): assert isinstance(command_info, CommandInfo) client = client_helper.client try: await client.room_typing(room.room_id, typing_state=True, timeout=global_config['response_timeout'] * 1000) - api_client = api_client_helper.get_client(command_info.api_type, client_helper) + api_client = api_client_helper.get_client(command_info.api_type, client_helper, room, event) if not api_client: # If this was None then we were missing an API key for this client type. Error has already been logged. await client_helper.react_to_event( @@ -42,7 +44,13 @@ async def generate_ai_response( await client.room_typing(room.room_id, typing_state=False, timeout=1000) return - messages = api_client.assemble_context(msg, system_prompt=command_info.system_prompt, injected_system_prompt=command_info.injected_system_prompt) + # The input context can be either a string if this is the first message in the thread or a list of all messages in the thread. + # Handling this here instead of the caller simplifies things. + if isinstance(context, str): + context = [{'role': api_client.HUMAN_NAME, 'content': context}] + + # Build the context and do the things that need to be done for our specific API type. + api_client.assemble_context(context, system_prompt=command_info.system_prompt, injected_system_prompt=command_info.injected_system_prompt) if api_client.check_ignore_request(): logger.debug(f'Reply to {event.event_id} was ignored by the model "{command_info.model}".') @@ -50,12 +58,13 @@ async def generate_ai_response( return response = None + extra_data = None try: - task = asyncio.create_task(api_client.generate(command_info)) + task = asyncio.create_task(api_client.generate(command_info, matrix_gpt_data)) for task in asyncio.as_completed([task], timeout=global_config['response_timeout']): # TODO: add a while loop and heartbeat the background thread try: - response = await task + response, extra_data = await task break except asyncio.TimeoutError: logger.warning(f'Response to event {event.event_id} timed out.') @@ -92,9 +101,13 @@ async def generate_ai_response( # The AI's response. text_response = response.strip().strip('\n') + if not extra_data: + extra_data = {} + # Logging if global_config['logging']['log_full_response']: - data = {'event_id': event.event_id, 'room': room.room_id, 'messages': messages, 'response': response} + assembled_context = api_client.context + data = {'event_id': event.event_id, 'room': room.room_id, 'messages': assembled_context, 'response': response} # Remove images from the logged data. for i in range(len(data['messages'])): if isinstance(data['messages'][i]['content'], list): @@ -105,7 +118,7 @@ async def generate_ai_response( elif data['messages'][i]['content'][0].get('image_url'): # OpenAI data['messages'][i]['content'][0]['image_url']['url'] = '...' - logger.debug(data) + logger.debug(json.dumps(data)) z = text_response.replace("\n", "\\n") logger.info(f'Reply to {event.event_id} --> {command_info.model} responded with "{z}"') @@ -116,7 +129,8 @@ async def generate_ai_response( reply_to_event_id=event.event_id, thread=True, thread_root_id=thread_root_id if thread_root_id else event.event_id, - markdown_convert=True + markdown_convert=True, + extra_data=extra_data ) await client.room_typing(room.room_id, typing_state=False, timeout=1000) if not isinstance(resp, RoomSendResponse): diff --git a/matrix_gpt/generate_clients/anthropic.py b/matrix_gpt/generate_clients/anthropic.py index b0a666e..b87d959 100644 --- a/matrix_gpt/generate_clients/anthropic.py +++ b/matrix_gpt/generate_clients/anthropic.py @@ -1,5 +1,3 @@ -from typing import Union - from anthropic import AsyncAnthropic from nio import RoomMessageImage @@ -18,18 +16,14 @@ class AnthropicApiClient(ApiClient): api_key=self._api_key ) - def assemble_context(self, messages: Union[str, list], system_prompt: str = None, injected_system_prompt: str = None): - if isinstance(messages, list): - messages = messages - else: - messages = [{"role": self._HUMAN_NAME, "content": [{"type": "text", "text": str(messages)}]}] - self._context = messages - return messages + def assemble_context(self, context: list, system_prompt: str = None, injected_system_prompt: str = None): + assert not len(self._context) + self._context = context + self.verify_context() def verify_context(self): """ - Verify that the context alternates between the human and assistant, inserting the opposite - user type if it does not alternate correctly. + Verify that the context alternates between the human and assistant, inserting the opposite user type if it does not alternate correctly. """ i = 0 while i < len(self._context) - 1: @@ -64,8 +58,7 @@ class AnthropicApiClient(ApiClient): }] }) - async def generate(self, command_info: CommandInfo): - self.verify_context() + async def generate(self, command_info: CommandInfo, matrix_gpt_data: str = None): r = await self._create_client().messages.create( model=command_info.model, max_tokens=None if command_info.max_tokens == 0 else command_info.max_tokens, @@ -73,4 +66,4 @@ class AnthropicApiClient(ApiClient): system='' if not command_info.system_prompt else command_info.system_prompt, messages=self.context ) - return r.content[0].text + return r.content[0].text, None diff --git a/matrix_gpt/generate_clients/api_client.py b/matrix_gpt/generate_clients/api_client.py index 1e45a6a..7dd3bcd 100644 --- a/matrix_gpt/generate_clients/api_client.py +++ b/matrix_gpt/generate_clients/api_client.py @@ -1,6 +1,6 @@ -from typing import Union +from typing import Tuple -from nio import RoomMessageImage +from nio import RoomMessageImage, MatrixRoom, Event from matrix_gpt import MatrixClientHelper from matrix_gpt.generate_clients.command_info import CommandInfo @@ -10,9 +10,11 @@ class ApiClient: _HUMAN_NAME = 'user' _BOT_NAME = 'assistant' - def __init__(self, api_key: str, client_helper: MatrixClientHelper): + def __init__(self, api_key: str, client_helper: MatrixClientHelper, room: MatrixRoom, event: Event): self._api_key = api_key self._client_helper = client_helper + self._room = room + self._event = event self._context = [] def _create_client(self, base_url: str = None): @@ -21,7 +23,8 @@ class ApiClient: def check_ignore_request(self): return False - def assemble_context(self, messages: Union[str, list], system_prompt: str = None, injected_system_prompt: str = None): + def assemble_context(self, context: list, system_prompt: str = None, injected_system_prompt: str = None): + assert not len(self._context) raise NotImplementedError def generate_text_msg(self, content: str, role: str): @@ -33,12 +36,12 @@ class ApiClient: async def append_img(self, img_event: RoomMessageImage, role: str): raise NotImplementedError - async def generate(self, command_info: CommandInfo): + async def generate(self, command_info: CommandInfo, matrix_gpt_data: str = None) -> Tuple[str, dict | None]: raise NotImplementedError @property def context(self): - return self._context + return self._context.copy() @property def HUMAN_NAME(self): diff --git a/matrix_gpt/generate_clients/copilot.py b/matrix_gpt/generate_clients/copilot.py index e166756..920be76 100644 --- a/matrix_gpt/generate_clients/copilot.py +++ b/matrix_gpt/generate_clients/copilot.py @@ -1,10 +1,14 @@ +import json import re -from typing import Union +import time from urllib.parse import urlparse +from cryptography.fernet import Fernet from nio import RoomMessageImage from sydney import SydneyClient +from sydney.exceptions import ThrottledRequestException +from matrix_gpt.config import global_config from matrix_gpt.generate_clients.api_client import ApiClient from matrix_gpt.generate_clients.command_info import CommandInfo @@ -16,13 +20,15 @@ _REGEX_ATTR_RE_STR = r'^\[(\d*)]:\s(https?://(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,2 _REGEX_ATTR_RE = re.compile(_REGEX_ATTR_RE_STR) _REGEX_ATTR_LINK_RE_STR = [r'\[\^\d*\^]\[', r']'] _REGEX_ATTR_LINK_RE = re.compile(r'\d*'.join(_REGEX_ATTR_LINK_RE_STR)) +_COPILOT_WARNING_STR = "\n\n*Conversations with Copilot are not private.*" -""" -To implement context, could we maybe pickle the `sydney` object and track state via requester event ID? -Probably best not to store it in memory, but maybe a sqlite database in /tmp? -But might have to store it in memory because of async issues and not being able to restore the state of an old async loop. -""" +def encrypt_string(string: str) -> str: + return Fernet(global_config['copilot']['event_encryption_key']).encrypt(string.encode()).decode('utf-8') + + +def decrypt_string(token: str) -> bytes: + return Fernet(global_config['copilot']['event_encryption_key']).decrypt(token.encode()) class CopilotClient(ApiClient): @@ -39,22 +45,43 @@ class CopilotClient(ApiClient): async def append_img(self, img_event: RoomMessageImage, role: str): raise NotImplementedError - def check_ignore_request(self): - if len(self._context) > 1: - return True - return False + # def check_ignore_request(self): + # if len(self._context) > 1: + # return True + # return False - def assemble_context(self, messages: Union[str, list], system_prompt: str = None, injected_system_prompt: str = None): - if isinstance(messages, list): - messages = messages - else: - messages = [{'role': self._HUMAN_NAME, 'content': messages}] - self._context = messages - return messages + def assemble_context(self, context: list, system_prompt: str = None, injected_system_prompt: str = None): + assert not len(self._context) + self._context = context + for i in range(len(self._context)): + if _COPILOT_WARNING_STR in self._context[i]['content']: + self._context[i]['content'] = self._context[i]['content'].replace(_COPILOT_WARNING_STR, '', 1) + + async def generate(self, command_info: CommandInfo, matrix_gpt_data: str = None): + # TODO: config option for style + async with SydneyClient(bing_cookies=self._api_key, style='precise') as sydney: + # Ignore any exceptions doing this since they will be caught by the caller. + if matrix_gpt_data: + decrypted_metadata = decrypt_string(matrix_gpt_data) + conversation_metadata = json.loads(decrypted_metadata) + sydney.conversation_signature = conversation_metadata["conversation_signature"] + sydney.encrypted_conversation_signature = conversation_metadata["encrypted_conversation_signature"] + sydney.conversation_id = conversation_metadata["conversation_id"] + sydney.client_id = conversation_metadata["client_id"] + sydney.invocation_id = conversation_metadata["invocation_id"] + + response = None + for i in range(3): + try: + response = dict(await sydney.ask(self._context[-1]['content'], citations=True, raw=True)) + break + except ThrottledRequestException: + time.sleep(10) + if not response: + # If this happens you should first try to change your cookies. + # Otherwise, you've used all your credits for today. + raise ThrottledRequestException - async def generate(self, command_info: CommandInfo): - async with SydneyClient(bing_cookies=self._api_key) as sydney: - response = dict(await sydney.ask(self._context[0]['content'], citations=True, raw=True)) bot_response = response['item']['messages'][-1] text_card = {} @@ -72,25 +99,48 @@ class CopilotClient(ApiClient): i = int(m.group(1)) attributions_strs.insert(i, m.group(2)) - if len(attributions_strs): - # Remove the original attributions from the text. - response_text = response_text.split("\n", len(attributions_strs) + 1)[len(attributions_strs) + 1] + if len(attributions_strs): + # Remove the original attributions from the text. + response_text = response_text.split("\n", len(attributions_strs) + 1)[len(attributions_strs) + 1] - # Add a list of attributions at the bottom of the response. - response_text += '\n\nCitations:' - for i in range(len(attributions_strs)): - url = attributions_strs[i] - domain = urlparse(url).netloc - response_text += f'\n\n{i + 1}. [{domain}]({url})' + # Add a list of attributions at the bottom of the response. + response_text += '\n\nCitations:' + for i in range(len(attributions_strs)): + url = attributions_strs[i] + domain = urlparse(url).netloc + response_text += f'\n\n{i + 1}. [{domain}]({url})' - # Add links to the inline attributions. - for match in re.findall(_REGEX_ATTR_LINK_RE, response_text): - match_clean = re.sub(r'\[\^\d*\^]', '', match) - i = int(re.match(r'\[(\d*)]', match_clean).group(1)) - assert i - 1 >= 0 - new_str = f'[[{i}]]({attributions_strs[i - 1]})' - n = response_text.replace(match, new_str) - response_text = n + # Add links to the inline attributions. + for match in re.findall(_REGEX_ATTR_LINK_RE, response_text): + match_clean = re.sub(r'\[\^\d*\^]', '', match) + i = int(re.match(r'\[(\d*)]', match_clean).group(1)) + try: + assert i - 1 >= 0 + new_str = f'[[{i}]]({attributions_strs[i - 1]})' + except: + raise Exception(f'Failed to parse attribution_str array.\n{attributions_strs}\n{i} {i - 1}\n{match_clean}{response_text}') + n = response_text.replace(match, new_str) + response_text = n - response_text += "\n\n*Copilot lacks a context mechanism so the bot cannot respond past the first message. Conversations with Copilot are not private.*" - return response_text + event_data = json.dumps( + { + "conversation_signature": sydney.conversation_signature, + "encrypted_conversation_signature": sydney.encrypted_conversation_signature, + "conversation_id": sydney.conversation_id, + "client_id": sydney.client_id, + "invocation_id": sydney.invocation_id, + "number_of_messages": sydney.number_of_messages, + "max_messages": sydney.max_messages, + } + ) + + if len(self._context) == 1: + response_text += _COPILOT_WARNING_STR + + # Store the conversation metadata in the response. It's encrypted for privacy purposes. + custom_data = { + 'thread_root_event': self._event.event_id, + 'data': encrypt_string(event_data) + } + + return response_text, custom_data diff --git a/matrix_gpt/generate_clients/openai.py b/matrix_gpt/generate_clients/openai.py index 2bb5936..7c72b6a 100644 --- a/matrix_gpt/generate_clients/openai.py +++ b/matrix_gpt/generate_clients/openai.py @@ -1,5 +1,3 @@ -from typing import Union - from nio import RoomMessageImage from openai import AsyncOpenAI @@ -43,29 +41,24 @@ class OpenAIClient(ApiClient): }] }) - def assemble_context(self, messages: Union[str, list], system_prompt: str = None, injected_system_prompt: str = None): - if isinstance(messages, list): - messages = messages - else: - messages = [{'role': self._HUMAN_NAME, 'content': messages}] - + def assemble_context(self, context: list, system_prompt: str = None, injected_system_prompt: str = None): + assert not len(self._context) + self._context = context if isinstance(system_prompt, str) and len(system_prompt): - messages.insert(0, {"role": "system", "content": system_prompt}) - if (isinstance(injected_system_prompt, str) and len(injected_system_prompt)) and len(messages) >= 3: + self._context.insert(0, {"role": "system", "content": system_prompt}) + if (isinstance(injected_system_prompt, str) and len(injected_system_prompt)) and len(self._context) >= 3: # Only inject the system prompt if this isn't the first reply. - if messages[-1]['role'] == 'system': + if self._context[-1]['role'] == 'system': # Delete the last system message since we want to replace it with our inject prompt. - del messages[-1] - messages.insert(-1, {"role": "system", "content": injected_system_prompt}) - self._context = messages - return messages + del self._context[-1] + self._context.insert(-1, {"role": "system", "content": injected_system_prompt}) - async def generate(self, command_info: CommandInfo): + async def generate(self, command_info: CommandInfo, matrix_gpt_data: str = None): r = await self._create_client(command_info.api_base).chat.completions.create( model=command_info.model, messages=self._context, temperature=command_info.temperature, timeout=global_config['response_timeout'], - max_tokens=None if command_info.max_tokens == 0 else command_info.max_tokens + max_tokens=None if command_info.max_tokens == 0 else command_info.max_tokens, ) - return r.choices[0].message.content + return r.choices[0].message.content, None diff --git a/matrix_gpt/handle_actions.py b/matrix_gpt/handle_actions.py index 3411b86..bdd370b 100644 --- a/matrix_gpt/handle_actions.py +++ b/matrix_gpt/handle_actions.py @@ -14,15 +14,15 @@ from matrix_gpt.generate_clients.command_info import CommandInfo logger = logging.getLogger('MatrixGPT').getChild('HandleActions') -async def do_reply_msg(client_helper: MatrixClientHelper, room: MatrixRoom, requestor_event: RoomMessageText, command_info: CommandInfo, command_activated: bool): +async def do_reply_msg(client_helper: MatrixClientHelper, room: MatrixRoom, requestor_event: RoomMessageText, command_info: CommandInfo): try: raw_msg = requestor_event.body.strip().strip('\n') - msg = raw_msg if not command_activated else raw_msg[len(command_info.trigger):].strip() # Remove the command prefix + msg = raw_msg[len(command_info.trigger):].strip() # Remove the command prefix await generate_ai_response( client_helper=client_helper, room=room, event=requestor_event, - msg=msg, + context=msg, command_info=command_info, ) except Exception: @@ -34,7 +34,7 @@ async def do_reply_msg(client_helper: MatrixClientHelper, room: MatrixRoom, requ async def do_reply_threaded_msg(client_helper: MatrixClientHelper, room: MatrixRoom, requestor_event: RoomMessageText): client = client_helper.client - is_our_thread, sent_command_prefix, command_info = await is_this_our_thread(client, room, requestor_event) + is_our_thread, command_info = await is_this_our_thread(client, room, requestor_event) if not is_our_thread: # or room.member_count == 2 return @@ -50,7 +50,8 @@ async def do_reply_threaded_msg(client_helper: MatrixClientHelper, room: MatrixR await client.room_typing(room.room_id, typing_state=True, timeout=30000) thread_content = await get_thread_content(client, room, requestor_event) - api_client = api_client_helper.get_client(command_info.api_type, client_helper) + api_client = api_client_helper.get_client(command_info.api_type, client_helper, room, requestor_event) + matrix_gpt_data = {} for event in thread_content: if isinstance(event, MegolmEvent): await client_helper.send_text_to_room( @@ -69,8 +70,10 @@ async def do_reply_threaded_msg(client_helper: MatrixClientHelper, room: MatrixR thread_msg = event.body.strip().strip('\n') api_client.append_msg( role=role, - content=thread_msg if not check_command_prefix(thread_msg)[0] else thread_msg[len(sent_command_prefix):].strip(), + content=thread_msg if not check_command_prefix(thread_msg)[0] else thread_msg[len(command_info.trigger):].strip(), ) + if event.source.get('content', {}).get('m.matrixgpt', {}).get('data'): + matrix_gpt_data = event.source['content']['m.matrixgpt']['data'] elif command_info.vision: await api_client.append_img(event, role) @@ -78,9 +81,10 @@ async def do_reply_threaded_msg(client_helper: MatrixClientHelper, room: MatrixR client_helper=client_helper, room=room, event=requestor_event, - msg=api_client.context, + context=api_client.context, command_info=command_info, - thread_root_id=thread_content[0].event_id + thread_root_id=thread_content[0].event_id, + matrix_gpt_data=matrix_gpt_data ) except: logger.error(traceback.format_exc()) diff --git a/matrix_gpt/matrix_helper.py b/matrix_gpt/matrix_helper.py index df0795f..b92e8e8 100644 --- a/matrix_gpt/matrix_helper.py +++ b/matrix_gpt/matrix_helper.py @@ -120,7 +120,7 @@ class MatrixClientHelper: async def send_text_to_room(self, room_id: str, message: str, notice: bool = False, markdown_convert: bool = False, reply_to_event_id: Optional[str] = None, thread: bool = False, thread_root_id: Optional[str] = None, extra_error: Optional[str] = None, - extra_msg: Optional[str] = None) -> Union[RoomSendResponse, ErrorResponse]: + extra_msg: Optional[str] = None, extra_data: Optional[dict] = None) -> Union[RoomSendResponse, ErrorResponse]: """Send text to a matrix room. Args: @@ -168,6 +168,8 @@ class MatrixClientHelper: "error": str(extra_error), "msg": str(extra_msg), } + if extra_data: + content["m.matrixgpt"].update(extra_data) try: return await self.client.room_send(room_id, "m.room.message", content, ignore_unverified_devices=True) except SendRetryError: diff --git a/new-fernet-key.py b/new-fernet-key.py new file mode 100644 index 0000000..c26ae9f --- /dev/null +++ b/new-fernet-key.py @@ -0,0 +1,4 @@ +from cryptography.fernet import Fernet + +key = Fernet.generate_key() +print(key.decode()) diff --git a/requirements.txt b/requirements.txt index c1a0d5f..7bd6c03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ openai==1.16.2 anthropic==0.23.1 pillow==10.3.0 sydney.py +cryptography==42.0.5 git+https://git.evulid.cc/cyberes/bison.git