fix syntax
This commit is contained in:
@@ -148,16 +148,18 @@ async def private_voice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
|
|||||||
)
|
)
|
||||||
status_message = await update.message.reply_text("Распознаю голосовое сообщение...")
|
status_message = await update.message.reply_text("Распознаю голосовое сообщение...")
|
||||||
temp_path: Path | None = None
|
temp_path: Path | None = None
|
||||||
|
transcript = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
telegram_file = await context.bot.get_file(voice.file_id)
|
telegram_file = await context.bot.get_file(voice.file_id)
|
||||||
file_descriptor, raw_path = tempfile.mkstemp(suffix=".ogg")
|
file_descriptor, raw_path = tempfile.mkstemp(suffix=".ogg")
|
||||||
os.close(file_descriptor)
|
os.close(file_descriptor)
|
||||||
temp_path = Path(raw_path)
|
voice_path = Path(raw_path)
|
||||||
await telegram_file.download_to_drive(custom_path=temp_path)
|
temp_path = voice_path
|
||||||
|
await telegram_file.download_to_drive(custom_path=voice_path)
|
||||||
transcript = await asyncio.to_thread(
|
transcript = await asyncio.to_thread(
|
||||||
get_speech_recognizer(context).transcribe,
|
get_speech_recognizer(context).transcribe,
|
||||||
temp_path,
|
voice_path,
|
||||||
)
|
)
|
||||||
except (SpeechRecognitionError, TelegramError):
|
except (SpeechRecognitionError, TelegramError):
|
||||||
logger.exception("Failed to transcribe Telegram voice message")
|
logger.exception("Failed to transcribe Telegram voice message")
|
||||||
@@ -364,7 +366,7 @@ async def remind_command(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
reminder_id = get_storage(context).add_reminder(
|
reminder_id = get_storage(context).add_reminder(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
chat_id=int(update.effective_chat.id),
|
chat_id=int(update.message.chat_id),
|
||||||
text=parsed.text,
|
text=parsed.text,
|
||||||
remind_at_utc=parsed.remind_at_utc,
|
remind_at_utc=parsed.remind_at_utc,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
import warnings
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Protocol
|
from typing import Any, Callable, Protocol, cast
|
||||||
|
|
||||||
|
|
||||||
class SpeechRecognitionError(RuntimeError):
|
class SpeechRecognitionError(RuntimeError):
|
||||||
@@ -21,9 +21,9 @@ def load_whisper_model_class() -> Any:
|
|||||||
missing = object()
|
missing = object()
|
||||||
loaded_torch = sys.modules.get("torch", missing)
|
loaded_torch = sys.modules.get("torch", missing)
|
||||||
if loaded_torch is missing:
|
if loaded_torch is missing:
|
||||||
sys.modules["torch"] = None
|
sys.modules["torch"] = cast(Any, None)
|
||||||
try:
|
try:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings(record=False):
|
||||||
warnings.filterwarnings(
|
warnings.filterwarnings(
|
||||||
"ignore",
|
"ignore",
|
||||||
message="pkg_resources is deprecated as an API.*",
|
message="pkg_resources is deprecated as an API.*",
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ from .config import get_default_model
|
|||||||
from .time_utils import to_utc_iso, utc_now
|
from .time_utils import to_utc_iso, utc_now
|
||||||
|
|
||||||
|
|
||||||
|
def row_to_dict(row: sqlite3.Row) -> dict[str, Any]:
|
||||||
|
return {str(key): row[key] for key in row.keys()}
|
||||||
|
|
||||||
|
|
||||||
|
def required_lastrowid(cursor: sqlite3.Cursor) -> int:
|
||||||
|
lastrowid = cursor.lastrowid
|
||||||
|
if lastrowid is None:
|
||||||
|
raise RuntimeError("SQLite insert did not return a row ID")
|
||||||
|
return lastrowid
|
||||||
|
|
||||||
|
|
||||||
class AssistantStorage:
|
class AssistantStorage:
|
||||||
def __init__(self, path: Path) -> None:
|
def __init__(self, path: Path) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -162,7 +173,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(user_id, chat_id, started_after_id, limit),
|
(user_id, chat_id, started_after_id, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(row) for row in reversed(rows)]
|
return [row_to_dict(row) for row in reversed(rows)]
|
||||||
|
|
||||||
def start_new_conversation(self, user_id: int, chat_id: int) -> None:
|
def start_new_conversation(self, user_id: int, chat_id: int) -> None:
|
||||||
now = to_utc_iso(utc_now())
|
now = to_utc_iso(utc_now())
|
||||||
@@ -230,7 +241,7 @@ class AssistantStorage:
|
|||||||
age = max(0, newest_id - int(row["id"]))
|
age = max(0, newest_id - int(row["id"]))
|
||||||
recency_weight = 1.0 / (1.0 + age / 50.0)
|
recency_weight = 1.0 / (1.0 + age / 50.0)
|
||||||
score = (coverage + phrase_bonus) * (0.5 + 0.5 * recency_weight)
|
score = (coverage + phrase_bonus) * (0.5 + 0.5 * recency_weight)
|
||||||
item = dict(row)
|
item = row_to_dict(row)
|
||||||
item["score"] = round(score, 4)
|
item["score"] = round(score, 4)
|
||||||
matches.append((score, item))
|
matches.append((score, item))
|
||||||
|
|
||||||
@@ -266,7 +277,7 @@ class AssistantStorage:
|
|||||||
(user_id, chat_id, message_id, radius),
|
(user_id, chat_id, message_id, radius),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
rows = [*reversed(before), *after]
|
rows = [*reversed(before), *after]
|
||||||
return [dict(row) for row in rows]
|
return [row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
def get_user_model(self, user_id: int, provider: str = "local") -> str:
|
def get_user_model(self, user_id: int, provider: str = "local") -> str:
|
||||||
column = self._model_column(provider)
|
column = self._model_column(provider)
|
||||||
@@ -315,7 +326,7 @@ class AssistantStorage:
|
|||||||
"INSERT INTO memories(user_id, text, created_at) VALUES (?, ?, ?)",
|
"INSERT INTO memories(user_id, text, created_at) VALUES (?, ?, ?)",
|
||||||
(user_id, text, to_utc_iso(utc_now())),
|
(user_id, text, to_utc_iso(utc_now())),
|
||||||
)
|
)
|
||||||
return int(cursor.lastrowid)
|
return required_lastrowid(cursor)
|
||||||
|
|
||||||
def list_memories(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
|
def list_memories(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -329,7 +340,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(user_id, limit),
|
(user_id, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
def delete_memory(self, user_id: int, memory_id: int) -> bool:
|
def delete_memory(self, user_id: int, memory_id: int) -> bool:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -345,7 +356,7 @@ class AssistantStorage:
|
|||||||
"INSERT INTO notes(user_id, text, created_at) VALUES (?, ?, ?)",
|
"INSERT INTO notes(user_id, text, created_at) VALUES (?, ?, ?)",
|
||||||
(user_id, text, to_utc_iso(utc_now())),
|
(user_id, text, to_utc_iso(utc_now())),
|
||||||
)
|
)
|
||||||
return int(cursor.lastrowid)
|
return required_lastrowid(cursor)
|
||||||
|
|
||||||
def list_notes(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
|
def list_notes(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -359,7 +370,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(user_id, limit),
|
(user_id, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
def delete_note(self, user_id: int, note_id: int) -> bool:
|
def delete_note(self, user_id: int, note_id: int) -> bool:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -390,7 +401,7 @@ class AssistantStorage:
|
|||||||
to_utc_iso(utc_now()),
|
to_utc_iso(utc_now()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return int(cursor.lastrowid)
|
return required_lastrowid(cursor)
|
||||||
|
|
||||||
def list_reminders(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
|
def list_reminders(self, user_id: int, limit: int = 20) -> list[dict[str, Any]]:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -404,7 +415,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(user_id, limit),
|
(user_id, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
def cancel_reminder(self, user_id: int, reminder_id: int) -> bool:
|
def cancel_reminder(self, user_id: int, reminder_id: int) -> bool:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -430,7 +441,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(to_utc_iso(now_utc), limit),
|
(to_utc_iso(now_utc), limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
def mark_reminder_sent(self, reminder_id: int) -> None:
|
def mark_reminder_sent(self, reminder_id: int) -> None:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -453,7 +464,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(user_id, title, status, now, now),
|
(user_id, title, status, now, now),
|
||||||
)
|
)
|
||||||
return int(cursor.lastrowid)
|
return required_lastrowid(cursor)
|
||||||
|
|
||||||
def list_tracked_items(self, user_id: int, limit: int = 30) -> list[dict[str, Any]]:
|
def list_tracked_items(self, user_id: int, limit: int = 30) -> list[dict[str, Any]]:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
@@ -467,7 +478,7 @@ class AssistantStorage:
|
|||||||
""",
|
""",
|
||||||
(user_id, limit),
|
(user_id, limit),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [row_to_dict(row) for row in rows]
|
||||||
|
|
||||||
def set_tracked_status(self, user_id: int, item_id: int, status: str) -> bool:
|
def set_tracked_status(self, user_id: int, item_id: int, status: str) -> bool:
|
||||||
with self._connection() as connection:
|
with self._connection() as connection:
|
||||||
|
|||||||
@@ -57,12 +57,13 @@ def get_voice_max_duration(context: ContextTypes.DEFAULT_TYPE) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def command_text(context: ContextTypes.DEFAULT_TYPE) -> str:
|
def command_text(context: ContextTypes.DEFAULT_TYPE) -> str:
|
||||||
return " ".join(context.args).strip()
|
return " ".join(context.args or []).strip()
|
||||||
|
|
||||||
|
|
||||||
def require_user_id(update: Update) -> int | None:
|
def require_user_id(update: Update) -> int | None:
|
||||||
if update.effective_user:
|
user = update.effective_user
|
||||||
return int(update.effective_user.id)
|
if user:
|
||||||
|
return int(user.id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ from assistant_bot.storage import AssistantStorage
|
|||||||
|
|
||||||
class ReminderParserTests(unittest.TestCase):
|
class ReminderParserTests(unittest.TestCase):
|
||||||
def test_supported_relative_unit(self) -> None:
|
def test_supported_relative_unit(self) -> None:
|
||||||
self.assertEqual(unit_to_timedelta(2, "часа").total_seconds(), 7200)
|
delta = unit_to_timedelta(2, "часа")
|
||||||
|
self.assertIsNotNone(delta)
|
||||||
|
assert delta is not None
|
||||||
|
self.assertEqual(delta.total_seconds(), 7200)
|
||||||
|
|
||||||
def test_absolute_datetime(self) -> None:
|
def test_absolute_datetime(self) -> None:
|
||||||
result = parse_reminder(
|
result = parse_reminder(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import unittest
|
import unittest
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from assistant_bot.jobs import post_init, post_shutdown
|
from assistant_bot.jobs import post_init, post_shutdown
|
||||||
@@ -10,11 +11,11 @@ class ReminderJobLifecycleTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
async def test_reminder_task_is_cancelled_on_shutdown(self) -> None:
|
async def test_reminder_task_is_cancelled_on_shutdown(self) -> None:
|
||||||
started = asyncio.Event()
|
started = asyncio.Event()
|
||||||
|
|
||||||
async def fake_reminder_loop(application) -> None:
|
async def fake_reminder_loop(_application) -> None:
|
||||||
started.set()
|
started.set()
|
||||||
await asyncio.Event().wait()
|
await asyncio.Event().wait()
|
||||||
|
|
||||||
application = SimpleNamespace(bot_data={})
|
application: Any = SimpleNamespace(bot_data={})
|
||||||
with patch("assistant_bot.jobs.reminder_loop", fake_reminder_loop):
|
with patch("assistant_bot.jobs.reminder_loop", fake_reminder_loop):
|
||||||
await post_init(application)
|
await post_init(application)
|
||||||
await started.wait()
|
await started.wait()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from assistant_bot.handlers import private_voice
|
from assistant_bot.handlers import private_voice
|
||||||
@@ -15,7 +16,7 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
voice=SimpleNamespace(duration=duration, file_id="voice-file-id"),
|
voice=SimpleNamespace(duration=duration, file_id="voice-file-id"),
|
||||||
reply_text=AsyncMock(return_value=status_message),
|
reply_text=AsyncMock(return_value=status_message),
|
||||||
)
|
)
|
||||||
update = SimpleNamespace(message=message)
|
update: Any = SimpleNamespace(message=message)
|
||||||
telegram_file = SimpleNamespace(download_to_drive=AsyncMock())
|
telegram_file = SimpleNamespace(download_to_drive=AsyncMock())
|
||||||
bot = SimpleNamespace(
|
bot = SimpleNamespace(
|
||||||
get_file=AsyncMock(return_value=telegram_file),
|
get_file=AsyncMock(return_value=telegram_file),
|
||||||
@@ -23,7 +24,7 @@ class VoiceHandlerTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
recognizer = MagicMock()
|
recognizer = MagicMock()
|
||||||
recognizer.transcribe.return_value = transcript
|
recognizer.transcribe.return_value = transcript
|
||||||
context = SimpleNamespace(
|
context: Any = SimpleNamespace(
|
||||||
bot=bot,
|
bot=bot,
|
||||||
application=SimpleNamespace(
|
application=SimpleNamespace(
|
||||||
bot_data={
|
bot_data={
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ class FakeChatCompletions:
|
|||||||
return self.completion
|
return self.completion
|
||||||
|
|
||||||
|
|
||||||
|
class FakeHTTPError(RuntimeError):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__("forbidden")
|
||||||
|
self.response = SimpleNamespace(
|
||||||
|
status_code=403,
|
||||||
|
text='{"error":{"message":"folder mismatch"}}',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class YandexAIClientTests(unittest.IsolatedAsyncioTestCase):
|
class YandexAIClientTests(unittest.IsolatedAsyncioTestCase):
|
||||||
def test_multiple_system_messages_are_merged_at_the_beginning(self) -> None:
|
def test_multiple_system_messages_are_merged_at_the_beginning(self) -> None:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -62,11 +71,7 @@ class YandexAIClientTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_http_error_includes_safe_response_body(self) -> None:
|
def test_http_error_includes_safe_response_body(self) -> None:
|
||||||
error = RuntimeError("forbidden")
|
error = FakeHTTPError()
|
||||||
error.response = SimpleNamespace(
|
|
||||||
status_code=403,
|
|
||||||
text='{"error":{"message":"folder mismatch"}}',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
describe_yandex_error(error),
|
describe_yandex_error(error),
|
||||||
|
|||||||
Reference in New Issue
Block a user