antimoda1 commited on
Commit
d061e47
·
1 Parent(s): 92e422e
app.py CHANGED
@@ -5,10 +5,7 @@ from generation import wrap_prompt
5
  from llm import get_llm_answer
6
  from retrieval import Retrieval
7
  from _2_splitting import _parse_single_year
8
- from vocabulary.parse_vocabulary import parse_vocabulary
9
-
10
-
11
- vocabulary, _ = parse_vocabulary('vocabulary/vocabulary.md')
12
 
13
 
14
  class Perform:
@@ -98,7 +95,7 @@ def ask_llm(query, filtered_indices_state):
98
  return
99
 
100
  # Формируем промпт и отправляем в LLM
101
- prompt = wrap_prompt(context, query, vocabulary)
102
 
103
  # Потоковая выдача ответа
104
  full_answer = ""
 
5
  from llm import get_llm_answer
6
  from retrieval import Retrieval
7
  from _2_splitting import _parse_single_year
8
+ from vocabulary.parse_vocabulary import VOCABULARY_MANAGER
 
 
 
9
 
10
 
11
  class Perform:
 
95
  return
96
 
97
  # Формируем промпт и отправляем в LLM
98
+ prompt = wrap_prompt(context, query, VOCABULARY_MANAGER.vocabulary)
99
 
100
  # Потоковая выдача ответа
101
  full_answer = ""
lemmatizer.py CHANGED
@@ -1,26 +1,10 @@
1
  # -*- coding: utf-8 -*-
2
- """
3
- Общий модуль для лемматизации на русском языке с поддержкой пользовательских терминов.
4
- Используется в retrieval.py и test_lemmatization.py
5
- """
6
- from pathlib import Path
7
  import spacy
8
- from vocabulary.parse_vocabulary import parse_vocabulary, extract_stem
9
 
10
 
11
- class RussianLemmatizer:
12
- """
13
- Русский лемматизатор на основе spaCy с поддержкой пользовательских терминов.
14
- Содержит общую логику для всех компонентов системы.
15
- """
16
-
17
- def __init__(self, load_terms: bool = True):
18
- """
19
- Инициализация лемматизатора.
20
-
21
- Args:
22
- load_terms: загружать ли термины из vocabulary.md при инициализации
23
- """
24
  print(" Загрузка русской модели spaCy...")
25
  try:
26
  self.nlp = spacy.load("ru_core_news_sm")
@@ -33,27 +17,10 @@ class RussianLemmatizer:
33
  self.terms = {}
34
  self.stems = {}
35
 
36
- if load_terms:
37
- self._register_terms()
38
-
39
- def _register_terms(self):
40
- """
41
- Загружает термины из vocabulary/vocabulary.md и регистрирует их в spaCy
42
- как custom component для исправления лемм.
43
- """
44
- # Находим файл vocabulary относительно этого модуля
45
- vocab_file = Path(__file__).parent / "vocabulary" / "vocabulary.md"
46
-
47
- if not vocab_file.exists():
48
- print(f" ⚠️ Файл {vocab_file} не найден, управление терминами пропущено")
49
- return
50
-
51
- # Парсим словарь термин -> описание и извлекаем информацию о корнях
52
- vocab_dict, stems_dict = parse_vocabulary(str(vocab_file))
53
-
54
  # Создаём словарь для быстрого поиска (приводим к нижнему регистру)
55
- self.terms = {term.lower(): term.lower() for term in vocab_dict.keys()}
56
- self.stems = stems_dict
57
 
58
  print(f" Загружено {len(self.terms)} терминов из vocabulary.md")
59
  print(f" Информация о корнях: {len(self.stems)} корней с окончаниями")
@@ -64,15 +31,9 @@ class RussianLemmatizer:
64
  """Компонент для исправления лемм терминов и их форм"""
65
  for token in doc:
66
  lemma_lower = token.lemma_.lower()
