feat: refine assistant logic and update docs
This commit is contained in:
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
# Глобальный экземпляр
|
||||
|
||||
Reference in New Issue
Block a user