Fix hanging test and establish comprehensive development environment
- Fix infinite retry loop in ai_responder.py that caused test_fix1 to hang - Add missing picture_edit parameter to all AIResponse constructor calls - Set up complete development toolchain with Black, isort, Bandit, and MyPy - Create comprehensive Makefile for development workflows - Add pre-commit hooks with formatting, linting, security, and type checking - Update test mocking to provide contextual responses for different scenarios - Configure all tools for 140 character line length and strict type checking - Add DEVELOPMENT.md with setup instructions and workflow documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
from .discord_bot import FjerkroaBot, main
|
||||
from .ai_responder import AIMessage, AIResponse, AIResponder
|
||||
from .ai_responder import AIMessage, AIResponder, AIResponse
|
||||
from .bot_logging import setup_logging
|
||||
from .discord_bot import FjerkroaBot, main
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
|
||||
from .discord_bot import main
|
||||
|
||||
sys.exit(main())
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import os
|
||||
import json
|
||||
import random
|
||||
import multiline
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import os
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
from io import BytesIO
|
||||
from pprint import pformat
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from functools import lru_cache, wraps
|
||||
from typing import Optional, List, Dict, Any, Tuple, Union
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import multiline
|
||||
|
||||
|
||||
def pp(*args, **kw):
|
||||
if 'width' not in kw:
|
||||
kw['width'] = 300
|
||||
if "width" not in kw:
|
||||
kw["width"] = 300
|
||||
return pformat(*args, **kw)
|
||||
|
||||
|
||||
@@ -45,7 +46,7 @@ def exponential_backoff(base=2, max_delay=60, factor=1, jitter=0.1, max_attempts
|
||||
"""
|
||||
attempt = 0
|
||||
while True:
|
||||
sleep = min(max_delay, factor * base ** attempt)
|
||||
sleep = min(max_delay, factor * base**attempt)
|
||||
jitter_amount = jitter * sleep
|
||||
sleep += random.uniform(-jitter_amount, jitter_amount)
|
||||
yield sleep
|
||||
@@ -59,7 +60,7 @@ def async_cache_to_file(filename):
|
||||
cache = None
|
||||
if cache_file.exists():
|
||||
try:
|
||||
with cache_file.open('rb') as fd:
|
||||
with cache_file.open("rb") as fd:
|
||||
cache = pickle.load(fd)
|
||||
except Exception:
|
||||
cache = {}
|
||||
@@ -74,10 +75,12 @@ def async_cache_to_file(filename):
|
||||
return cache[key]
|
||||
result = await func(*args, **kwargs)
|
||||
cache[key] = result
|
||||
with cache_file.open('wb') as fd:
|
||||
with cache_file.open("wb") as fd:
|
||||
pickle.dump(cache, fd)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@@ -85,24 +88,24 @@ def parse_maybe_json(json_string):
|
||||
if json_string is None:
|
||||
return None
|
||||
if isinstance(json_string, (list, dict)):
|
||||
return ' '.join(map(str, (json_string.values() if isinstance(json_string, dict) else json_string)))
|
||||
return " ".join(map(str, (json_string.values() if isinstance(json_string, dict) else json_string)))
|
||||
json_string = str(json_string).strip()
|
||||
try:
|
||||
parsed_json = parse_json(json_string)
|
||||
except Exception:
|
||||
for b, e in [('{', '}'), ('[', ']')]:
|
||||
for b, e in [("{", "}"), ("[", "]")]:
|
||||
if json_string.startswith(b) and json_string.endswith(e):
|
||||
return parse_maybe_json(json_string[1:-1])
|
||||
return json_string
|
||||
if isinstance(parsed_json, str):
|
||||
return parsed_json
|
||||
if isinstance(parsed_json, (list, dict)):
|
||||
return '\n'.join(map(str, (parsed_json.values() if isinstance(parsed_json, dict) else parsed_json)))
|
||||
return "\n".join(map(str, (parsed_json.values() if isinstance(parsed_json, dict) else parsed_json)))
|
||||
return str(parsed_json)
|
||||
|
||||
|
||||
def same_channel(item1: Dict[str, Any], item2: Dict[str, Any]) -> bool:
|
||||
return parse_json(item1['content']).get('channel') == parse_json(item2['content']).get('channel')
|
||||
return parse_json(item1["content"]).get("channel") == parse_json(item2["content"]).get("channel")
|
||||
|
||||
|
||||
class AIMessageBase(object):
|
||||
@@ -121,64 +124,66 @@ class AIMessage(AIMessageBase):
|
||||
self.channel = channel
|
||||
self.direct = direct
|
||||
self.historise_question = historise_question
|
||||
self.vars = ['user', 'message', 'channel', 'direct']
|
||||
self.vars = ["user", "message", "channel", "direct", "historise_question"]
|
||||
|
||||
|
||||
class AIResponse(AIMessageBase):
|
||||
def __init__(self,
|
||||
answer: Optional[str],
|
||||
answer_needed: bool,
|
||||
channel: Optional[str],
|
||||
staff: Optional[str],
|
||||
picture: Optional[str],
|
||||
hack: bool
|
||||
) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
answer: Optional[str],
|
||||
answer_needed: bool,
|
||||
channel: Optional[str],
|
||||
staff: Optional[str],
|
||||
picture: Optional[str],
|
||||
picture_edit: bool,
|
||||
hack: bool,
|
||||
) -> None:
|
||||
self.answer = answer
|
||||
self.answer_needed = answer_needed
|
||||
self.channel = channel
|
||||
self.staff = staff
|
||||
self.picture = picture
|
||||
self.picture_edit = picture_edit
|
||||
self.hack = hack
|
||||
self.vars = ['answer', 'answer_needed', 'channel', 'staff', 'picture', 'hack']
|
||||
self.vars = ["answer", "answer_needed", "channel", "staff", "picture", "hack"]
|
||||
|
||||
|
||||
class AIResponderBase(object):
|
||||
def __init__(self, config: Dict[str, Any], channel: Optional[str] = None) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.channel = channel if channel is not None else 'system'
|
||||
self.channel = channel if channel is not None else "system"
|
||||
|
||||
|
||||
class AIResponder(AIResponderBase):
|
||||
def __init__(self, config: Dict[str, Any], channel: Optional[str] = None) -> None:
|
||||
super().__init__(config, channel)
|
||||
self.history: List[Dict[str, Any]] = []
|
||||
self.memory: str = 'I am an assistant.'
|
||||
self.memory: str = "I am an assistant."
|
||||
self.rate_limit_backoff = exponential_backoff()
|
||||
self.history_file: Optional[Path] = None
|
||||
self.memory_file: Optional[Path] = None
|
||||
if 'history-directory' in self.config:
|
||||
self.history_file = Path(self.config['history-directory']).expanduser() / f'{self.channel}.dat'
|
||||
if "history-directory" in self.config:
|
||||
self.history_file = Path(self.config["history-directory"]).expanduser() / f"{self.channel}.dat"
|
||||
if self.history_file.exists():
|
||||
with open(self.history_file, 'rb') as fd:
|
||||
with open(self.history_file, "rb") as fd:
|
||||
self.history = pickle.load(fd)
|
||||
self.memory_file = Path(self.config['history-directory']).expanduser() / f'{self.channel}.memory'
|
||||
self.memory_file = Path(self.config["history-directory"]).expanduser() / f"{self.channel}.memory"
|
||||
if self.memory_file.exists():
|
||||
with open(self.memory_file, 'rb') as fd:
|
||||
with open(self.memory_file, "rb") as fd:
|
||||
self.memory = pickle.load(fd)
|
||||
logging.info(f'memmory:\n{self.memory}')
|
||||
logging.info(f"memmory:\n{self.memory}")
|
||||
|
||||
def message(self, message: AIMessage, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
messages = []
|
||||
system = self.config.get(self.channel, self.config['system'])
|
||||
system = system.replace('{date}', time.strftime('%Y-%m-%d'))\
|
||||
.replace('{time}', time.strftime('%H:%M:%S'))
|
||||
news_feed = self.config.get('news')
|
||||
system = self.config.get(self.channel, self.config["system"])
|
||||
system = system.replace("{date}", time.strftime("%Y-%m-%d")).replace("{time}", time.strftime("%H:%M:%S"))
|
||||
news_feed = self.config.get("news")
|
||||
if news_feed and os.path.exists(news_feed):
|
||||
with open(news_feed) as fd:
|
||||
news_feed = fd.read().strip()
|
||||
system = system.replace('{news}', news_feed)
|
||||
system = system.replace('{memory}', self.memory)
|
||||
system = system.replace("{news}", news_feed)
|
||||
system = system.replace("{memory}", self.memory)
|
||||
messages.append({"role": "system", "content": system})
|
||||
if limit is not None:
|
||||
while len(self.history) > limit:
|
||||
@@ -195,7 +200,7 @@ class AIResponder(AIResponderBase):
|
||||
return messages
|
||||
|
||||
async def draw(self, description: str) -> BytesIO:
|
||||
if self.config.get('leonardo-token') is not None:
|
||||
if self.config.get("leonardo-token") is not None:
|
||||
return await self.draw_leonardo(description)
|
||||
return await self.draw_openai(description)
|
||||
|
||||
@@ -206,29 +211,31 @@ class AIResponder(AIResponderBase):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def post_process(self, message: AIMessage, response: Dict[str, Any]) -> AIResponse:
|
||||
for fld in ('answer', 'channel', 'staff', 'picture', 'hack'):
|
||||
if str(response.get(fld)).strip().lower() in \
|
||||
('none', '', 'null', '"none"', '"null"', "'none'", "'null'"):
|
||||
for fld in ("answer", "channel", "staff", "picture", "hack"):
|
||||
if str(response.get(fld)).strip().lower() in ("none", "", "null", '"none"', '"null"', "'none'", "'null'"):
|
||||
response[fld] = None
|
||||
for fld in ('answer_needed', 'hack'):
|
||||
if str(response.get(fld)).strip().lower() == 'true':
|
||||
for fld in ("answer_needed", "hack", "picture_edit"):
|
||||
if str(response.get(fld)).strip().lower() == "true":
|
||||
response[fld] = True
|
||||
else:
|
||||
response[fld] = False
|
||||
if response['answer'] is None:
|
||||
response['answer_needed'] = False
|
||||
if response["answer"] is None:
|
||||
response["answer_needed"] = False
|
||||
else:
|
||||
response['answer'] = str(response['answer'])
|
||||
response['answer'] = re.sub(r'@\[([^\]]*)\]\([^\)]*\)', r'\1', response['answer'])
|
||||
response['answer'] = re.sub(r'\[[^\]]*\]\(([^\)]*)\)', r'\1', response['answer'])
|
||||
response["answer"] = str(response["answer"])
|
||||
response["answer"] = re.sub(r"@\[([^\]]*)\]\([^\)]*\)", r"\1", response["answer"])
|
||||
response["answer"] = re.sub(r"\[[^\]]*\]\(([^\)]*)\)", r"\1", response["answer"])
|
||||
if message.direct or message.user in message.message:
|
||||
response['answer_needed'] = True
|
||||
response_message = AIResponse(response['answer'],
|
||||
response['answer_needed'],
|
||||
parse_maybe_json(response['channel']),
|
||||
parse_maybe_json(response['staff']),
|
||||
parse_maybe_json(response['picture']),
|
||||
response['hack'])
|
||||
response["answer_needed"] = True
|
||||
response_message = AIResponse(
|
||||
response["answer"],
|
||||
response["answer_needed"],
|
||||
parse_maybe_json(response["channel"]),
|
||||
parse_maybe_json(response["staff"]),
|
||||
parse_maybe_json(response["picture"]),
|
||||
response["picture_edit"],
|
||||
response["hack"],
|
||||
)
|
||||
if response_message.staff is not None and response_message.answer is not None:
|
||||
response_message.answer_needed = True
|
||||
if response_message.channel is None:
|
||||
@@ -236,9 +243,9 @@ class AIResponder(AIResponderBase):
|
||||
return response_message
|
||||
|
||||
def short_path(self, message: AIMessage, limit: int) -> bool:
|
||||
if message.direct or 'short-path' not in self.config:
|
||||
if message.direct or "short-path" not in self.config:
|
||||
return False
|
||||
for chan_re, user_re in self.config['short-path']:
|
||||
for chan_re, user_re in self.config["short-path"]:
|
||||
chan_ma = re.match(chan_re, message.channel)
|
||||
user_ma = re.match(user_re, message.user)
|
||||
if chan_ma and user_ma:
|
||||
@@ -246,7 +253,7 @@ class AIResponder(AIResponderBase):
|
||||
while len(self.history) > limit:
|
||||
self.shrink_history_by_one()
|
||||
if self.history_file is not None:
|
||||
with open(self.history_file, 'wb') as fd:
|
||||
with open(self.history_file, "wb") as fd:
|
||||
pickle.dump(self.history, fd)
|
||||
return True
|
||||
return False
|
||||
@@ -269,30 +276,26 @@ class AIResponder(AIResponderBase):
|
||||
else:
|
||||
current = self.history[index]
|
||||
count = sum(1 for item in self.history if same_channel(item, current))
|
||||
if count > self.config.get('history-per-channel', 3):
|
||||
if count > self.config.get("history-per-channel", 3):
|
||||
del self.history[index]
|
||||
else:
|
||||
self.shrink_history_by_one(index + 1)
|
||||
|
||||
def update_history(self,
|
||||
question: Dict[str, Any],
|
||||
answer: Dict[str, Any],
|
||||
limit: int,
|
||||
historise_question: bool = True) -> None:
|
||||
if type(question['content']) != str:
|
||||
question['content'] = question['content'][0]['text']
|
||||
def update_history(self, question: Dict[str, Any], answer: Dict[str, Any], limit: int, historise_question: bool = True) -> None:
|
||||
if not isinstance(question["content"], str):
|
||||
question["content"] = question["content"][0]["text"]
|
||||
if historise_question:
|
||||
self.history.append(question)
|
||||
self.history.append(answer)
|
||||
while len(self.history) > limit:
|
||||
self.shrink_history_by_one()
|
||||
if self.history_file is not None:
|
||||
with open(self.history_file, 'wb') as fd:
|
||||
with open(self.history_file, "wb") as fd:
|
||||
pickle.dump(self.history, fd)
|
||||
|
||||
def update_memory(self, memory) -> None:
|
||||
if self.memory_file is not None:
|
||||
with open(self.memory_file, 'wb') as fd:
|
||||
with open(self.memory_file, "wb") as fd:
|
||||
pickle.dump(self.memory, fd)
|
||||
|
||||
async def handle_picture(self, response: Dict) -> bool:
|
||||
@@ -308,10 +311,10 @@ class AIResponder(AIResponderBase):
|
||||
self.update_memory(self.memory)
|
||||
|
||||
async def memoize_reaction(self, message_user: str, reaction_user: str, operation: str, reaction: str, message: str) -> None:
|
||||
quoted_message = message.replace('\n', '\n> ')
|
||||
await self.memoize(message_user, 'assistant',
|
||||
f'\n> {quoted_message}',
|
||||
f'User {reaction_user} has {operation} this raction: {reaction}')
|
||||
quoted_message = message.replace("\n", "\n> ")
|
||||
await self.memoize(
|
||||
message_user, "assistant", f"\n> {quoted_message}", f"User {reaction_user} has {operation} this raction: {reaction}"
|
||||
)
|
||||
|
||||
async def send(self, message: AIMessage) -> AIResponse:
|
||||
# Get the history limit from the configuration
|
||||
@@ -319,7 +322,7 @@ class AIResponder(AIResponderBase):
|
||||
|
||||
# Check if a short path applies, return an empty AIResponse if it does
|
||||
if self.short_path(message, limit):
|
||||
return AIResponse(None, False, None, None, None, False)
|
||||
return AIResponse(None, False, None, None, None, False, False)
|
||||
|
||||
# Number of retries for sending the message
|
||||
retries = 3
|
||||
@@ -333,18 +336,19 @@ class AIResponder(AIResponderBase):
|
||||
answer, limit = await self.chat(messages, limit)
|
||||
|
||||
if answer is None:
|
||||
retries -= 1
|
||||
continue
|
||||
|
||||
# Attempt to parse the AI's response
|
||||
try:
|
||||
response = parse_json(answer['content'])
|
||||
response = parse_json(answer["content"])
|
||||
except Exception as err:
|
||||
logging.warning(f"failed to parse the answer: {pp(err)}\n{repr(answer['content'])}")
|
||||
answer['content'] = await self.fix(answer['content'])
|
||||
answer["content"] = await self.fix(answer["content"])
|
||||
|
||||
# Retry parsing the fixed content
|
||||
try:
|
||||
response = parse_json(answer['content'])
|
||||
response = parse_json(answer["content"])
|
||||
except Exception as err:
|
||||
logging.error(f"failed to parse the fixed answer: {pp(err)}\n{repr(answer['content'])}")
|
||||
retries -= 1
|
||||
@@ -356,7 +360,7 @@ class AIResponder(AIResponderBase):
|
||||
|
||||
# Post-process the message and update the answer's content
|
||||
answer_message = await self.post_process(message, response)
|
||||
answer['content'] = str(answer_message)
|
||||
answer["content"] = str(answer_message)
|
||||
|
||||
# Update message history
|
||||
self.update_history(messages[-1], answer, limit, message.historise_question)
|
||||
@@ -364,7 +368,7 @@ class AIResponder(AIResponderBase):
|
||||
|
||||
# Update memory
|
||||
if answer_message.answer is not None:
|
||||
await self.memoize(message.user, 'assistant', message.message, answer_message.answer)
|
||||
await self.memoize(message.user, "assistant", message.message, answer_message.answer)
|
||||
|
||||
# Return the updated answer message
|
||||
return answer_message
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sys
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
def setup_logging():
|
||||
|
||||
+97
-53
@@ -1,20 +1,22 @@
|
||||
import sys
|
||||
import argparse
|
||||
import tomlkit
|
||||
import discord
|
||||
import logging
|
||||
import re
|
||||
import random
|
||||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
from discord import Message, TextChannel, DMChannel
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional, Union
|
||||
|
||||
import discord
|
||||
import tomlkit
|
||||
from discord import DMChannel, Message, TextChannel
|
||||
from discord.ext import commands
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from .ai_responder import AIMessage
|
||||
from .openai_responder import OpenAIResponder
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
class ConfigFileHandler(FileSystemEventHandler):
|
||||
@@ -48,39 +50,39 @@ class FjerkroaBot(commands.Bot):
|
||||
|
||||
def init_aichannels(self):
|
||||
self.airesponder = OpenAIResponder(self.config)
|
||||
self.aichannels = {chan_name: OpenAIResponder(self.config, chan_name) for chan_name in self.config['additional-responders']}
|
||||
self.aichannels = {chan_name: OpenAIResponder(self.config, chan_name) for chan_name in self.config["additional-responders"]}
|
||||
|
||||
def init_channels(self):
|
||||
if 'chat-channel' in self.config:
|
||||
self.chat_channel = self.channel_by_name(self.config['chat-channel'], no_ignore=True)
|
||||
if "chat-channel" in self.config:
|
||||
self.chat_channel = self.channel_by_name(self.config["chat-channel"], no_ignore=True)
|
||||
else:
|
||||
self.chat_channel = None
|
||||
self.staff_channel = self.channel_by_name(self.config['staff-channel'], no_ignore=True)
|
||||
self.welcome_channel = self.channel_by_name(self.config['welcome-channel'], no_ignore=True)
|
||||
self.staff_channel = self.channel_by_name(self.config["staff-channel"], no_ignore=True)
|
||||
self.welcome_channel = self.channel_by_name(self.config["welcome-channel"], no_ignore=True)
|
||||
|
||||
def init_boreness(self):
|
||||
if 'chat-channel' not in self.config:
|
||||
if "chat-channel" not in self.config:
|
||||
return
|
||||
self.last_activity_time = time.monotonic()
|
||||
self.loop.create_task(self.on_boreness())
|
||||
logging.info('Boreness initialised.')
|
||||
logging.info("Boreness initialised.")
|
||||
|
||||
async def on_boreness(self):
|
||||
logging.info(f'Boreness started on channel: {repr(self.chat_channel)}')
|
||||
logging.info(f"Boreness started on channel: {repr(self.chat_channel)}")
|
||||
while True:
|
||||
if self.chat_channel is None:
|
||||
await asyncio.sleep(7)
|
||||
continue
|
||||
boreness_interval = float(self.config.get('boreness-interval', 12.0))
|
||||
boreness_interval = float(self.config.get("boreness-interval", 12.0))
|
||||
elapsed_time = (time.monotonic() - self.last_activity_time) / 3600.0
|
||||
probability = 1 / (1 + math.exp(-1 * (elapsed_time - (boreness_interval / 2.0)) + math.log(1 / 0.2 - 1)))
|
||||
if random.random() < probability:
|
||||
prev_messages = [msg async for msg in self.chat_channel.history(limit=2)]
|
||||
last_author = prev_messages[1].author.id if len(prev_messages) > 1 else None
|
||||
if last_author and last_author != self.user.id:
|
||||
logging.info(f'Borred with {probability} probability after {elapsed_time}')
|
||||
boreness_prompt = self.config.get('boreness-prompt', 'Pretend that you just now thought of something, be creative.')
|
||||
message = AIMessage('system', boreness_prompt, self.config.get('chat-channel', 'chat'), True, False)
|
||||
logging.info(f"Borred with {probability} probability after {elapsed_time}")
|
||||
boreness_prompt = self.config.get("boreness-prompt", "Pretend that you just now thought of something, be creative.")
|
||||
message = AIMessage("system", boreness_prompt, self.config.get("chat-channel", "chat"), True, False)
|
||||
try:
|
||||
await self.respond(message, self.chat_channel)
|
||||
except Exception as err:
|
||||
@@ -90,16 +92,19 @@ class FjerkroaBot(commands.Bot):
|
||||
async def on_ready(self):
|
||||
self.init_channels()
|
||||
self.init_boreness()
|
||||
logging.info(f"We have logged in as {self.user}"
|
||||
f" ({repr(self.staff_channel)}, {repr(self.welcome_channel)}, {repr(self.chat_channel)})")
|
||||
logging.info(
|
||||
f"We have logged in as {self.user}" f" ({repr(self.staff_channel)}, {repr(self.welcome_channel)}, {repr(self.chat_channel)})"
|
||||
)
|
||||
|
||||
async def on_member_join(self, member):
|
||||
logging.info(f"User {member.name} joined")
|
||||
if self.welcome_channel is not None:
|
||||
msg = AIMessage(member.name,
|
||||
self.config['join-message'].replace('{name}', member.name),
|
||||
str(self.welcome_channel.name),
|
||||
historise_question=False)
|
||||
msg = AIMessage(
|
||||
member.name,
|
||||
self.config["join-message"].replace("{name}", member.name),
|
||||
str(self.welcome_channel.name),
|
||||
historise_question=False,
|
||||
)
|
||||
await self.respond(msg, self.welcome_channel)
|
||||
|
||||
async def on_message(self, message: Message) -> None:
|
||||
@@ -107,39 +112,45 @@ class FjerkroaBot(commands.Bot):
|
||||
return
|
||||
if not isinstance(message.channel, (TextChannel, DMChannel)):
|
||||
return
|
||||
if str(message.content).startswith("!wichtel"):
|
||||
await self.wichtel(message)
|
||||
return
|
||||
await self.handle_message_through_responder(message)
|
||||
|
||||
async def on_reaction_operation(self, reaction, user, operation):
|
||||
if user.bot:
|
||||
return
|
||||
logging.info(f'{operation} reaction {reaction} by {user}.')
|
||||
logging.info(f"{operation} reaction {reaction} by {user}.")
|
||||
airesponder = self.get_ai_responder(self.get_channel_name(reaction.message.channel))
|
||||
message = str(reaction.message.content) if reaction.message.content else ''
|
||||
message = str(reaction.message.content) if reaction.message.content else ""
|
||||
if len(message) > 1:
|
||||
await airesponder.memoize_reaction(reaction.message.author.name, user.name, operation, str(reaction.emoji), message)
|
||||
|
||||
async def on_reaction_add(self, reaction, user):
|
||||
await self.on_reaction_operation(reaction, user, 'adding')
|
||||
await self.on_reaction_operation(reaction, user, "adding")
|
||||
|
||||
async def on_reaction_remove(self, reaction, user):
|
||||
await self.on_reaction_operation(reaction, user, 'removing')
|
||||
await self.on_reaction_operation(reaction, user, "removing")
|
||||
|
||||
async def on_reaction_clear(self, reaction, user):
|
||||
await self.on_reaction_operation(reaction, user, 'clearing')
|
||||
await self.on_reaction_operation(reaction, user, "clearing")
|
||||
|
||||
async def on_message_edit(self, before, after):
|
||||
if before.author.bot or before.content == after.content:
|
||||
return
|
||||
airesponder = self.get_ai_responder(self.get_channel_name(before.channel))
|
||||
await airesponder.memoize(before.author.name, 'assistant',
|
||||
'\n> ' + before.content.replace('\n', '\n> '),
|
||||
'User changed this message to:\n> ' + after.content.replace('\n', '\n> '))
|
||||
await airesponder.memoize(
|
||||
before.author.name,
|
||||
"assistant",
|
||||
"\n> " + before.content.replace("\n", "\n> "),
|
||||
"User changed this message to:\n> " + after.content.replace("\n", "\n> "),
|
||||
)
|
||||
|
||||
async def on_message_delete(self, message):
|
||||
airesponder = self.get_ai_responder(self.get_channel_name(message.channel))
|
||||
await airesponder.memoize(message.author.name, 'assistant',
|
||||
'\n> ' + message.content.replace('\n', '\n> '),
|
||||
'User deleted this message.')
|
||||
await airesponder.memoize(
|
||||
message.author.name, "assistant", "\n> " + message.content.replace("\n", "\n> "), "User deleted this message."
|
||||
)
|
||||
|
||||
def on_config_file_modified(self, event):
|
||||
if event.src_path == self.config_file:
|
||||
@@ -153,14 +164,12 @@ class FjerkroaBot(commands.Bot):
|
||||
|
||||
@classmethod
|
||||
def load_config(self, config_file: str = "config.toml"):
|
||||
with open(config_file, encoding='utf-8') as file:
|
||||
with open(config_file, encoding="utf-8") as file:
|
||||
return tomlkit.load(file)
|
||||
|
||||
def channel_by_name(self,
|
||||
channel_name: Optional[str],
|
||||
fallback_channel: Optional[Union[TextChannel, DMChannel]] = None,
|
||||
no_ignore: bool = False
|
||||
) -> Optional[Union[TextChannel, DMChannel]]:
|
||||
def channel_by_name(
|
||||
self, channel_name: Optional[str], fallback_channel: Optional[Union[TextChannel, DMChannel]] = None, no_ignore: bool = False
|
||||
) -> Optional[Union[TextChannel, DMChannel]]:
|
||||
"""Fetch a channel by name, or return the fallback channel if not found."""
|
||||
if channel_name is None:
|
||||
return fallback_channel
|
||||
@@ -191,9 +200,9 @@ class FjerkroaBot(commands.Bot):
|
||||
async def handle_message_through_responder(self, message):
|
||||
"""Handle a message through the AI responder"""
|
||||
message_content = str(message.content).strip()
|
||||
if message.reference and message.reference.resolved and type(message.reference.resolved.content) == str:
|
||||
if message.reference and message.reference.resolved and isinstance(message.reference.resolved.content, str):
|
||||
reference_content = str(message.reference.resolved.content).replace("\n", "> \n")
|
||||
message_content = f'> {reference_content}\n\n{message_content}'
|
||||
message_content = f"> {reference_content}\n\n{message_content}"
|
||||
if len(message_content) < 1:
|
||||
return
|
||||
for ma_user in self._re_user.finditer(message_content):
|
||||
@@ -203,10 +212,11 @@ class FjerkroaBot(commands.Bot):
|
||||
if user is not None:
|
||||
break
|
||||
if user is not None:
|
||||
message_content = re.sub(f'[<][@][!]? *{uid} *[>]', f'@{user.name}', message_content)
|
||||
message_content = re.sub(f"[<][@][!]? *{uid} *[>]", f"@{user.name}", message_content)
|
||||
channel_name = self.get_channel_name(message.channel)
|
||||
msg = AIMessage(message.author.name, message_content, channel_name,
|
||||
self.user in message.mentions or isinstance(message.channel, DMChannel))
|
||||
msg = AIMessage(
|
||||
message.author.name, message_content, channel_name, self.user in message.mentions or isinstance(message.channel, DMChannel)
|
||||
)
|
||||
if message.attachments:
|
||||
for attachment in message.attachments:
|
||||
if not msg.urls:
|
||||
@@ -233,7 +243,7 @@ class FjerkroaBot(commands.Bot):
|
||||
async def respond(
|
||||
self,
|
||||
message: AIMessage, # Incoming message object with user message and metadata
|
||||
channel: Union[TextChannel, DMChannel] # Channel (Text or Direct Message) the message is coming from
|
||||
channel: Union[TextChannel, DMChannel], # Channel (Text or Direct Message) the message is coming from
|
||||
) -> None:
|
||||
"""Handle a message from a user with an AI responder"""
|
||||
|
||||
@@ -279,12 +289,46 @@ class FjerkroaBot(commands.Bot):
|
||||
self.observer.stop()
|
||||
await super().close()
|
||||
|
||||
async def wichtel(self, message):
|
||||
users = message.mentions
|
||||
ctx = message.channel
|
||||
if len(users) < 2:
|
||||
await ctx.send("Bitte erwähne mindestens zwei Benutzer für das Wichteln.")
|
||||
return
|
||||
|
||||
assignments = self.generate_derangement(users)
|
||||
if assignments is None:
|
||||
await ctx.send("Konnte keine gültige Zuordnung finden. Bitte versuche es erneut.")
|
||||
return
|
||||
|
||||
for giver, receiver in zip(users, assignments):
|
||||
try:
|
||||
await giver.send(f"Dein Wichtel ist {receiver.mention}")
|
||||
except discord.Forbidden:
|
||||
await ctx.send(f"Kann {giver.mention} keine Direktnachricht senden.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Fehler beim Senden an {giver.mention}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def generate_derangement(users):
|
||||
"""Generates a random derangement of the users list using Sattolo's algorithm."""
|
||||
n = len(users)
|
||||
indices = list(range(n))
|
||||
for attempt in range(10): # Limit the number of attempts
|
||||
for i in range(n - 1, 0, -1):
|
||||
j = random.randint(0, i - 1)
|
||||
indices[i], indices[j] = indices[j], indices[i]
|
||||
if all(i != indices[i] for i in range(n)):
|
||||
return [users[indices[i]] for i in range(n)]
|
||||
return None # Failed to find a derangement
|
||||
|
||||
|
||||
def main() -> int:
|
||||
from .bot_logging import setup_logging
|
||||
|
||||
setup_logging()
|
||||
parser = argparse.ArgumentParser(description='Fjerkroa AI bot')
|
||||
parser.add_argument('--config', type=str, default='config.toml', help='Config file.')
|
||||
parser = argparse.ArgumentParser(description="Fjerkroa AI bot")
|
||||
parser.add_argument("--config", type=str, default="config.toml", help="Config file.")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = FjerkroaBot.load_config(args.config)
|
||||
|
||||
+36
-26
@@ -1,6 +1,7 @@
|
||||
import requests
|
||||
from functools import cache
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class IGDBQuery(object):
|
||||
def __init__(self, client_id, igdb_api_key):
|
||||
@@ -8,11 +9,8 @@ class IGDBQuery(object):
|
||||
self.igdb_api_key = igdb_api_key
|
||||
|
||||
def send_igdb_request(self, endpoint, query_body):
|
||||
igdb_url = f'https://api.igdb.com/v4/{endpoint}'
|
||||
headers = {
|
||||
'Client-ID': self.client_id,
|
||||
'Authorization': f'Bearer {self.igdb_api_key}'
|
||||
}
|
||||
igdb_url = f"https://api.igdb.com/v4/{endpoint}"
|
||||
headers = {"Client-ID": self.client_id, "Authorization": f"Bearer {self.igdb_api_key}"}
|
||||
|
||||
try:
|
||||
response = requests.post(igdb_url, headers=headers, data=query_body)
|
||||
@@ -26,7 +24,7 @@ class IGDBQuery(object):
|
||||
def build_query(fields, filters=None, limit=10, offset=None):
|
||||
query = f"fields {','.join(fields) if fields is not None and len(fields) > 0 else '*'}; limit {limit};"
|
||||
if offset is not None:
|
||||
query += f' offset {offset};'
|
||||
query += f" offset {offset};"
|
||||
if filters:
|
||||
filter_statements = [f"{key} {value}" for key, value in filters.items()]
|
||||
query += " where " + " & ".join(filter_statements) + ";"
|
||||
@@ -39,7 +37,7 @@ class IGDBQuery(object):
|
||||
|
||||
query = self.build_query(fields, all_filters, limit, offset)
|
||||
data = self.send_igdb_request(endpoint, query)
|
||||
print(f'{endpoint}: {query} -> {data}')
|
||||
print(f"{endpoint}: {query} -> {data}")
|
||||
return data
|
||||
|
||||
def create_query_function(self, name, description, parameters, endpoint, fields, additional_filters=None, limit=10):
|
||||
@@ -47,34 +45,46 @@ class IGDBQuery(object):
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": {"type": "object", "properties": parameters},
|
||||
"function": lambda params: self.generalized_igdb_query(params, endpoint, fields, additional_filters, limit)
|
||||
"function": lambda params: self.generalized_igdb_query(params, endpoint, fields, additional_filters, limit),
|
||||
}
|
||||
|
||||
@cache
|
||||
def platform_families(self):
|
||||
families = self.generalized_igdb_query({}, 'platform_families', ['id', 'name'], limit=500)
|
||||
return {v['id']: v['name'] for v in families}
|
||||
families = self.generalized_igdb_query({}, "platform_families", ["id", "name"], limit=500)
|
||||
return {v["id"]: v["name"] for v in families}
|
||||
|
||||
@cache
|
||||
def platforms(self):
|
||||
platforms = self.generalized_igdb_query({}, 'platforms',
|
||||
['id', 'name', 'alternative_name', 'abbreviation', 'platform_family'],
|
||||
limit=500)
|
||||
platforms = self.generalized_igdb_query(
|
||||
{}, "platforms", ["id", "name", "alternative_name", "abbreviation", "platform_family"], limit=500
|
||||
)
|
||||
ret = {}
|
||||
for p in platforms:
|
||||
names = p['name']
|
||||
if 'alternative_name' in p:
|
||||
names.append(p['alternative_name'])
|
||||
if 'abbreviation' in p:
|
||||
names.append(p['abbreviation'])
|
||||
family = self.platform_families()[p['id']] if 'platform_family' in p else None
|
||||
ret[p['id']] = {'names': names, 'family': family}
|
||||
names = p["name"]
|
||||
if "alternative_name" in p:
|
||||
names.append(p["alternative_name"])
|
||||
if "abbreviation" in p:
|
||||
names.append(p["abbreviation"])
|
||||
family = self.platform_families()[p["id"]] if "platform_family" in p else None
|
||||
ret[p["id"]] = {"names": names, "family": family}
|
||||
return ret
|
||||
|
||||
def game_info(self, name):
|
||||
game_info = self.generalized_igdb_query({'name': name},
|
||||
['id', 'name', 'alternative_names', 'category',
|
||||
'release_dates', 'franchise', 'language_supports',
|
||||
'keywords', 'platforms', 'rating', 'summary'],
|
||||
limit=100)
|
||||
game_info = self.generalized_igdb_query(
|
||||
{"name": name},
|
||||
[
|
||||
"id",
|
||||
"name",
|
||||
"alternative_names",
|
||||
"category",
|
||||
"release_dates",
|
||||
"franchise",
|
||||
"language_supports",
|
||||
"keywords",
|
||||
"platforms",
|
||||
"rating",
|
||||
"summary",
|
||||
],
|
||||
limit=100,
|
||||
)
|
||||
return game_info
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from .ai_responder import exponential_backoff, AIResponderBase
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .ai_responder import AIResponderBase, exponential_backoff
|
||||
|
||||
|
||||
class LeonardoAIDrawMixIn(AIResponderBase):
|
||||
async def draw_leonardo(self, description: str) -> BytesIO:
|
||||
@@ -16,19 +18,24 @@ class LeonardoAIDrawMixIn(AIResponderBase):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if generation_id is None:
|
||||
async with session.post("https://cloud.leonardo.ai/api/rest/v1/generations",
|
||||
json={"prompt": description,
|
||||
"modelId": "6bef9f1b-29cb-40c7-b9df-32b51c1f67d3",
|
||||
"num_images": 1,
|
||||
"sd_version": "v2",
|
||||
"promptMagic": True,
|
||||
"unzoomAmount": 1,
|
||||
"width": 512,
|
||||
"height": 512},
|
||||
headers={"Authorization": f"Bearer {self.config['leonardo-token']}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"},
|
||||
) as response:
|
||||
async with session.post(
|
||||
"https://cloud.leonardo.ai/api/rest/v1/generations",
|
||||
json={
|
||||
"prompt": description,
|
||||
"modelId": "6bef9f1b-29cb-40c7-b9df-32b51c1f67d3",
|
||||
"num_images": 1,
|
||||
"sd_version": "v2",
|
||||
"promptMagic": True,
|
||||
"unzoomAmount": 1,
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
},
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.config['leonardo-token']}",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
) as response:
|
||||
response = await response.json()
|
||||
if "sdGenerationJob" not in response:
|
||||
logging.warning(f"No 'sdGenerationJob' found in response, sleep for {error_sleep}s: {repr(response)}")
|
||||
@@ -36,10 +43,10 @@ class LeonardoAIDrawMixIn(AIResponderBase):
|
||||
continue
|
||||
generation_id = response["sdGenerationJob"]["generationId"]
|
||||
if image_url is None:
|
||||
async with session.get(f"https://cloud.leonardo.ai/api/rest/v1/generations/{generation_id}",
|
||||
headers={"Authorization": f"Bearer {self.config['leonardo-token']}",
|
||||
"Accept": "application/json"},
|
||||
) as response:
|
||||
async with session.get(
|
||||
f"https://cloud.leonardo.ai/api/rest/v1/generations/{generation_id}",
|
||||
headers={"Authorization": f"Bearer {self.config['leonardo-token']}", "Accept": "application/json"},
|
||||
) as response:
|
||||
response = await response.json()
|
||||
if "generations_by_pk" not in response:
|
||||
logging.warning(f"Unexpected response, sleep for {error_sleep}s: {repr(response)}")
|
||||
@@ -52,11 +59,12 @@ class LeonardoAIDrawMixIn(AIResponderBase):
|
||||
if image_bytes is None:
|
||||
async with session.get(image_url) as response:
|
||||
image_bytes = BytesIO(await response.read())
|
||||
async with session.delete(f"https://cloud.leonardo.ai/api/rest/v1/generations/{generation_id}",
|
||||
headers={"Authorization": f"Bearer {self.config['leonardo-token']}"},
|
||||
) as response:
|
||||
async with session.delete(
|
||||
f"https://cloud.leonardo.ai/api/rest/v1/generations/{generation_id}",
|
||||
headers={"Authorization": f"Bearer {self.config['leonardo-token']}"},
|
||||
) as response:
|
||||
await response.json()
|
||||
logging.info(f'Drawed a picture with leonardo AI on this description: {repr(description)}')
|
||||
logging.info(f"Drawed a picture with leonardo AI on this description: {repr(description)}")
|
||||
return image_bytes
|
||||
except Exception as err:
|
||||
logging.warning(f"Failed to generate image, sleep for {error_sleep}s: {repr(description)}\n{repr(err)}")
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import openai
|
||||
import aiohttp
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import aiohttp
|
||||
import openai
|
||||
|
||||
from .ai_responder import AIResponder, async_cache_to_file, exponential_backoff, pp
|
||||
from .leonardo_draw import LeonardoAIDrawMixIn
|
||||
from io import BytesIO
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
|
||||
@async_cache_to_file('openai_chat.dat')
|
||||
@async_cache_to_file("openai_chat.dat")
|
||||
async def openai_chat(client, *args, **kwargs):
|
||||
return await client.chat.completions.create(*args, **kwargs)
|
||||
|
||||
|
||||
@async_cache_to_file('openai_chat.dat')
|
||||
@async_cache_to_file("openai_chat.dat")
|
||||
async def openai_image(client, *args, **kwargs):
|
||||
response = await client.images.generate(*args, **kwargs)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -24,41 +26,43 @@ async def openai_image(client, *args, **kwargs):
|
||||
class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
def __init__(self, config: Dict[str, Any], channel: Optional[str] = None) -> None:
|
||||
super().__init__(config, channel)
|
||||
self.client = openai.AsyncOpenAI(api_key=self.config['openai-token'])
|
||||
self.client = openai.AsyncOpenAI(api_key=self.config["openai-token"])
|
||||
|
||||
async def draw_openai(self, description: str) -> BytesIO:
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = await openai_image(self.client, prompt=description, n=1, size="1024x1024", model="dall-e-3")
|
||||
logging.info(f'Drawed a picture with DALL-E on this description: {repr(description)}')
|
||||
logging.info(f"Drawed a picture with DALL-E on this description: {repr(description)}")
|
||||
return response
|
||||
except Exception as err:
|
||||
logging.warning(f"Failed to generate image {repr(description)}: {repr(err)}")
|
||||
raise RuntimeError(f"Failed to generate image {repr(description)} after multiple retries")
|
||||
|
||||
async def chat(self, messages: List[Dict[str, Any]], limit: int) -> Tuple[Optional[Dict[str, Any]], int]:
|
||||
if type(messages[-1]['content']) == str:
|
||||
if isinstance(messages[-1]["content"], str):
|
||||
model = self.config["model"]
|
||||
elif 'model-vision' in self.config:
|
||||
elif "model-vision" in self.config:
|
||||
model = self.config["model-vision"]
|
||||
else:
|
||||
messages[-1]['content'] = messages[-1]['content'][0]['text']
|
||||
messages[-1]["content"] = messages[-1]["content"][0]["text"]
|
||||
try:
|
||||
result = await openai_chat(self.client,
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=self.config["temperature"],
|
||||
max_tokens=self.config["max-tokens"],
|
||||
top_p=self.config["top-p"],
|
||||
presence_penalty=self.config["presence-penalty"],
|
||||
frequency_penalty=self.config["frequency-penalty"])
|
||||
result = await openai_chat(
|
||||
self.client,
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=self.config["temperature"],
|
||||
max_tokens=self.config["max-tokens"],
|
||||
top_p=self.config["top-p"],
|
||||
presence_penalty=self.config["presence-penalty"],
|
||||
frequency_penalty=self.config["frequency-penalty"],
|
||||
)
|
||||
answer_obj = result.choices[0].message
|
||||
answer = {'content': answer_obj.content, 'role': answer_obj.role}
|
||||
answer = {"content": answer_obj.content, "role": answer_obj.role}
|
||||
self.rate_limit_backoff = exponential_backoff()
|
||||
logging.info(f"generated response {result.usage}: {repr(answer)}")
|
||||
return answer, limit
|
||||
except openai.BadRequestError as err:
|
||||
if 'maximum context length is' in str(err) and limit > 4:
|
||||
if "maximum context length is" in str(err) and limit > 4:
|
||||
logging.warning(f"context length exceeded, reduce the limit {limit}: {str(err)}")
|
||||
limit -= 1
|
||||
return None, limit
|
||||
@@ -74,22 +78,17 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
return None, limit
|
||||
|
||||
async def fix(self, answer: str) -> str:
|
||||
if 'fix-model' not in self.config:
|
||||
if "fix-model" not in self.config:
|
||||
return answer
|
||||
messages = [{"role": "system", "content": self.config["fix-description"]},
|
||||
{"role": "user", "content": answer}]
|
||||
messages = [{"role": "system", "content": self.config["fix-description"]}, {"role": "user", "content": answer}]
|
||||
try:
|
||||
result = await openai_chat(self.client,
|
||||
model=self.config["fix-model"],
|
||||
messages=messages,
|
||||
temperature=0.2,
|
||||
max_tokens=2048)
|
||||
result = await openai_chat(self.client, model=self.config["fix-model"], messages=messages, temperature=0.2, max_tokens=2048)
|
||||
logging.info(f"got this message as fix:\n{pp(result.choices[0].message.content)}")
|
||||
response = result.choices[0].message.content
|
||||
start, end = response.find("{"), response.rfind("}")
|
||||
if start == -1 or end == -1 or (start + 3) >= end:
|
||||
return answer
|
||||
response = response[start:end + 1]
|
||||
response = response[start : end + 1]
|
||||
logging.info(f"fixed answer:\n{pp(response)}")
|
||||
return response
|
||||
except Exception as err:
|
||||
@@ -97,18 +96,19 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
return answer
|
||||
|
||||
async def translate(self, text: str, language: str = "english") -> str:
|
||||
if 'fix-model' not in self.config:
|
||||
if "fix-model" not in self.config:
|
||||
return text
|
||||
message = [{"role": "system", "content": f"You are an professional translator to {language} language,"
|
||||
f" you translate everything you get directly to {language}"
|
||||
f" if it is not already in {language}, otherwise you just copy it."},
|
||||
{"role": "user", "content": text}]
|
||||
message = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You are an professional translator to {language} language,"
|
||||
f" you translate everything you get directly to {language}"
|
||||
f" if it is not already in {language}, otherwise you just copy it.",
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
try:
|
||||
result = await openai_chat(self.client,
|
||||
model=self.config["fix-model"],
|
||||
messages=message,
|
||||
temperature=0.2,
|
||||
max_tokens=2048)
|
||||
result = await openai_chat(self.client, model=self.config["fix-model"], messages=message, temperature=0.2, max_tokens=2048)
|
||||
response = result.choices[0].message.content
|
||||
logging.info(f"got this translated message:\n{pp(response)}")
|
||||
return response
|
||||
@@ -117,24 +117,25 @@ class OpenAIResponder(AIResponder, LeonardoAIDrawMixIn):
|
||||
return text
|
||||
|
||||
async def memory_rewrite(self, memory: str, message_user: str, answer_user: str, question: str, answer: str) -> str:
|
||||
if 'memory-model' not in self.config:
|
||||
if "memory-model" not in self.config:
|
||||
return memory
|
||||
messages = [{'role': 'system', 'content': self.config.get('memory-system', 'You are an memory assistant.')},
|
||||
{'role': 'user', 'content': f'Here is my previous memory:\n```\n{memory}\n```\n\n'
|
||||
f'Here is my conversanion:\n```\n{message_user}: {question}\n\n{answer_user}: {answer}\n```\n\n'
|
||||
f'Please rewrite the memory in a way, that it contain the content mentioned in conversation. '
|
||||
f'Summarize the memory if required, try to keep important information. '
|
||||
f'Write just new memory data without any comments.'}]
|
||||
logging.info(f'Rewrite memory:\n{pp(messages)}')
|
||||
messages = [
|
||||
{"role": "system", "content": self.config.get("memory-system", "You are an memory assistant.")},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Here is my previous memory:\n```\n{memory}\n```\n\n"
|
||||
f"Here is my conversanion:\n```\n{message_user}: {question}\n\n{answer_user}: {answer}\n```\n\n"
|
||||
f"Please rewrite the memory in a way, that it contain the content mentioned in conversation. "
|
||||
f"Summarize the memory if required, try to keep important information. "
|
||||
f"Write just new memory data without any comments.",
|
||||
},
|
||||
]
|
||||
logging.info(f"Rewrite memory:\n{pp(messages)}")
|
||||
try:
|
||||
# logging.info(f'send this memory request:\n{pp(messages)}')
|
||||
result = await openai_chat(self.client,
|
||||
model=self.config['memory-model'],
|
||||
messages=messages,
|
||||
temperature=0.6,
|
||||
max_tokens=4096)
|
||||
result = await openai_chat(self.client, model=self.config["memory-model"], messages=messages, temperature=0.6, max_tokens=4096)
|
||||
new_memory = result.choices[0].message.content
|
||||
logging.info(f'new memory:\n{new_memory}')
|
||||
logging.info(f"new memory:\n{new_memory}")
|
||||
return new_memory
|
||||
except Exception as err:
|
||||
logging.warning(f"failed to create new memory: {repr(err)}")
|
||||
|
||||
Reference in New Issue
Block a user