67
-
68
- # Сначала проверяем прямое совпадение
69
- if lemma_lower in self.terms:
70
- token.lemma_ = self.terms[lemma_lower]
71
- else:
72
- # Если не нашли, пробуем найти по корню и окончанию
73
- canonical = extract_stem(lemma_lower, self.stems)
74
- if canonical:
75
- token.lemma_ = canonical.lower()
76
  return doc
77
 
78
  # Добавляем компонент после лемматизатора
@@ -80,8 +41,7 @@ class RussianLemmatizer:
80
  self.nlp.add_pipe("fix_terms", after="lemmatizer")
81
 
82
  def tokenize_text(self, text: str) -> list[str]:
83
- """
84
- Лемматизация текста для русского языка (spaCy).
85
 
86
  Args:
87
  text: текст для лемматизации
 
1
  # -*- coding: utf-8 -*-
 
 
 
 
 
2
  import spacy
3
+ from vocabulary.parse_vocabulary import VOCABULARY_MANAGER
4
 
5
 
6
+ class RussianLemmatizer:
7
+ def __init__(self):
 
 
 
 
 
 
 
 
 
 
 
8
  print(" Загрузка русской модели spaCy...")
9
  try:
10
  self.nlp = spacy.load("ru_core_news_sm")
 
17
  self.terms = {}
18
  self.stems = {}
19
 
20
+ # регистрация терминов
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Создаём словарь для быстрого поиска (приводим к нижнему регистру)
22
+ self.terms = {term.lower(): term.lower() for term in VOCABULARY_MANAGER.vocabulary}
23
+ self.stems = VOCABULARY_MANAGER.stems
24
 
25
  print(f" Загружено {len(self.terms)} терминов из vocabulary.md")
26
  print(f" Информация о корнях: {len(self.stems)} корней с окончаниями")
 
31
  """Компонент для исправления лемм терминов и их форм"""
32
  for token in doc:
33
  lemma_lower = token.lemma_.lower()
34
+ canonical = VOCABULARY_MANAGER.extract_stem(lemma_lower)
35
+ if canonical:
36
+ token.lemma_ = canonical.lower()
 
 
 
 
 
 
37
  return doc
38
 
39
  # Добавляем компонент после лемматизатора
 
41
  self.nlp.add_pipe("fix_terms", after="lemmatizer")
42
 
43
  def tokenize_text(self, text: str) -> list[str]:
44
+ """Лемматизация текста для русского языка (spaCy).
 
45
 
46
  Args:
47
  text: текст для лемматизации
retrieval.py CHANGED
@@ -66,7 +66,7 @@ class Retrieval:
66
 
67
  # Инициализация лемматизатора для русского языка
68
  print(" Инициализация лемматизатора...")
69
- self.lemmatizer = RussianLemmatizer(load_terms=True)
70
 
71
  # Загружаем и обрабатываем данные
72
  print("1. Загрузка данных из JSON...")
 
66
 
67
  # Инициализация лемматизатора для русского языка
68
  print(" Инициализация лемматизатора...")
69
+ self.lemmatizer = RussianLemmatizer()
70
 
71
  # Загружаем и обрабатываем данные
72
  print("1. Загрузка данных из JSON...")
tests/test_lemmatization.py CHANGED
@@ -115,7 +115,7 @@ def test_lemmatization():
115
  print("="*70)
116
 
117
  # Инициализируем лемматизатор один раз
118
- lemmatizer = RussianLemmatizer(load_terms=True)
119
 
120
  total_passed = 0
121
  total_failed = 0
 
115
  print("="*70)
116
 
117
  # Инициализируем лемматизатор один раз
118
+ lemmatizer = RussianLemmatizer()
119
 
120
  total_passed = 0
121
  total_failed = 0
