mirror of
https://github.com/Balshgit/gpt_chat_bot.git
synced 2026-02-03 11:40:39 +03:00
add style check by ruff (#34)
* reformat settings * add init tests for timed lru cache * add check style by ruff
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
from datetime import timezone
|
||||
from enum import StrEnum, unique
|
||||
|
||||
from dateutil import tz
|
||||
|
||||
AUDIO_SEGMENT_DURATION = 120 * 1000
|
||||
|
||||
API_PREFIX = "/api"
|
||||
CHATGPT_BASE_URI = "/backend-api/v2/conversation"
|
||||
INVALID_GPT_REQUEST_MESSAGES = ("Invalid request model", "return unexpected http status code")
|
||||
|
||||
MOSCOW_TZ = tz.gettz("Europe/Moscow")
|
||||
UTC_TZ = timezone.utc
|
||||
|
||||
|
||||
class BotStagesEnum(StrEnum):
|
||||
about_me = "about_me"
|
||||
|
||||
@@ -45,7 +45,7 @@ class BotApplication:
|
||||
|
||||
async def polling(self) -> None:
|
||||
if self.settings.STAGE == "runtests":
|
||||
return None
|
||||
return
|
||||
await self.application.initialize()
|
||||
await self.application.start()
|
||||
await self.application.updater.start_polling() # type: ignore
|
||||
|
||||
@@ -23,7 +23,7 @@ async def main_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> st
|
||||
|
||||
async def about_me(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not update.effective_message:
|
||||
return None
|
||||
return
|
||||
await update.effective_message.reply_text(
|
||||
"Автор бота: *Дмитрий Афанасьев*\n\nTg nickname: *Balshtg*", parse_mode="MarkdownV2"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ async def about_me(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
async def about_bot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not update.effective_message:
|
||||
return None
|
||||
return
|
||||
chatgpt_service = ChatGptService.build()
|
||||
model = await chatgpt_service.get_current_chatgpt_model()
|
||||
await update.effective_message.reply_text(
|
||||
@@ -44,7 +44,7 @@ async def about_bot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
|
||||
async def website(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not update.effective_message:
|
||||
return None
|
||||
return
|
||||
website = urljoin(settings.DOMAIN, f"{settings.chat_prefix}/")
|
||||
await update.effective_message.reply_text(f"Веб версия: {website}")
|
||||
|
||||
@@ -53,7 +53,7 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
"""Send a message when the command /help is issued."""
|
||||
|
||||
if not update.effective_message:
|
||||
return None
|
||||
return
|
||||
reply_markup = InlineKeyboardMarkup(main_keyboard)
|
||||
await update.effective_message.reply_text(
|
||||
"Help!",
|
||||
@@ -65,7 +65,7 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
|
||||
async def ask_question(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not update.message:
|
||||
return None
|
||||
return
|
||||
|
||||
await update.message.reply_text("Пожалуйста подождите, ответ в среднем занимает 10-15 секунд")
|
||||
|
||||
@@ -77,10 +77,10 @@ async def ask_question(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
|
||||
async def voice_recognize(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if not update.message:
|
||||
return None
|
||||
return
|
||||
await update.message.reply_text("Пожалуйста, ожидайте :)\nТрехминутная запись обрабатывается примерно 30 секунд")
|
||||
if not update.message.voice:
|
||||
return None
|
||||
return
|
||||
|
||||
sound_file = await update.message.voice.get_file()
|
||||
sound_bytes = await sound_file.download_as_bytearray()
|
||||
|
||||
@@ -70,15 +70,16 @@ class ChatGPTRepository:
|
||||
status = response.status_code
|
||||
for message in INVALID_GPT_REQUEST_MESSAGES:
|
||||
if message in response.text:
|
||||
message = f"{message}: {chatgpt_model}"
|
||||
logger.info(message, question=question, chatgpt_model=chatgpt_model)
|
||||
return message
|
||||
invalid_model_message = f"{message}: {chatgpt_model}"
|
||||
logger.info(invalid_model_message, question=question, chatgpt_model=chatgpt_model)
|
||||
return invalid_model_message
|
||||
if status != httpx.codes.OK:
|
||||
logger.info(f"got response status: {status} from chat api", response.text)
|
||||
return "Что-то пошло не так, попробуйте еще раз или обратитесь к администратору"
|
||||
return response.text
|
||||
except Exception as error:
|
||||
logger.error("error get data from chat api", error=error)
|
||||
else:
|
||||
return response.text
|
||||
return "Вообще всё сломалось :("
|
||||
|
||||
async def request_to_chatgpt_microservice(self, question: str, chatgpt_model: str) -> Response:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
import subprocess # noqa
|
||||
import subprocess # noqa: S404
|
||||
import tempfile
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
@@ -68,7 +68,7 @@ class SpeechToTextService:
|
||||
new_filename = self.filename + ".wav"
|
||||
cmd = ["ffmpeg", "-loglevel", "quiet", "-i", self.filename, "-vn", new_filename]
|
||||
try:
|
||||
subprocess.run(args=cmd) # noqa: S603
|
||||
subprocess.run(args=cmd, check=True) # noqa: S603
|
||||
logger.info("file has been converted to wav", filename=new_filename)
|
||||
except Exception as error:
|
||||
logger.error("cant convert voice", error=error, filename=self.filename)
|
||||
@@ -80,11 +80,10 @@ class SpeechToTextService:
|
||||
with AudioFile(tmpfile) as source:
|
||||
audio_text = self.recognizer.listen(source)
|
||||
try:
|
||||
text = self.recognizer.recognize_google(audio_text, language="ru-RU")
|
||||
return text
|
||||
return self.recognizer.recognize_google(audio_text, language="ru-RU")
|
||||
except SpeechRecognizerError as error:
|
||||
logger.error("error recognizing text with google", error=error)
|
||||
raise error
|
||||
raise
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -3,6 +3,8 @@ from functools import cache, wraps
|
||||
from inspect import cleandoc
|
||||
from typing import Any, Callable
|
||||
|
||||
from constants import MOSCOW_TZ
|
||||
|
||||
|
||||
def timed_lru_cache(
|
||||
microseconds: int = 0,
|
||||
@@ -15,14 +17,14 @@ def timed_lru_cache(
|
||||
update_delta = timedelta(
|
||||
microseconds=microseconds, milliseconds=milliseconds, seconds=seconds, minutes=minutes, hours=hours
|
||||
)
|
||||
next_update = datetime.utcnow() + update_delta
|
||||
next_update = datetime.now(tz=MOSCOW_TZ) + update_delta
|
||||
|
||||
cached_func = cache(func)
|
||||
|
||||
@wraps(func)
|
||||
def _wrapped(*args: Any, **kwargs: Any) -> Callable[[Any], Any]:
|
||||
nonlocal next_update
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(tz=MOSCOW_TZ)
|
||||
if now >= next_update:
|
||||
cached_func.cache_clear()
|
||||
next_update = now + update_delta
|
||||
|
||||
@@ -44,9 +44,9 @@ class Database:
|
||||
session: Session = self._sync_session_factory()
|
||||
try:
|
||||
return session
|
||||
except Exception as err:
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise err
|
||||
raise
|
||||
finally:
|
||||
session.commit()
|
||||
session.close()
|
||||
@@ -71,9 +71,9 @@ class Database:
|
||||
async with self._async_session_factory() as session, session.begin():
|
||||
try:
|
||||
yield session
|
||||
except Exception as error:
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise error
|
||||
raise
|
||||
|
||||
async def create_database(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""initial commit
|
||||
|
||||
Revision ID: eb78565abec7
|
||||
Revises:
|
||||
Revises:
|
||||
Create Date: 2023-10-05 18:28:30.915361
|
||||
|
||||
"""
|
||||
|
||||
@@ -30,7 +30,7 @@ def upgrade() -> None:
|
||||
models = results.scalars().all()
|
||||
|
||||
if models:
|
||||
return None
|
||||
return
|
||||
models = []
|
||||
for model in ChatGptModelsEnum.values():
|
||||
priority = 0 if model != "gpt-3.5-turbo-stream-FreeGpt" else 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from functools import cached_property, lru_cache
|
||||
from functools import cache, cached_property
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -71,9 +71,11 @@ class AppSettings(SentrySettings, LoggingSettings, BaseSettings):
|
||||
# Enable uvicorn reloading
|
||||
RELOAD: bool = False
|
||||
|
||||
# telegram settings
|
||||
TELEGRAM_API_TOKEN: str = "123456789:AABBCCDDEEFFaabbccddeeff-1234567890"
|
||||
# webhook settings
|
||||
START_WITH_WEBHOOK: bool = False
|
||||
|
||||
# domain settings
|
||||
DOMAIN: str = "https://localhost"
|
||||
URL_PREFIX: str = ""
|
||||
CHAT_PREFIX: str = ""
|
||||
@@ -82,7 +84,6 @@ class AppSettings(SentrySettings, LoggingSettings, BaseSettings):
|
||||
DB_ECHO: bool = False
|
||||
|
||||
# ==== gpt settings ====
|
||||
GPT_MODEL: str = "gpt-3.5-turbo-stream-DeepAi"
|
||||
GPT_BASE_HOST: str = "http://chathpt_chat_service:8858"
|
||||
|
||||
@model_validator(mode="before") # type: ignore[arg-type]
|
||||
@@ -146,7 +147,7 @@ class AppSettings(SentrySettings, LoggingSettings, BaseSettings):
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
@cache
|
||||
def get_settings() -> AppSettings:
|
||||
return AppSettings()
|
||||
|
||||
|
||||
@@ -97,8 +97,7 @@ class BotMessageFactory(factory.DictFactory):
|
||||
|
||||
@classmethod
|
||||
def create_instance(cls, **kwargs: Any) -> dict[str, Any]:
|
||||
data = {**cls.build(**kwargs), "from": BotUserFactory()._asdict()}
|
||||
return data
|
||||
return {**cls.build(**kwargs), "from": BotUserFactory()._asdict()}
|
||||
|
||||
|
||||
class BotUpdateFactory(factory.DictFactory):
|
||||
@@ -114,8 +113,7 @@ class CallBackFactory(factory.DictFactory):
|
||||
|
||||
@classmethod
|
||||
def create_instance(cls, **kwargs: Any) -> dict[str, Any]:
|
||||
data = {**cls.build(**kwargs), "from": BotUserFactory()._asdict()}
|
||||
return data
|
||||
return {**cls.build(**kwargs), "from": BotUserFactory()._asdict()}
|
||||
|
||||
|
||||
class BotCallBackQueryFactory(factory.DictFactory):
|
||||
|
||||
0
bot_microservice/tests/unit/__init__.py
Normal file
0
bot_microservice/tests/unit/__init__.py
Normal file
60
bot_microservice/tests/unit/test_system_utils.py
Normal file
60
bot_microservice/tests/unit/test_system_utils.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from core.utils import timed_lru_cache
|
||||
|
||||
|
||||
class TestTimedLruCache:
|
||||
call_count: int = 0
|
||||
|
||||
def sum_two_numbers(self, first: int, second: int) -> int:
|
||||
self.call_count += 1
|
||||
return first + second
|
||||
|
||||
def test_timed_lru_cache_cached_for_1_second(self) -> None:
|
||||
self.call_count = 0
|
||||
|
||||
tested_func = timed_lru_cache(seconds=1)(self.sum_two_numbers)
|
||||
|
||||
self._call_function_many_times(call_times=2, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(0.5)
|
||||
self._call_function_many_times(call_times=4, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(1)
|
||||
self._call_function_many_times(call_times=3, func=tested_func, first=2, second=40, result=42)
|
||||
assert tested_func(2, 2) == 4
|
||||
assert self.call_count == 3
|
||||
|
||||
def test_timed_lru_cache_cached_for_long_time(self) -> None:
|
||||
self.call_count = 0
|
||||
|
||||
tested_func = timed_lru_cache(minutes=5)(self.sum_two_numbers)
|
||||
|
||||
self._call_function_many_times(call_times=3, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(0.2)
|
||||
self._call_function_many_times(call_times=4, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(0.2)
|
||||
self._call_function_many_times(call_times=2, func=tested_func, first=2, second=40, result=42)
|
||||
assert tested_func(2, 2) == 4
|
||||
assert self.call_count == 2
|
||||
|
||||
def test_timed_lru_cache_cached_for_short_time(self) -> None:
|
||||
self.call_count = 0
|
||||
|
||||
tested_func = timed_lru_cache(milliseconds=200)(self.sum_two_numbers)
|
||||
|
||||
self._call_function_many_times(call_times=2, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(0.3)
|
||||
self._call_function_many_times(call_times=5, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(0.3)
|
||||
self._call_function_many_times(call_times=7, func=tested_func, first=2, second=40, result=42)
|
||||
time.sleep(0.3)
|
||||
self._call_function_many_times(call_times=3, func=tested_func, first=2, second=40, result=42)
|
||||
assert tested_func(2, 2) == 4
|
||||
assert self.call_count == 5
|
||||
|
||||
@staticmethod
|
||||
def _call_function_many_times(
|
||||
call_times: int, func: Callable[[int, int], int], first: int, second: int, result: int
|
||||
) -> None:
|
||||
for _ in range(call_times):
|
||||
assert func(first, second) == result
|
||||
Reference in New Issue
Block a user