implement context into copilot, various minor changes

This commit is contained in:
Cyberes 2024-04-10 22:47:15 -06:00
parent d811deebc9
commit c50b369f2f
17 changed files with 217 additions and 115 deletions

View File

@ -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.

View File

@ -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

View File

@ -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
TODO

15
docs/Copilot.md Normal file
View File

@ -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.

View File

@ -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
)

View File

@ -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.

View File

@ -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]:

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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:

4
new-fernet-key.py Normal file
View File

@ -0,0 +1,4 @@
from cryptography.fernet import Fernet
key = Fernet.generate_key()
print(key.decode())

View File

@ -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