vocabulary/__init__.py DELETED
@@ -1,5 +0,0 @@
1
- """Vocabulary management module for RAG system."""
2
-
3
- from .vocabulary_manager import VocabularyManager
4
-
5
- __all__ = ["VocabularyManager"]
 
 
 
 
 
 
vocabulary/parse_vocabulary.py CHANGED
@@ -1,121 +1,115 @@
1
- def parse_vocabulary(filepath):
2
- """
3
- Парсит файл vocabulary.md и возвращает два словаря:
4
-
5
- Формат файла:
6
- ## Категория
7
- ### Термин[окончание], Синоним1[окончание]
8
- Определение термина
9
-
10
- Возвращает кортеж (vocabulary, stems):
11
- - vocabulary: термин (без скобок) -> определение
12
- - stems: корень -> информация об окончании
13
- """
14
- vocabulary = {}
15
- stems = {} # корень -> информация об окончании
16
-
17
- with open(filepath, 'r', encoding='utf-8') as f:
18
- lines = f.readlines()
19
-
20
- i = 0
21
- while i < len(lines):
22
- line = lines[i].strip()
23
 
24
- # Пропускаем категории (##)
25
- if line.startswith('## '):
26
- i += 1
27
- continue
28
 
29
- # Если это заголовок термина (начинается с ###)
30
- if line.startswith('### '):
31
- # Взять текст после ###
32
- terms_line = line[4:].strip()
33
-
34
- # Разбить на отдельные термины (синонимы разделены ", ")
35
- terms_raw = [term.strip() for term in terms_line.split(',')]
 
 
 
 
 
 
36
 
37
- # Следующая непустая строка - определение
38
- i += 1
39
- definition = ''
40
- while i < len(lines):
41
- def_line = lines[i].strip()
42
- # Если это не пустая строка и не заголовок
43
- if def_line and not def_line.startswith('###') and not def_line.startswith('## '):
44
- definition = def_line
45
- break
46
  i += 1
 
47
 
48
- # Обработка каждого термина
49
- for term_raw in terms_raw:
50
- if not term_raw:
51
- continue
52
-
53
- # Извлекаем корень (часть перед скобками) и окончание (в скобках)
54
- if '[' in term_raw and ']' in term_raw:
55
- bracket_pos = term_raw.index('[')
56
- stem = term_raw[:bracket_pos]
57
- ending_info = term_raw[bracket_pos+1:term_raw.index(']')]
58
- else:
59
- stem = term_raw
60
- ending_info = None
61
 
62
- # Убираем скобки - это чистый термин для поиска
63
- term_clean = term_raw.replace('[', '').replace(']', '')
 
64
 
65
- # Добавляем в основной словарь
66
- vocabulary[term_clean] = definition
 
 
 
 
 
 
 
 
67
 
68
- # Сохраняем информацию о корне для гибкого поиска
69
- if stem and ending_info is not None:
70
- if stem.lower() not in stems:
71
- stems[stem.lower()] = {
72
- 'canonical': term_clean,
73
- 'ending': ending_info
74
- }
 
 
75
 
76
- i += 1
77
-
78
- return vocabulary, stems
 
 
 
 
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- def extract_stem(word, stems_dict):
82
- """
83
- Проверяет, совпадает ли слово с каким-то корнем из stems_dict
84
- с допустимым окончанием (может отличаться на 0-2 буквы)
85
-
86
- Args:
87
- word: слово для проверки (в нижнем регистре)
88
- stems_dict: словарь корней {корень: {canonical, ending}}
89
-
90
- Returns:
91
- Канонический вид термина или None
92
- """
93
- for stem, info in stems_dict.items():
94
- if word.startswith(stem):
95
- # Вычисляем окончание в слове
96
- word_ending = word[len(stem):]
97
- expected_ending = info['ending']
98
-
99
- if abs(len(word_ending) - len(expected_ending)) <= 2:
100
- return info['canonical']
101
- return None
102
 
103
 
104
  if __name__ == '__main__':
