implement context into copilot, various minor changes
This commit is contained in:
parent
d811deebc9
commit
c50b369f2f
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
from cryptography.fernet import Fernet
|
||||
|
||||
key = Fernet.generate_key()
|
||||
print(key.decode())
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue