Files
LawBot/protocol/generate_protocol_docx.py
2026-05-25 01:12:43 +03:00

783 lines
52 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
from pathlib import Path
OUTPUT_PATH = Path("protocol/Протокол_курсовой_работы_LawBot.docx")
BLACK = RGBColor(0, 0, 0)
def apply_run_format(run, *, bold: bool | None = None, font_size: int | None = None) -> None:
if bold is not None:
run.bold = bold
if font_size is not None:
run.font.size = Pt(font_size)
run.font.name = "Times New Roman"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
run.font.color.rgb = BLACK
run.font.underline = False
def set_cell_text(cell, text: str, align=WD_ALIGN_PARAGRAPH.CENTER, bold: bool = False, font_size=12):
cell.text = ""
paragraph = cell.paragraphs[0]
paragraph.alignment = align
run = paragraph.add_run(text)
apply_run_format(run, bold=bold, font_size=font_size)
def configure_document(document: Document) -> None:
section = document.sections[0]
section.page_width = Cm(21)
section.page_height = Cm(29.7)
section.top_margin = Cm(2)
section.bottom_margin = Cm(2)
section.left_margin = Cm(3)
section.right_margin = Cm(1.5)
section.different_first_page_header_footer = True
normal = document.styles["Normal"]
normal.font.name = "Times New Roman"
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
normal.font.size = Pt(14)
normal.font.color.rgb = BLACK
normal.paragraph_format.first_line_indent = Cm(1.25)
normal.paragraph_format.line_spacing = 1.5
normal.paragraph_format.space_after = Pt(0)
normal.paragraph_format.space_before = Pt(0)
for style_name in ["Heading 1", "Heading 2", "Heading 3"]:
style = document.styles[style_name]
style.font.name = "Times New Roman"
style._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
style.font.size = Pt(14)
style.font.bold = True
style.font.color.rgb = BLACK
style.font.underline = False
style.paragraph_format.first_line_indent = Cm(0)
style.paragraph_format.line_spacing = 1.5
style.paragraph_format.space_before = Pt(12)
style.paragraph_format.space_after = Pt(6)
footer = section.footer
paragraph = footer.paragraphs[0]
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
add_page_number_field(paragraph)
def add_page_number_field(paragraph) -> None:
run = paragraph.add_run()
fld_begin = OxmlElement("w:fldChar")
fld_begin.set(qn("w:fldCharType"), "begin")
instr_text = OxmlElement("w:instrText")
instr_text.set(qn("xml:space"), "preserve")
instr_text.text = " PAGE "
fld_separate = OxmlElement("w:fldChar")
fld_separate.set(qn("w:fldCharType"), "separate")
fld_end = OxmlElement("w:fldChar")
fld_end.set(qn("w:fldCharType"), "end")
run._r.append(fld_begin)
run._r.append(instr_text)
run._r.append(fld_separate)
run._r.append(fld_end)
def add_toc(paragraph) -> None:
run = paragraph.add_run()
fld_begin = OxmlElement("w:fldChar")
fld_begin.set(qn("w:fldCharType"), "begin")
instr_text = OxmlElement("w:instrText")
instr_text.set(qn("xml:space"), "preserve")
instr_text.text = ' TOC \\o "1-3" \\h \\z \\u '
fld_separate = OxmlElement("w:fldChar")
fld_separate.set(qn("w:fldCharType"), "separate")
placeholder = OxmlElement("w:t")
placeholder.text = "Обновите поле содержания в Microsoft Word (ПКМ -> Обновить поле)."
fld_end = OxmlElement("w:fldChar")
fld_end.set(qn("w:fldCharType"), "end")
run._r.append(fld_begin)
run._r.append(instr_text)
run._r.append(fld_separate)
run._r.append(placeholder)
run._r.append(fld_end)
def add_title_page(document: Document) -> None:
p = document.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
apply_run_format(p.add_run("МИНОБРНАУКИ РОССИИ"), bold=True, font_size=14)
for line in [
"Федеральное государственное бюджетное образовательное учреждение",
"высшего образования",
"_______________________________",
"_______________________________",
]:
paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = paragraph.add_run(line)
apply_run_format(run, font_size=14)
document.add_paragraph()
document.add_paragraph()
document.add_paragraph()
p = document.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run("ПРОТОКОЛ КУРСОВОЙ РАБОТЫ")
apply_run_format(run, bold=True, font_size=16)
p = document.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(
"на тему «Разработка Telegram-бота для юридических консультаций\n"
"на основе технологии Retrieval-Augmented Generation»"
)
apply_run_format(run, font_size=14)
document.add_paragraph()
document.add_paragraph()
table = document.add_table(rows=4, cols=2)
table.alignment = WD_TABLE_ALIGNMENT.RIGHT
table.columns[0].width = Cm(5.5)
table.columns[1].width = Cm(7.5)
rows = [
("Выполнил:", "студент группы ____________"),
("Ф.И.О.:", "__________________________"),
("Руководитель:", "__________________________"),
("Дата:", "__________________________"),
]
for row, (left, right) in zip(table.rows, rows):
set_cell_text(row.cells[0], left, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, font_size=14)
set_cell_text(row.cells[1], right, align=WD_ALIGN_PARAGRAPH.LEFT, bold=False, font_size=14)
document.add_paragraph()
document.add_paragraph()
document.add_paragraph()
document.add_paragraph()
document.add_paragraph()
p = document.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
apply_run_format(p.add_run("Волгоград, 2026"), font_size=14)
document.add_page_break()
def add_heading(document: Document, text: str, level: int = 1) -> None:
paragraph = document.add_heading(text, level=level)
paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
for run in paragraph.runs:
apply_run_format(run, bold=True, font_size=14)
def add_paragraph(document: Document, text: str, align=WD_ALIGN_PARAGRAPH.JUSTIFY) -> None:
paragraph = document.add_paragraph(text)
paragraph.alignment = align
def add_list(document: Document, items: list[str]) -> None:
for index, item in enumerate(items, start=1):
paragraph = document.add_paragraph(f"{index}. {item}")
paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
paragraph.paragraph_format.first_line_indent = Cm(0)
paragraph.paragraph_format.left_indent = Cm(1.25)
def add_figure_placeholder(document: Document, number: int, title: str, marker: str) -> None:
p = document.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(f"[МЕСТО ДЛЯ {marker.upper()}]")
apply_run_format(run, bold=True, font_size=14)
caption = document.add_paragraph()
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = caption.add_run(f"Рисунок {number} {title}")
apply_run_format(run, font_size=14)
def add_table_with_caption(
document: Document,
number: int,
title: str,
headers: list[str],
rows: list[list[str]],
) -> None:
caption = document.add_paragraph()
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = caption.add_run(f"Таблица {number} {title}")
apply_run_format(run, font_size=14)
table = document.add_table(rows=len(rows) + 1, cols=len(headers))
table.style = "Table Grid"
table.alignment = WD_TABLE_ALIGNMENT.CENTER
for cell, header in zip(table.rows[0].cells, headers):
set_cell_text(cell, header, bold=True)
for row_obj, row_data in zip(table.rows[1:], rows):
for cell, value in zip(row_obj.cells, row_data):
set_cell_text(cell, value, align=WD_ALIGN_PARAGRAPH.LEFT)
document.add_paragraph()
def build_document() -> None:
document = Document()
configure_document(document)
add_title_page(document)
add_heading(document, "СОДЕРЖАНИЕ", level=1)
toc_paragraph = document.add_paragraph()
toc_paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
add_toc(toc_paragraph)
document.add_page_break()
add_heading(document, "ВВЕДЕНИЕ", level=1)
add_paragraph(
document,
"Развитие цифровых сервисов и широкое распространение генеративных моделей "
"искусственного интеллекта создают предпосылки для появления специализированных "
"информационных систем, способных предоставлять пользователю понятные и быстрые "
"консультации в узких предметных областях. Одной из таких областей является "
"юриспруденция, где особенно важны актуальность источников, проверяемость ответа "
"и возможность сослаться на конкретную норму права.",
)
add_paragraph(
document,
"В рамках курсовой работы разработан Telegram-бот LawBot, предназначенный для "
"консультирования пользователей по бытовым юридическим вопросам в правовом поле "
"Российской Федерации. Система реализует подход Retrieval-Augmented Generation: "
"пользовательский запрос классифицируется, затем по локальной базе нормативных "
"документов выполняется гибридный поиск, после чего большая языковая модель "
"формирует ответ только по найденным источникам. Такой подход позволяет объединить "
"преимущества генеративных моделей и контролируемого поиска по локальному корпусу.",
)
add_paragraph(
document,
"Объектом исследования является процесс автоматизации первичной юридической "
"консультации в Telegram. Предметом исследования являются методы проектирования "
"чат-ботов, механизмы гибридного поиска по правовым документам и способы "
"организации RAG-контура для прикладной юридической системы.",
)
add_paragraph(
document,
"Цель работы состоит в повышении доступности правовой информации для пользователя "
"за счет разработки Telegram-бота, который принимает вопрос, подбирает релевантные "
"нормы закона и объясняет их простым языком с сохранением ссылок на источники.",
)
add_paragraph(document, "Для достижения поставленной цели решены следующие задачи:")
add_list(
document,
[
"проведен анализ предметной области и существующих решений в области юридических чат-ботов;",
"спроектированы сценарии использования, функциональные требования и архитектура системы;",
"реализованы Telegram-бот, парсер нормативных документов, общий слой доступа к данным и RAG API;",
"выполнено тестирование разработанного программного продукта.",
],
)
add_paragraph(
document,
"Работа состоит из четырех глав, заключения и списка использованных источников. "
"В документе содержатся 10 рисунков и 4 таблицы. Первая глава посвящена анализу "
"предметной области и подходов к разработке чат-ботов, во второй главе рассмотрены "
"вопросы проектирования, в третьей главе описана реализация, а в четвертой приведены "
"результаты тестирования.",
)
add_heading(document, "1 АНАЛИЗ ПРЕДМЕТНОЙ ОБЛАСТИ", level=1)
add_heading(document, "1.1 Характеристика предметной области", level=2)
add_paragraph(
document,
"Предметной областью проекта является автоматизированная первичная юридическая "
"консультация по законодательству Российской Федерации. В отличие от развлекательных "
"или сервисных чат-ботов, юридический бот должен обеспечивать не только корректный "
"диалог, но и высокую точность подбора правовой нормы, прозрачность происхождения "
"ответа и сохранение истории взаимодействия. Ошибочный ответ в данной области может "
"привести пользователя к неверным действиям, поэтому в системе сделан акцент на "
"локальный корпус документов и обязательную ссылку на найденный источник.",
)
add_paragraph(
document,
"Для MVP-версии были выбраны наиболее частые бытовые категории вопросов: трудовые "
"отношения, защита прав потребителей, жилье и аренда, семейные споры, долги и займы, "
"договорные отношения, вопросы суда и процесса, а также обобщенная категория "
"«Другое». В качестве документной базы используются федеральные нормативные акты: "
"Конституция Российской Федерации, Гражданский кодекс РФ (части 1–4), Гражданский "
"процессуальный кодекс РФ, Жилищный кодекс РФ, Семейный кодекс РФ, Трудовой кодекс РФ, "
"Закон РФ «О защите прав потребителей», Федеральный закон «Об исполнительном "
"производстве», Федеральный закон «Об ипотеке» и КоАП РФ. Источником для первичного "
"получения текстов выбран раздел популярных документов на сайте КонсультантПлюс [1].",
)
add_paragraph(
document,
"Основная проблема пользователя в такой системе заключается в том, что ему сложно "
"самостоятельно определить нужный нормативный акт, статью и актуальную редакцию "
"документа. Кроме того, нормативный язык зачастую сложен для восприятия. Следовательно, "
"бот должен решать сразу две задачи: находить релевантные правовые фрагменты и "
"объяснять их доступным языком, не выходя за пределы найденных источников.",
)
add_heading(document, "1.2 Анализ существующих решений", level=2)
add_paragraph(
document,
"В области юридических информационных сервисов можно выделить несколько классов "
"решений: генеративные универсальные ИИ-ассистенты, rule-based FAQ-боты, "
"retrieval-ориентированные справочные системы и гибридные системы, совмещающие "
"поиск и генерацию. На практике каждое решение имеет собственные преимущества и "
"ограничения.",
)
add_table_with_caption(
document,
1,
"Сравнение существующих решений и подходов",
["Решение", "Подход", "Преимущества", "Ограничения"],
[
[
"ChatGPT, GigaChat и аналогичные LLM-ассистенты",
"generation-based",
"Естественный диалог, гибкость формулировок, хорошие пояснения",
"Без внешнего поиска возможны галлюцинации и отсутствие точных ссылок на нормы",
],
[
"FAQ-боты государственных и коммерческих сервисов",
"rule-based",
"Предсказуемость ответов, простая проверяемость сценариев",
"Ограниченный охват, слабая работа со свободным текстом",
],
[
"Справочно-правовые системы",
"retrieval-based",
"Высокая полнота поиска по нормативным актам, удобная работа с документами",
"Требуют самостоятельного анализа текста пользователем, не ориентированы на диалог в Telegram",
],
[
"Гибридные RAG-системы",
"hybrid",
"Сочетают поиск по корпусу и генерацию понятного ответа",
"Сложнее в проектировании, требуют отдельного контура индексации и контроля качества",
],
],
)
add_paragraph(
document,
"Для юридической области pure generation-подход является недостаточно надежным, "
"поскольку модель может предложить правдоподобный, но не подтвержденный нормой "
"закона ответ. Чисто rule-based подход, напротив, слишком ограничен и плохо подходит "
"для свободно сформулированных пользовательских ситуаций. Справочно-правовые системы "
"эффективны как поиск по документам, однако требуют от пользователя самостоятельного "
"изучения и интерпретации результатов. По этим причинам наиболее рациональным для "
"поставленной задачи является гибридный RAG-подход.",
)
add_heading(document, "1.3 Обоснование выбранного подхода", level=2)
add_paragraph(
document,
"В проекте выбран гибридный сценарий, в котором генеративная модель используется для "
"классификации запроса, генерации поисковых фраз и финального формулирования ответа, "
"а поиск выполняется по локальной базе нормативных актов. На первом этапе пользовательский "
"вопрос дополняется контекстом категории, региона и типа пользователя. На втором этапе "
"LLM формирует несколько поисковых фраз и предварительные фильтры. На третьем этапе "
"система выполняет full-text поиск в PostgreSQL и vector search в ChromaDB, объединяет "
"результаты и отбирает наиболее релевантные фрагменты. Финальный ответ формируется "
"исключительно по этим фрагментам.",
)
add_paragraph(
document,
"Выбранная схема обеспечивает компромисс между качеством диалога и контролем над "
"источниками. Она хорошо масштабируется: можно добавлять новые документы, повторно "
"индексировать корпус, улучшать embedding-модель, не меняя пользовательский интерфейс. "
"Именно поэтому данный подход выбран как основа архитектуры LawBot.",
)
add_heading(document, "2 ПРОЕКТИРОВАНИЕ ЧАТ-БОТА", level=1)
add_heading(document, "2.1 Сценарии использования системы", level=2)
add_paragraph(
document,
"Основным актором системы является пользователь Telegram, который может начать новую "
"консультацию, выбрать категорию вопроса, описать ситуацию, указать регион, получить "
"ответ с найденными источниками и сохранить консультацию в историю. Дополнительным "
"актором является администратор, имеющий доступ к административным функциям управления. "
"Также в архитектуре присутствуют внешние сервисы: источник нормативных документов, "
"LLM-провайдер и сервисы хранения.",
)
add_figure_placeholder(
document,
1,
"Диаграмма вариантов использования Telegram-бота LawBot",
"диаграммы use case",
)
add_paragraph(
document,
"Ключевой пользовательский сценарий включает последовательность «выбор категории – "
"описание ситуации – указание региона – поиск по базе – выдача ответа – сохранение "
"в историю». Отдельно поддерживается сценарий продолжения ранее начатой консультации, "
"в котором система учитывает краткую историю сообщений и использует ее как дополнительный "
"контекст для генерации ответа.",
)
add_heading(document, "2.2 Функциональные требования", level=2)
add_table_with_caption(
document,
2,
"Ключевые функциональные требования к системе",
["Группа требований", "Содержание"],
[
[
"Пользовательский интерфейс",
"Система должна поддерживать команды /start, главное меню, категории вопросов, профиль и историю консультаций.",
],
[
"Юридический поиск",
"Система должна выполнять гибридный поиск по локальному корпусу правовых документов и учитывать категорию, регион и тип пользователя.",
],
[
"Формирование ответа",
"Система должна формировать ответ простым языком только по найденным источникам и отображать ссылки на используемые нормы.",
],
[
"Управление данными",
"Система должна хранить пользователей, консультации, сообщения, список документов, чанки и историю RAG-запросов.",
],
[
"Подготовка корпуса",
"Система должна скачивать, нормализовать и загружать тексты законов с сохранением версионности и исходного HTML.",
],
],
)
add_figure_placeholder(
document,
2,
"Диаграмма требований к системе LawBot",
"диаграммы requirements",
)
add_paragraph(
document,
"Сценарии и требования были согласованы с задачами курсовой работы. Особое внимание "
"уделено функции контроля источников: бот не должен ссылаться на правовые нормы, которых "
"нет среди найденных RAG-фрагментов. Это требование является критически важным для "
"юридической предметной области.",
)
add_heading(document, "2.3 Архитектура решения", level=2)
add_paragraph(
document,
"Архитектура системы состоит из пяти логических подсистем: Telegram-бота, парсера, "
"общего слоя доступа к базе данных, RAG API и подсистем хранения. Telegram-бот "
"реализован на aiogram [2] и отвечает за пользовательский диалог. Парсер выделен в "
"отдельный CLI-модуль, который подготавливает нормативный корпус и сохраняет его в "
"PostgreSQL [5]. FastAPI-сервис [3] выполняет индексацию в ChromaDB [6], гибридный "
"поиск, классификацию запроса и формирование ответа. Общие модели и репозитории "
"вынесены в пакет shared, чтобы разные сервисы работали с единым описанием данных.",
)
add_figure_placeholder(
document,
3,
"Компонентная диаграмма системы LawBot",
"компонентной диаграммы",
)
add_paragraph(
document,
"Физическое развертывание выполняется в Docker Compose [10]. В составе стенда работают "
"контейнеры tgbot, api, postgredb, redisdb и chromadb. Для каждого сервиса задан healthcheck, "
"а для логов используется драйвер json-file с ротацией. Для embedding-модели дополнительно "
"прокинут volume, чтобы модель не загружалась заново после каждого перезапуска.",
)
add_figure_placeholder(
document,
4,
"Схема развертывания контейнеров в Docker Compose",
"схемы развертывания",
)
add_heading(document, "2.4 Проектирование хранилища данных", level=2)
add_paragraph(
document,
"Для хранения структурированных данных выбрана реляционная СУБД PostgreSQL [5]. "
"В системе используются таблицы users, consultations, messages, law_sources, law_chunks "
"и rag_queries. Таблица users хранит профиль пользователя Telegram, consultations и messages "
"описывают историю взаимодействия, law_sources и law_chunks содержат нормативные документы "
"и их фрагменты, а rag_queries фиксирует сгенерированные поисковые фразы и найденные чанки. "
"Для полнотекстового поиска в PostgreSQL используется поле tsv с русской морфологией.",
)
add_paragraph(
document,
"Векторный индекс вынесен в ChromaDB [6]. Такое разделение позволяет хранить канонический "
"текст документов в PostgreSQL и независимо перестраивать embedding-индекс, не дублируя "
"основную бизнес-логику. Для поддержки будущих сценариев поиска по истории консультаций "
"и аудита источников все сообщения и RAG-запросы также сохраняются в основной базе данных.",
)
add_figure_placeholder(
document,
5,
"ER-диаграмма и логическая модель хранения данных LawBot",
"er-диаграммы",
)
add_heading(document, "3 РЕАЛИЗАЦИЯ ЧАТ-БОТА", level=1)
add_heading(document, "3.1 Используемые технологии и среда разработки", level=2)
add_table_with_caption(
document,
3,
"Основные технологии проекта",
["Технология", "Назначение"],
[
["Python 3.12/3.13", "Основной язык разработки сервисов"],
["aiogram 3.x [2]", "Telegram-бот и обработчики пользовательских состояний"],
["FastAPI [3]", "REST API для RAG-контура и индексации"],
["SQLAlchemy 2.x [4]", "ORM и общий слой доступа к базе данных"],
["PostgreSQL 16 [5]", "Хранение пользователей, истории консультаций и правовых документов"],
["ChromaDB [6]", "Векторная база данных для семантического поиска по чанкам"],
["Sentence Transformers [7]", "Локальная CPU-модель эмбеддингов deepvk/USER2-small [9]"],
["OpenRouter [8]", "OpenAI-compatible доступ к LLM openai/gpt-5.2 для классификации и генерации ответа"],
["Docker Compose [10]", "Контейнеризация и оркестрация сервисов"],
["Redis [11]", "Хранилище состояний FSM и служебных данных бота"],
],
)
add_paragraph(
document,
"Разработка выполнялась в виде многомодульного проекта. Для бота, API и парсера выбрана "
"единая рабочая директория с общим пакетом shared. Такое решение позволило использовать "
"одни и те же ORM-модели и репозитории как из Telegram-сервиса, так и из API и парсера, "
"не дублируя схему данных.",
)
add_heading(document, "3.2 Реализация парсера нормативных документов", level=2)
add_paragraph(
document,
"Парсер реализован как самостоятельный CLI-модуль parser. Он поддерживает команды "
"discover, fetch, normalize, ingest и run. Команда discover анализирует страницу "
"популярных правовых подборок на сайте КонсультантПлюс [1], формирует manifest и "
"определяет набор документов для загрузки. Команда fetch скачивает HTML-документы и "
"сохраняет их в raw-кеш. Команда normalize выделяет метаданные, статьи и внутреннюю "
"структуру документа, после чего сохраняет нормализованный JSON. Команда ingest "
"загружает полученные данные в PostgreSQL.",
)
add_paragraph(
document,
"Для каждого документа рассчитывается version_hash, что позволяет повторно запускать "
"пайплайн без создания лишних дублей. Документы сохраняются как сущности law_sources, "
"а статьи и их фрагменты – как law_chunks. В рамках MVP был успешно загружен набор из "
"13 правовых источников и 5304 чанков, что покрывает ключевые бытовые юридические "
"сценарии проекта.",
)
add_paragraph(
document,
"Сохранение исходного HTML и normalized JSON делает парсер пригодным для последующей "
"диагностики, повторной индексации и возможной смены механизма chunking без повторной "
"загрузки всего корпуса.",
)
add_heading(document, "3.3 Реализация Telegram-бота", level=2)
add_paragraph(
document,
"Пользовательский интерфейс реализован в сервисе tgbot на базе aiogram [2]. Бот "
"поддерживает главное меню, разделы «Задать вопрос», «Мои консультации», «Профиль» "
"и «Помощь», а также административные разделы. Логика диалога построена на FSM, "
"где отдельные состояния отвечают за выбор категории, ввод вопроса, ввод региона, "
"продолжение консультации и редактирование профиля. Для хранения состояния используется "
"Redis [11].",
)
add_paragraph(
document,
"В момент получения вопроса бот обращается к RAG API через отдельный модуль на базе "
"httpx. Запрос включает идентификатор пользователя, категорию вопроса, регион, тип "
"пользователя и при необходимости краткую историю предыдущих сообщений. После получения "
"ответа бот отображает его пользователю и сохраняет возможность открыть или продолжить "
"консультацию. Также реализованы суточные лимиты на число консультаций и ограничение "
"на количество пользовательских сообщений в одной ветке консультации.",
)
add_figure_placeholder(
document,
6,
"Главное меню Telegram-бота LawBot",
"скриншота интерфейса",
)
add_figure_placeholder(
document,
7,
"Пример ответа бота с найденными источниками",
"скриншота диалога",
)
add_heading(document, "3.4 Реализация RAG API и гибридного поиска", level=2)
add_paragraph(
document,
"RAG API реализован как отдельный FastAPI-сервис [3]. Он содержит эндпоинты /health, "
"/api/v1/index/rebuild, /api/v1/rag/search и /api/v1/rag/answer. Во время старта сервиса "
"инициализируется схема базы данных и запускается фоновая задача автоиндексации: если "
"коллекция ChromaDB пуста или рассинхронизирована с PostgreSQL, сервис автоматически "
"запускает rebuild индекса в фоне и детально логирует все этапы процесса.",
)
add_paragraph(
document,
"Алгоритм retrieval включает несколько фаз. Сначала LLM классифицирует вопрос и генерирует "
"набор search_queries. Затем для каждой поисковой фразы выполняется полнотекстовый поиск "
"по PostgreSQL и векторный поиск по ChromaDB. Результаты объединяются в общую таблицу "
"оценок, сортируются и отбираются по верхнему порогу top_k. Такой механизм позволяет "
"учитывать как точные совпадения ключевых слов, так и семантическую близость запроса "
"и текста нормы.",
)
add_paragraph(
document,
"Векторные представления создаются локальной CPU-моделью deepvk/USER2-small [9] через "
"библиотеку Sentence Transformers [7]. Для LLM-интеграции используется OpenRouter [8], "
"через который подключена модель openai/gpt-5.2 по OpenAI-compatible интерфейсу. "
"В промпте генерации ответа жёстко задано правило: использовать только те источники, "
"которые были возвращены retrieval-контуром.",
)
add_figure_placeholder(
document,
8,
"Фоновая индексация чанков и продовые логи RAG API",
"скриншота логов",
)
add_heading(document, "3.5 Надежность и эксплуатационные особенности", level=2)
add_paragraph(
document,
"Для повышения надежности проект контейнеризирован. В compose-конфигурации настроены "
"healthcheck-проверки для PostgreSQL, Redis и API, а также ротация логов через driver "
"json-file. Для embedding-модели настроен отдельный volume, благодаря которому модель "
"не скачивается повторно при каждом перезапуске. Для Telegram Bot API предусмотрена "
"поддержка proxy, параметризуемая через переменную окружения.",
)
add_paragraph(
document,
"Сервис API ведет расширенные продовые логи: фиксируются старт и завершение HTTP-запросов, "
"загрузка embedding-модели, шаги индексации, число найденных полнотекстовых и векторных "
"совпадений, а также параметры классификации и генерации ответа. Такая детализация "
"позволяет анализировать производительность и находить проблемные участки в retrieval-пайплайне.",
)
add_heading(document, "4 ТЕСТИРОВАНИЕ ЧАТ-БОТА", level=1)
add_heading(document, "4.1 Методика тестирования", level=2)
add_paragraph(
document,
"Тестирование проекта выполнялось как на уровне отдельных сервисов, так и на уровне "
"сквозного пользовательского сценария. Проверялись корректность парсинга документов, "
"загрузка данных в PostgreSQL, построение индекса в ChromaDB, маршрутизация запросов "
"через RAG API и реакция Telegram-бота на действия пользователя. Дополнительно были "
"проверены фоновые механизмы: автоиндексация при старте API, ротация логов и сохранение "
"кеша embedding-модели на volume.",
)
add_table_with_caption(
document,
4,
"Основные тестовые сценарии",
["", "Сценарий", "Ожидаемый результат", "Фактический результат"],
[
["1", "Команда /start", "Открывается главное меню бота", "Успешно"],
["2", "Новая консультация", "Пользователь проходит шаги категория -> вопрос -> регион -> ответ", "Успешно"],
["3", "RAG answer", "API возвращает ответ со ссылками на найденные источники", "Успешно"],
["4", "История консультаций", "Пользователь может открыть, продолжить и удалить консультацию", "Успешно"],
["5", "Перестроение индекса", "API индексирует чанки в ChromaDB без ручного вмешательства", "Успешно"],
["6", "Повторный запуск API", "Модель эмбеддингов берется из локального volume, скачивание не повторяется", "Успешно"],
],
)
add_heading(document, "4.2 Результаты тестирования", level=2)
add_paragraph(
document,
"В ходе функционального тестирования был выполнен полный пайплайн parser -> PostgreSQL -> "
"ChromaDB -> RAG API -> Telegram-бот. После запуска parser run в базе данных присутствовали "
"13 активных источников и 5304 чанка, что подтвердило корректность загрузки нормативного корпуса. "
"Затем был выполнен rebuild индекса и проверены поисковые запросы по темам трудового права, "
"защиты прав потребителей, жилья и договоров. Система корректно возвращала подборку правовых "
"норм и формировала итоговый ответ по найденным фрагментам.",
)
add_paragraph(
document,
"При пользовательском тестировании проверялись сценарии работы с главным меню, "
"оформление новой консультации, сохранение истории, изменение региона и типа пользователя, "
"а также повторное продолжение старой консультации. Все ключевые сценарии были отработаны "
"успешно, критических ошибок, приводящих к потере данных или невозможности получить ответ, "
"не обнаружено.",
)
add_figure_placeholder(
document,
9,
"Тестовый сценарий получения ответа по трудовому вопросу",
"скриншота тестирования",
)
add_figure_placeholder(
document,
10,
"Тестовый сценарий работы с историей консультаций",
"скриншота тестирования",
)
add_heading(document, "ЗАКЛЮЧЕНИЕ", level=1)
add_paragraph(
document,
"В результате выполнения курсовой работы был разработан Telegram-бот LawBot для "
"юридических консультаций по законодательству Российской Федерации. В ходе работы были "
"решены все поставленные задачи: проведен анализ предметной области, спроектированы "
"сценарии и архитектура системы, реализованы пользовательский бот, модуль подготовки "
"правового корпуса, общий слой работы с данными и RAG API, а также выполнено "
"тестирование полученного решения.",
)
add_paragraph(
document,
"Практическая значимость результата заключается в том, что пользователь получает "
"доступный интерфейс для первичной юридической консультации с опорой на локальный "
"корпус нормативных документов. В отличие от обычных генеративных ассистентов, "
"разработанная система контролирует происхождение ответа и ограничивает генерацию "
"только найденными источниками.",
)
add_paragraph(
document,
"В качестве направлений дальнейшего развития можно выделить расширение корпуса "
"документов, добавление автоматического обновления базы законов, подключение более "
"сильных reranking-моделей, поддержку загрузки пользовательских документов и развитие "
"аналитики по качеству retrieval и генерации ответов.",
)
add_heading(document, "СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ", level=1)
references = [
"КонсультантПлюс. Популярные документы и кодексы [Электронный ресурс]. URL: https://www.consultant.ru/popular/ (дата обращения: 24.05.2026).",
"aiogram 3 documentation [Электронный ресурс]. URL: https://docs.aiogram.dev/ (дата обращения: 24.05.2026).",
"FastAPI Documentation [Электронный ресурс]. URL: https://fastapi.tiangolo.com/ (дата обращения: 24.05.2026).",
"SQLAlchemy 2.0 Documentation [Электронный ресурс]. URL: https://docs.sqlalchemy.org/ (дата обращения: 24.05.2026).",
"PostgreSQL Documentation [Электронный ресурс]. URL: https://www.postgresql.org/docs/ (дата обращения: 24.05.2026).",
"Chroma Documentation [Электронный ресурс]. URL: https://docs.trychroma.com/ (дата обращения: 24.05.2026).",
"Sentence Transformers Documentation [Электронный ресурс]. URL: https://www.sbert.net/ (дата обращения: 24.05.2026).",
"OpenRouter Documentation [Электронный ресурс]. URL: https://openrouter.ai/docs (дата обращения: 24.05.2026).",
"Модель deepvk/USER2-small на Hugging Face [Электронный ресурс]. URL: https://huggingface.co/deepvk/USER2-small (дата обращения: 24.05.2026).",
"Docker Compose file reference [Электронный ресурс]. URL: https://docs.docker.com/reference/compose-file/ (дата обращения: 24.05.2026).",
"Redis Documentation [Электронный ресурс]. URL: https://redis.io/docs/ (дата обращения: 24.05.2026).",
"PlantUML language reference guide [Электронный ресурс]. URL: https://plantuml.com/ru/guide (дата обращения: 24.05.2026).",
]
for index, item in enumerate(references, start=1):
paragraph = document.add_paragraph()
paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
paragraph.paragraph_format.first_line_indent = Cm(0)
paragraph.paragraph_format.left_indent = Cm(0)
run = paragraph.add_run(f"{index}. {item}")
run.font.name = "Times New Roman"
run._element.rPr.rFonts.set(qn("w:eastAsia"), "Times New Roman")
run.font.size = Pt(14)
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
document.save(OUTPUT_PATH)
if __name__ == "__main__":
build_document()