105
- import os
106
- # Получить путь к файлу vocabulary.md в той же папке
107
- current_dir = os.path.dirname(os.path.abspath(__file__))
108
- vocab_file = os.path.join(current_dir, 'vocabulary.md')
109
-
110
- # Спарсить файл
111
- vocabulary, stems = parse_vocabulary(vocab_file)
112
-
113
- # Вывести результаты
114
- print(f"Загружено терминов: {len(vocabulary)}\n")
115
- print("=== ПОЛНЫЙ СПИСОК ТЕРМИНОВ ===")
116
- for term, definition in sorted(vocabulary.items()):
117
- print(f"{term}: {definition[:50]}...")
118
-
119
- print(f"\n=== ИНФОРМАЦИЯ О КОРНЯХ ({len(stems)} корней) ===")
120
- for stem, info in sorted(stems.items()):
121
- print(f"{stem} [{info['ending']}] -> {info['canonical']}")
 
1
+ from typing import Optional
2
+
3
+
4
+ def parse_term(term_raw: str) -> tuple[str, str, Optional[str]]:
5
+ """Извлекает лемму термина, корень и окончание из строки термина"""
6
+ if "[" in term_raw and "]" in term_raw:
7
+ bracket_pos = term_raw.index("[")
8
+ stem = term_raw[:bracket_pos]
9
+ ending = term_raw[bracket_pos + 1: term_raw.index("]")]
10
+ else:
11
+ stem = term_raw
12
+ ending = None
13
+ # важно, что не пустую строку! Есть пустое окончание, есть отсутствие окончания у несклоняемых мслов
14
+ term_lemma = term_raw.replace("[", "").replace("]", "")
15
+ return term_lemma, stem, ending
16
+
17
+
18
+ class ParserVocabulary:
19
+ def __init__(self, filepath) -> None:
20
+ """
21
+ Парсит файл vocabulary.md и возвращает два словаря:
 
22
 
23
+ Формат файла:
24
+ ## Категория
25
+ ### Термин[окончание], Синоним1[окончание]
26
+ Определение термина
27
 
28
+ - vocabulary: термин (без скобок) -> определение
29
+ - stems: корень -> информация об окончании
30
+ """
31
+ self.vocabulary: dict[str, str] = {}
32
+ self.stems: dict[str, dict[str, str]] = {} # корень -> информация об окончании
33
+
34
+ with open(filepath, 'r', encoding='utf-8') as f:
35
+ lines = f.readlines()
36
+
37
+ i = 0
38
+ len_of_lines = len(lines)
39
+ while i < len_of_lines:
40
+ line = lines[i].strip()
41
 
42
+ # Пропускаем категории (##)
43
+ if line.startswith('## '):
 
 
 
 
 
 
 
44
  i += 1
45
+ continue
46
 
47
+ # Если это заголовок термина (начинается с ###)
48
+ if line.startswith('### '):
49
+ # Взять текст после ###
50
+ terms_line = line[4:].strip()
 
 
 
 
 
 
 
 
 
51
 
52
+ # Разбить на отдельные термины (синонимы разделены ", ")
53
+ terms_raw = [term.strip() for term in terms_line.split(',')]
54
+ assert all(term_raw for term_raw in terms_raw)
55
 
56
+ # Следующая непустая строка - определение
57
+ i += 1
58
+ definition = ''
59
+ while i < len_of_lines:
60
+ def_line = lines[i].strip()
61
+ # Если это не пустая строка и не заголовок
62
+ if def_line and not def_line.startswith('###') and not def_line.startswith('## '):
63
+ definition = def_line
64
+ break
65
+ i += 1
66
 
67
+ # Обработка каждого термина
68
+ for term_raw in terms_raw:
69
+ self.__parse_term_raw(term_raw, definition)
70
+
71
+ i += 1
72
+
73
+ def __parse_term_raw(self, term_raw, definition):
74
+ term_lemma, stem, ending = parse_term(term_raw)
75
+ self.vocabulary[term_lemma] = definition
76
 
77
+ # Сохраняем информацию о корне для гибкого поиска
78
+ if stem and ending is not None:
79
+ if stem.lower() not in self.stems:
80
+ self.stems[stem.lower()] = {
81
+ 'canonical': term_lemma,
82
+ 'ending': ending
83
+ }
84
 
