feat: refine assistant logic and update docs

This commit is contained in:
future
2026-04-09 21:03:02 +03:00
parent ebe79c3692
commit 42c064a274
19 changed files with 1958 additions and 492 deletions

View File

@@ -54,6 +54,16 @@ _PARTS_OF_DAY = {"утра", "дня", "вечера", "ночи"}
_FILLER_WORDS = {"мне", "меня", "пожалуйста", "на", "в", "во", "к", "и"}
_HOUR_WORDS = {"час", "часа", "часов"}
_MINUTE_WORDS = {"минута", "минуту", "минуты", "минут"}
_ALARM_MARKERS = {"будильник", "разбуди", "поставь", "установи", "включи", "на", "в", "к"}
_ALARM_LIST_RE = re.compile(
r"\b(какие|какой|список|активн|покажи|показать|сколько|есть ли|перечисли)\b"
)
_ALARM_CANCEL_RE = re.compile(
r"\b(отмени|отмена|удали|удалить|выключи|отключи|деактивир|сбрось|очисти)\b"
)
_ALARM_CREATE_RE = re.compile(
r"\b(постав|установ|запусти|включи|разбуди|создай|добавь|измени|перенес|назнач)\b"
)
def _parse_number_tokens(tokens, start_index: int):
@@ -97,10 +107,9 @@ def _apply_part_of_day(hour: int, part_of_day: str | None) -> int:
def _extract_alarm_time_words(text: str):
tokens = re.findall(r"[a-zа-я0-9]+", text.lower().replace("ё", "е"))
markers = {"будильник", "разбуди", "поставь", "установи", "включи", "на", "в", "к"}
for index, token in enumerate(tokens):
if token not in markers:
if token not in _ALARM_MARKERS:
continue
current = index + 1
@@ -134,6 +143,40 @@ def _extract_alarm_time_words(text: str):
return None
def _extract_alarm_time(text: str):
# Формат "7:30", "7.30", "7-30" и варианты с "в/на/к".
match = re.search(r"(?:\b(?:на|в|во|к)\s+)?(\d{1,2})[:.-](\d{2})\b", text)
if match:
h, m = int(match.group(1)), int(match.group(2))
period_match = re.search(
r"\b(?:на|в|во|к)?\s*"
+ re.escape(match.group(0).strip())
+ r"\s+(утра|дня|вечера|ночи)\b",
text,
)
part_of_day = period_match.group(1) if period_match else None
h = _apply_part_of_day(h, part_of_day)
if 0 <= h <= 23 and 0 <= m <= 59:
return h, m
# Формат цифрами: "в 7 утра", "на 7", "к 6 30".
match_time = re.search(
r"(?:\b(?:на|в|во|к)\s+)?(\d{1,2})(?:\s*(?:часов|часа|час))?"
r"(?:\s+(\d{1,2})(?:\s*(?:минут|минуты|минута))?)?"
r"(?:\s+(утра|дня|вечера|ночи))?\b",
text,
)
if match_time:
h = int(match_time.group(1))
m = int(match_time.group(2)) if match_time.group(2) else 0
h = _apply_part_of_day(h, match_time.group(3))
if 0 <= h <= 23 and 0 <= m <= 59:
return h, m
# Формат словами: "в семь утра", "будильник семь тридцать".
return _extract_alarm_time_words(text)
class AlarmClock:
def __init__(self):
self.alarms = []
@@ -229,7 +272,14 @@ class AlarmClock:
return self.add_alarm_with_days(hour, minute, days=None)
def add_alarm_with_days(self, hour: int, minute: int, days=None):
"""Добавление нового будильника (или обновление существующего) с днями недели."""
"""
Добавление нового будильника (или обновление существующего) с днями недели.
Returns:
"created" - создан новый будильник
"reactivated" - найден существующий неактивный, включён обратно
"already_active" - такой будильник уже активен
"""
days_key = self._days_key(days)
for alarm in self.alarms:
if (
@@ -237,11 +287,13 @@ class AlarmClock:
and alarm.get("minute") == minute
and self._days_key(alarm.get("days")) == days_key
):
if alarm.get("active"):
return "already_active"
alarm["active"] = True
alarm["days"] = days_key
alarm["last_triggered"] = None
self.save_alarms()
return
return "reactivated"
self.alarms.append(
{"hour": hour, "minute": minute, "active": True, "days": days_key}
@@ -250,6 +302,7 @@ class AlarmClock:
days_phrase = self._format_days_phrase(days_key)
suffix = f" {days_phrase}" if days_phrase else ""
print(f"⏰ Будильник установлен на {hour:02d}:{minute:02d}{suffix}")
return "created"
def cancel_all_alarms(self):
"""Выключение (деактивация) всех будильников."""
@@ -258,6 +311,33 @@ class AlarmClock:
self.save_alarms()
print("🔕 Все будильники отменены.")
def remove_alarms(self, hour: int, minute: int, days=None) -> int:
"""
Удаляет будильники по времени.
Если переданы days, удаляются только будильники с совпадающими днями.
"""
days_key = self._days_key(days)
kept = []
removed = 0
for alarm in self.alarms:
alarm_hour = alarm.get("hour")
alarm_minute = alarm.get("minute")
if alarm_hour != hour or alarm_minute != minute:
kept.append(alarm)
continue
if days_key is not None and self._days_key(alarm.get("days")) != days_key:
kept.append(alarm)
continue
removed += 1
if removed:
self.alarms = kept
self.save_alarms()
return removed
def describe_alarms(self) -> str:
"""Возвращает текстовое описание активных будильников."""
active = [
@@ -365,73 +445,60 @@ class AlarmClock:
def parse_command(self, text: str) -> str | None:
"""
Парсинг команды установки будильника из текста.
Примеры: "разбуди в 7:30", "будильник на 8 утра".
Парсинг команд управления будильниками.
Примеры: "разбуди в 7:30", "удали будильник на 8:00", "какие будильники".
"""
text = replace_roman_numerals(text.lower())
if "будильник" not in text and "разбуди" not in text:
text = replace_roman_numerals(text.lower().replace("ё", "е"))
if not re.search(r"\b(будильник\w*|разбуд\w*)\b", text):
return None
if "будильник" in text and re.search(
r"(какие|какой|список|активн|покажи|сколько|есть ли)", text
):
if _ALARM_LIST_RE.search(text):
return self.describe_alarms()
if "отмени" in text:
self.cancel_all_alarms()
return "Хорошо, я отменил все будильники."
if _ALARM_CANCEL_RE.search(text):
cancel_time = _extract_alarm_time(text)
cancel_days = self._extract_alarm_days(text)
if cancel_time:
h, m = cancel_time
removed = self.remove_alarms(h, m, days=cancel_days)
if removed:
days_phrase = self._format_days_phrase(cancel_days)
suffix = f" {days_phrase}" if days_phrase else ""
return f"Удалил {removed} будильник(а) на {h:02d}:{m:02d}{suffix}."
return f"Не нашел будильник на {h:02d}:{m:02d}."
if re.search(r"\b(все|всех)\b", text) or "будильники" in text:
self.cancel_all_alarms()
return "Хорошо, я отменил все будильники."
return (
"Скажите время будильника, который нужно удалить. "
"Например: удалите будильник на 7:30."
)
days = self._extract_alarm_days(text)
# Поиск формата "7:30", "7.30" и вариантов с "в/на/к".
match = re.search(r"(?:\b(?:на|в|во|к)\s+)?(\d{1,2})[:.-](\d{2})\b", text)
if match:
h, m = int(match.group(1)), int(match.group(2))
period_match = re.search(
r"\b(?:на|в|во|к)?\s*" + re.escape(match.group(0).strip()) + r"\s+(утра|дня|вечера|ночи)\b",
text,
)
part_of_day = period_match.group(1) if period_match else None
h = _apply_part_of_day(h, part_of_day)
if 0 <= h <= 23 and 0 <= m <= 59:
self.add_alarm_with_days(h, m, days=days)
days_phrase = self._format_days_phrase(days)
suffix = f" {days_phrase}" if days_phrase else ""
return f"Я установил будильник на {h} часов {m} минут{suffix}."
# Поиск формата цифрами: "в 7 утра", "на 7", "к 6 30"
match_time = re.search(
r"(?:\b(?:на|в|во|к)\s+)?(\d{1,2})(?:\s*(?:часов|часа|час))?(?:\s+(\d{1,2})(?:\s*(?:минут|минуты|минута))?)?(?:\s+(утра|дня|вечера|ночи))?\b",
text,
)
if match_time:
h = int(match_time.group(1))
m = int(match_time.group(2)) if match_time.group(2) else 0
h = _apply_part_of_day(h, match_time.group(3))
if 0 <= h <= 23 and 0 <= m <= 59:
self.add_alarm_with_days(h, m, days=days)
days_phrase = self._format_days_phrase(days)
suffix = f" {days_phrase}" if days_phrase else ""
return f"Хорошо, разбужу вас в {h}:{m:02d}{suffix}."
# Поиск формата словами: "в семь утра", "будильник семь тридцать"
word_time = _extract_alarm_time_words(text)
if word_time:
h, m = word_time
self.add_alarm_with_days(h, m, days=days)
alarm_time = _extract_alarm_time(text)
if alarm_time:
h, m = alarm_time
add_status = self.add_alarm_with_days(h, m, days=days)
if add_status == "already_active":
return "Такой будильник уже установлен."
days_phrase = self._format_days_phrase(days)
suffix = f" {days_phrase}" if days_phrase else ""
return f"Хорошо, разбужу вас в {h}:{m:02d}{suffix}."
if re.search(r"(постав|установ|запусти|включи|разбуди)", text) or text.strip() in {
if _ALARM_CREATE_RE.search(text) or text.strip() in {
"будильник",
"поставь будильник",
"создай будильник",
"добавь будильник",
}:
return ASK_ALARM_TIME_PROMPT
return "Я не понял время для будильника. Пожалуйста, скажите точное время, например 'семь тридцать'."
return (
"Я не понял команду для будильника. "
"Скажите, например: поставь на 7:30, покажи будильники или удали будильник на 7:30."
)
# Глобальный экземпляр