|
|
| import re
|
| import logging
|
| from typing import Dict, Tuple, Optional
|
| from dataclasses import dataclass
|
|
|
| @dataclass
|
| class ParsedIntent:
|
| """Универсальный контейнер для распознанного намерения"""
|
| intent: str
|
| confidence: float
|
| original_text: str
|
| normalized_text: str
|
| parameters: Dict[str, any]
|
| source: str = "fast_parser"
|
|
|
| class FastIntentParser:
|
| """
|
| Быстрый парсер намерений на основе ключевых слов и правил.
|
| Обрабатывает 80-90% типичных запросов без использования ML.
|
| """
|
|
|
| def __init__(self):
|
| self.logger = logging.getLogger(__name__)
|
| self._setup_domains()
|
| self._setup_synonyms()
|
| self._setup_patterns()
|
|
|
| def _setup_domains(self):
|
| """Настройка доменов и ключевых слов"""
|
| self.domains = {
|
| 'greeting': {
|
| 'keywords': ['привет', 'здравствуй', 'добрый', 'хай', 'салют', 'здаров'],
|
| 'priority': 1,
|
| 'response_templates': [
|
| "Привет! Готов к работе.",
|
| "Здравствуйте! Чем могу помочь?",
|
| "Приветствую! Ariel на связи."
|
| ]
|
| },
|
| 'system': {
|
| 'keywords': ['будильник', 'таймер', 'открой', 'запусти', 'выключи', 'громкость'],
|
| 'priority': 2,
|
| 'subdomains': {
|
| 'alarm': ['будильник', 'разбуди', 'напомни'],
|
| 'app_launch': ['открой', 'запусти', 'включи'],
|
| 'system_control': ['выключи', 'перезагрузи', 'громкость']
|
| }
|
| },
|
| 'visualization': {
|
| 'keywords': ['график', 'диаграмм', 'схем', 'визуализир', 'построй', 'нарисуй'],
|
| 'priority': 3,
|
| 'subdomains': {
|
| 'plot': ['график', 'построй'],
|
| 'chart': ['диаграмм', 'гистограмм'],
|
| 'scheme': ['схем', 'блок-схем']
|
| }
|
| },
|
| 'knowledge': {
|
| 'keywords': ['что такое', 'как работает', 'объясни', 'найди информацию', 'база данных'],
|
| 'priority': 4
|
| },
|
| 'creative': {
|
| 'keywords': ['расскажи', 'пошути', 'придумай', 'рекомендуй', 'советуй'],
|
| 'priority': 5
|
| },
|
| 'help': {
|
| 'keywords': ['помощь', 'команды', 'что ты умеешь', 'справка'],
|
| 'priority': 6
|
| }
|
| }
|
|
|
| def _setup_synonyms(self):
|
| """Настройка синонимов для нормализации текста"""
|
| self.synonyms = {
|
| 'привет': ['салют', 'здаров', 'хей', 'хай', 'здорово', 'добрый день'],
|
| 'будильник': ['будильничек', 'напоминание', 'оповещение', 'звонок'],
|
| 'поставь': ['заведи', 'установи', 'создай', 'активируй'],
|
| 'открой': ['запусти', 'включи', 'открой', 'запусти'],
|
| 'график': ['графики', 'графичек', 'плотик'],
|
| 'помощь': ['справка', 'хелп', 'помоги', 'подскажи']
|
| }
|
|
|
| def _setup_patterns(self):
|
| """Настройка regex-паттернов для сложных случаев"""
|
| self.patterns = {
|
| 'system': [
|
|
|
| r'(поставь|заведи|установи).*будильник.*(\d{1,2}:\d{2})',
|
| r'будильник.*(\d{1,2}).*(утра|вечера|часов|час)',
|
| r'разбуди.*(\d{1,2}).*(утра|вечера)',
|
|
|
| r'(открой|запусти).*(браузер|chrome|хром|firefox|файрфокс)',
|
| r'(открой|запусти).*(терминал|cmd|командную строку)',
|
|
|
| r'(выключи|перезагрузи).*(компьютер|систему)',
|
| r'(сделай|поставь).*(громче|тише)'
|
| ],
|
| 'visualization': [
|
| r'построй.*график.*(\w+).*от.*(\d+).*до.*(\d+)',
|
| r'график.*(sin|синус|cos|косинус|tan|тангенс)',
|
| r'диаграмм.*(кругова|столбчата|гистограмм)',
|
| r'нарисуй.*схем.*работы'
|
| ],
|
| 'knowledge': [
|
| r'что такое (\w+)',
|
| r'как работает (\w+)',
|
| r'объясни.*(\w+)',
|
| r'найди.*информацию.*о (\w+)'
|
| ]
|
| }
|
|
|
| def normalize_text(self, text: str) -> str:
|
| """Нормализация текста: приведение к нижнему регистру и замена синонимов"""
|
| if not text:
|
| return ""
|
|
|
| text_lower = text.lower().strip()
|
|
|
|
|
| for main_word, synonyms in self.synonyms.items():
|
| for synonym in synonyms:
|
| if synonym in text_lower:
|
| text_lower = text_lower.replace(synonym, main_word)
|
| self.logger.debug(f"Заменен синоним '{synonym}' -> '{main_word}'")
|
|
|
| return text_lower
|
|
|
| def extract_parameters(self, domain: str, text: str) -> Dict[str, any]:
|
| """Извлечение параметров из текста команды"""
|
| normalized_text = self.normalize_text(text)
|
| parameters = {}
|
|
|
| if domain == 'system':
|
|
|
| time_match = re.search(r'(\d{1,2})(?::(\d{2}))?\s*(утра|вечера|часов|час)?', normalized_text)
|
| if time_match:
|
| hour = int(time_match.group(1))
|
| minute = int(time_match.group(2) or "0")
|
| period = time_match.group(3) or ""
|
|
|
|
|
| if period == 'вечера' and hour < 12:
|
| hour += 12
|
|
|
| parameters['time'] = f"{hour:02d}:{minute:02d}"
|
| parameters['period'] = period
|
|
|
|
|
| app_matches = re.findall(r'(браузер|хром|chrome|терминал|cmd)', normalized_text)
|
| if app_matches:
|
| parameters['app'] = app_matches[0]
|
|
|
| elif domain == 'visualization':
|
|
|
| func_match = re.search(r'(sin|синус|cos|косинус|tan|тангенс|x\^2)', normalized_text)
|
| if func_match:
|
| func_map = {'синус': 'sin', 'косинус': 'cos', 'тангенс': 'tan'}
|
| parameters['function'] = func_map.get(func_match.group(1), func_match.group(1))
|
|
|
|
|
| range_match = re.search(r'от\s*(\d+)\s*до\s*(\d+)', normalized_text)
|
| if range_match:
|
| parameters['x_range'] = [float(range_match.group(1)), float(range_match.group(2))]
|
|
|
| elif domain == 'knowledge':
|
|
|
| topic_match = re.search(r'что такое\s+(\w+)', normalized_text)
|
| if not topic_match:
|
| topic_match = re.search(r'как работает\s+(\w+)', normalized_text)
|
| if not topic_match:
|
| topic_match = re.search(r'объясни\s+(\w+)', normalized_text)
|
|
|
| if topic_match:
|
| parameters['topic'] = topic_match.group(1)
|
|
|
| return parameters
|
|
|
| def parse(self, text: str) -> Optional[ParsedIntent]:
|
| """
|
| Основной метод парсинга намерения из текста.
|
| Возвращает ParsedIntent или None если намерение не распознано.
|
| """
|
| if not text or not text.strip():
|
| return None
|
|
|
| normalized_text = self.normalize_text(text)
|
| self.logger.debug(f"Парсинг текста: '{text}' -> '{normalized_text}'")
|
|
|
|
|
| domain_from_patterns = self._check_patterns(normalized_text)
|
| if domain_from_patterns:
|
| domain, subdomain, confidence = domain_from_patterns
|
| parameters = self.extract_parameters(domain, normalized_text)
|
|
|
| return ParsedIntent(
|
| intent=domain,
|
| confidence=confidence,
|
| original_text=text,
|
| normalized_text=normalized_text,
|
| parameters=parameters
|
| )
|
|
|
|
|
| domain_from_keywords = self._check_keywords(normalized_text)
|
| if domain_from_keywords:
|
| domain, subdomain, confidence = domain_from_keywords
|
| parameters = self.extract_parameters(domain, normalized_text)
|
|
|
| return ParsedIntent(
|
| intent=domain,
|
| confidence=confidence,
|
| original_text=text,
|
| normalized_text=normalized_text,
|
| parameters=parameters
|
| )
|
|
|
|
|
| self.logger.debug(f"Не удалось распознать намерение: '{text}'")
|
| return None
|
|
|
| def _check_patterns(self, text: str) -> Optional[Tuple[str, str, float]]:
|
| """Проверка текста по regex-паттернам"""
|
| for domain, pattern_list in self.patterns.items():
|
| for pattern in pattern_list:
|
| if re.search(pattern, text):
|
| self.logger.debug(f"Найден паттерн '{pattern}' для домена '{domain}'")
|
| return domain, None, 0.95
|
|
|
| return None
|
|
|
| def _check_keywords(self, text: str) -> Optional[Tuple[str, str, float]]:
|
| """Проверка текста по ключевым словам"""
|
| found_domains = []
|
|
|
| for domain, domain_config in self.domains.items():
|
| for keyword in domain_config['keywords']:
|
| if keyword in text:
|
| confidence = 0.9 if len(keyword) > 3 else 0.7
|
| found_domains.append((domain, None, confidence))
|
| self.logger.debug(f"Найдено ключевое слово '{keyword}' для домена '{domain}'")
|
|
|
| if not found_domains:
|
| return None
|
|
|
|
|
| found_domains.sort(key=lambda x: self.domains[x[0]]['priority'])
|
| return found_domains[0]
|
|
|
|
|
| def create_fast_parser() -> FastIntentParser:
|
| """Создание и настройка быстрого парсера"""
|
| return FastIntentParser() |