85
+ def extract_stem(self, word: str):
86
+ """
87
+ Проверяет, совпадает ли слово с каким-то корнем из stems_dict
88
+ с допустимым окончанием (может отличаться на 0-2 буквы)
89
+
90
+ Args:
91
+ word: слово для проверки (в нижнем регистре)
92
+
93
+ Returns:
94
+ Канонический вид термина или None
95
+ """
96
+ for stem, info in self.stems.items():
97
+ if word.startswith(stem):
98
+ # Вычисляем окончание в слове
99
+ word_ending = word[len(stem):]
100
+ expected_ending = info['ending']
101
+
102
+ if abs(len(word_ending) - len(expected_ending)) <= 2:
103
+ return info['canonical']
104
+ return None
105
 
106
+
107
+ import os
108
+ current_dir = os.path.dirname(os.path.abspath(__file__))
109
+ vocab_file = os.path.join(current_dir, 'vocabulary.md')
110
+ VOCABULARY_MANAGER = ParserVocabulary(vocab_file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
 
113
  if __name__ == '__main__':
114
+ print(VOCABULARY_MANAGER.vocabulary)
115
+ print(VOCABULARY_MANAGER.stems)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vocabulary/vocabulary.md CHANGED
@@ -115,13 +115,13 @@
115
  Троллейбусная конечная рядом с заводом цветных металлов,
116
 
117
  ## Модели автобусов и троллейбусов
118
- ### Икарус-180
119
  Автобус особо большого класса (с гармошкой).
120
 
121
- ### Икарус-260, Икарус 260
122
  Автобус производства венгерской фирмы Ikarus второго поколения.
123
 
124
- ### Икарус-280, Икарус 280, Икарус
125
  Автобус особо большого класса (с гармошкой), долгое время эксплуатировавшийся в Рязани и ставший символом рязанского автобуса.
126
 
127
  ### ЛиАЗ[]-5292, ЛиАЗ[]
 
115
  Троллейбусная конечная рядом с заводом цветных металлов,
116
 
117
  ## Модели автобусов и троллейбусов
118
+ ### Икарус[]-180
119
  Автобус особо большого класса (с гармошкой).
120
 
121
+ ### Икарус[]-260, Икарус[] 260
122
  Автобус производства венгерской фирмы Ikarus второго поколения.
123
 
124
+ ### Икарус[]-280, Икарус[] 280, Икарус[]
125
  Автобус особо большого класса (с гармошкой), долгое время эксплуатировавшийся в Рязани и ставший символом рязанского автобуса.
126
 
127
  ### ЛиАЗ[]-5292, ЛиАЗ[]
vocabulary/vocabulary_manager.py DELETED
@@ -1,129 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Парсер и менеджер для vocabulary.md
4
- Загружает категории, термины и синонимы для использования в NER и поиске.
5
- """
6
- from pathlib import Path
7
- from typing import Dict, List, Tuple, Set
8
-
9
-
10
- class VocabularyManager:
11
- """Загружает и управляет словарём из vocabulary.md"""
12
-
13
- def __init__(self, vocab_file: Path):
14
- self.vocab_file = vocab_file
15
- self.categories = {} # {category: [(term, [synonyms]), ...]}
16
- self.term_to_category = {} # {term_lower: category}
17
- self.all_terms = set() # Все термины и синонимы
18
- self.synonyms = {} # {term_lower: canonical_term_lower}
19
-
20
- self._parse_vocabulary()
21
-
22
- def _parse_vocabulary(self):
23
- """Парсит файл vocabulary.md"""
24
- if not self.vocab_file.exists():
25
- print(f"⚠️ Файл {self.vocab_file} не найден")
26
- return
27
-
28
- with open(self.vocab_file, 'r', encoding='utf-8') as f:
29
- lines = f.readlines()
30
-
31
- current_category = None
32
-
33
- for line in lines:
34
- line = line.rstrip('\n')
35
-
36
- # Категория (## Название)
37
- if line.startswith('## '):
38
- current_category = line[3:].strip()
39
- self.categories[current_category] = []
40
- continue
41
-
42
- # Термин (### название[окончание], синоним1[окончание], синоним2, ...)
43
- if line.startswith('### '):
44
- if current_category is None:
45
- continue
46
-
47
- term_line = line[4:].strip()
48
-
49
- # Парсим термины и синонимы (разделены запятыми)
50
- terms_raw = [t.strip() for t in term_line.split(',')]
51
-
52
- # Убираем квадратные скобки из всех терминов
53
- terms = [term_raw.replace('[', '').replace(']', '') for term_raw in terms_raw if term_raw.strip()]
54
-
55
- canonical_term = terms[0] # Первый термин - канонический
56
- synonyms = terms[1:] if len(terms) > 1 else []
57
-
58
- # Добавляем в категорию
59
- self.categories[current_category].append((canonical_term, synonyms))
60
-
61
- # Регистрируем все формы
62
- self.term_to_category[canonical_term.lower()] = current_category
63
- self.all_terms.add(canonical_term.lower())
64
-
65
- for synonym in synonyms:
66
- self.term_to_category[synonym.lower()] = current_category
67
- self.all_terms.add(synonym.lower())
68
- # Синонимы указывают на канонический термин
69
- self.synonyms[synonym.lower()] = canonical_term.lower()
70
-
71
- print(f"✓ Загружено из vocabulary.md:")
72
- print(f" - {len(self.categories)} категорий")
73
- print(f" - {sum(len(terms) for terms in self.categories.values())} термин(ов)")
74
- print(f" - {len(self.all_terms)} уникальных форм (с синонимами)")
75
-
76
- def get_category(self, term: str) -> str:
77
- """Возвращает категорию термина или None"""
78
- return self.term_to_category.get(term.lower())
79
-
80
- def get_canonical_form(self, term: str) -> str:
81
- """Возвращает канонический вид термина (если это синоним, или сам термин)"""
82
- term_lower = term.lower()
83
- if term_lower in self.synonyms:
84
- return self.synonyms[term_lower]
85
- return term_lower
86
-
87
- def is_known_term(self, term: str) -> bool:
88
- """Проверяет, есть ли это слово в словаре"""
89
- return term.lower() in self.all_terms
90
-
91
- def get_all_forms(self, canonical_term: str) -> Set[str]:
92
- """Возвращает все формы (синонимы) канонического термина"""
93
- forms = {canonical_term.lower()}
94
- for syn_form, syn_canonical in self.synonyms.items():
95
- if syn_canonical == canonical_term.lower():
96
- forms.add(syn_form)
97
- return forms
98
-
99
- def get_patterns_for_spacy(self) -> List[Tuple[str, str, List[Dict]]]:
100
- """
101
- Возвращает паттерны для spaCy EntityRuler.
102
- Формат: [(label, id, [patterns]), ...]
103
- """
104
- patterns = []
105
-
106
- for category, terms in self.categories.items():
107
- # Преобразуем категорию в метку (CATEGORY)
108
- label = category.upper().replace(' ', '_')
109
-
110
- for i, (canonical_term, synonyms) in enumerate(terms):
111
- # ID = category_index
112
- entity_id = f"{category.lower().replace(' ', '_')}_{i}"
113
-
114
- # Все формы (канонический + синонимы)
115
- all_forms = [canonical_term] + synonyms
116
-
117
- # Создаём паттерны для каждой формы
118
- entity_patterns = []
119
- for form in all_forms:
120
- # Простой паттерн - точное совпадение текста
121
- entity_patterns.append({"text": form})
122
- # Вариант с разными регистрами
123
- entity_patterns.append({"text": form.lower()})
124
- if form[0].isupper():
125
- entity_patterns.append({"text": form.capitalize()})
126
-
127
- patterns.append((label, entity_id, entity_patterns))
128
-
129
- return patterns