Add new file
This commit is contained in:
180
core/functions/generate_document.py
Normal file
180
core/functions/generate_document.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Модуль генерации документа по шаблону.
|
||||||
|
Заполняет плейсхолдеры {{...}} на основе данных из базы знаний.
|
||||||
|
|
||||||
|
АДАПТАЦИЯ: теперь функция принимает промпт и конфиг как аргументы.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
from docx import Document
|
||||||
|
from core.functions.file_processor import send_file_to_user
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_document_from_template(
|
||||||
|
bot,
|
||||||
|
msg,
|
||||||
|
room_jid,
|
||||||
|
template_path,
|
||||||
|
template_name,
|
||||||
|
user_jid,
|
||||||
|
giga,
|
||||||
|
kb,
|
||||||
|
config,
|
||||||
|
prompt_text,
|
||||||
|
temperature=0.1,
|
||||||
|
allow_public=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Фоновая задача: парсит шаблон, ищет плейсхолдеры {{...}},
|
||||||
|
для каждого делает контекстный поиск в БЗ комнаты, генерирует значение через LLM,
|
||||||
|
заменяет в копии шаблона и отправляет готовый файл в комнату.
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
bot: экземпляр бота
|
||||||
|
msg: объект XMPP-сообщения
|
||||||
|
room_jid: JID комнаты
|
||||||
|
template_path: путь к шаблону
|
||||||
|
template_name: имя шаблона
|
||||||
|
user_jid: JID пользователя
|
||||||
|
giga: клиент GigaChat
|
||||||
|
kb: сервис базы знаний
|
||||||
|
config: объект конфигурации
|
||||||
|
prompt_text: содержимое промпта для генерации
|
||||||
|
temperature: температура генерации
|
||||||
|
allow_public: разрешено ли использовать общие знания
|
||||||
|
"""
|
||||||
|
logger.info(f"🚀 [ГЕНЕРАЦИЯ] Запущена для шаблона '{template_name}' в комнате {room_jid}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc = Document(template_path)
|
||||||
|
|
||||||
|
# Собираем все плейсхолдеры и их контекст
|
||||||
|
placeholders_info = {}
|
||||||
|
for paragraph in doc.paragraphs:
|
||||||
|
for match in re.finditer(r'\{\{([^}]+)\}\}', paragraph.text):
|
||||||
|
ph = match.group(1)
|
||||||
|
if ph not in placeholders_info:
|
||||||
|
placeholders_info[ph] = paragraph.text
|
||||||
|
|
||||||
|
for table in doc.tables:
|
||||||
|
for row in table.rows:
|
||||||
|
for cell in row.cells:
|
||||||
|
for match in re.finditer(r'\{\{([^}]+)\}\}', cell.text):
|
||||||
|
ph = match.group(1)
|
||||||
|
if ph not in placeholders_info:
|
||||||
|
placeholders_info[ph] = cell.text
|
||||||
|
|
||||||
|
placeholders = set(placeholders_info.keys())
|
||||||
|
logger.info(f"🔍 [ГЕНЕРАЦИЯ] Найдено плейсхолдеров: {len(placeholders)}")
|
||||||
|
|
||||||
|
if not placeholders:
|
||||||
|
reply = msg.reply(f"❌ В шаблоне '{template_name}' нет плейсхолдеров {{...}}.")
|
||||||
|
if reply:
|
||||||
|
reply.send()
|
||||||
|
return
|
||||||
|
|
||||||
|
generated = {}
|
||||||
|
for ph, surrounding in placeholders_info.items():
|
||||||
|
logger.info(f"📖 [ГЕНЕРАЦИЯ] Обработка плейсхолдера '{ph}'")
|
||||||
|
|
||||||
|
# Формируем поисковый запрос
|
||||||
|
search_query = f"{ph} {surrounding[:100]}"
|
||||||
|
context = await kb.find_relevant_info(search_query, user_jid, room_jid, top_k=20)
|
||||||
|
|
||||||
|
if not context and allow_public:
|
||||||
|
logger.info(f"⚠️ [ГЕНЕРАЦИЯ] Контекст для '{ph}' не найден, используем общие знания")
|
||||||
|
prompt = f"Ты — эксперт. Ответь на вопрос: {ph}. Дай краткое значение, без пояснений."
|
||||||
|
try:
|
||||||
|
response = await giga.chat(
|
||||||
|
history=[],
|
||||||
|
query=prompt,
|
||||||
|
system_prompt=None,
|
||||||
|
file_id=None,
|
||||||
|
temperature=temperature
|
||||||
|
)
|
||||||
|
generated[ph] = response.strip()
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ [ГЕНЕРАЦИЯ] Ошибка LLM для '{ph}': {e}")
|
||||||
|
generated[ph] = "[ошибка]"
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not context:
|
||||||
|
logger.warning(f"⚠️ [ГЕНЕРАЦИЯ] Нет контекста для '{ph}', и общие знания запрещены")
|
||||||
|
generated[ph] = "не определено"
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Генерация значения на основе контекста
|
||||||
|
prompt = prompt_text.format(ph=ph, context=context)
|
||||||
|
try:
|
||||||
|
response = await giga.chat(
|
||||||
|
history=[],
|
||||||
|
query=prompt,
|
||||||
|
system_prompt=None,
|
||||||
|
file_id=None,
|
||||||
|
temperature=temperature
|
||||||
|
)
|
||||||
|
cleaned = re.sub(r'^```\w*\n?|```$', '', response.strip())
|
||||||
|
generated[ph] = cleaned
|
||||||
|
logger.info(f"✅ [ГЕНЕРАЦИЯ] Значение для '{ph}': {generated[ph][:100]}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ [ГЕНЕРАЦИЯ] Ошибка LLM для '{ph}': {e}", exc_info=True)
|
||||||
|
generated[ph] = "[ошибка]"
|
||||||
|
|
||||||
|
# Замена плейсхолдеров в копии документа
|
||||||
|
temp_dir = config.temp_dir
|
||||||
|
new_path = temp_dir / f"generated_{template_name}_{int(asyncio.get_event_loop().time())}.docx"
|
||||||
|
shutil.copy2(template_path, new_path)
|
||||||
|
|
||||||
|
new_doc = Document(new_path)
|
||||||
|
|
||||||
|
def replace_in_paragraph(paragraph, replacements):
|
||||||
|
full_text = ''.join(run.text for run in paragraph.runs)
|
||||||
|
modified = False
|
||||||
|
for key, value in replacements.items():
|
||||||
|
placeholder = f"{{{{{key}}}}}"
|
||||||
|
if placeholder in full_text:
|
||||||
|
for run in paragraph.runs:
|
||||||
|
if placeholder in run.text:
|
||||||
|
run.text = run.text.replace(placeholder, value)
|
||||||
|
modified = True
|
||||||
|
if not modified:
|
||||||
|
for key, value in replacements.items():
|
||||||
|
placeholder = f"{{{{{key}}}}}"
|
||||||
|
if placeholder in paragraph.text:
|
||||||
|
paragraph.text = paragraph.text.replace(placeholder, value)
|
||||||
|
|
||||||
|
def walk_document(doc_obj, replacements):
|
||||||
|
for paragraph in doc_obj.paragraphs:
|
||||||
|
replace_in_paragraph(paragraph, replacements)
|
||||||
|
for table in doc_obj.tables:
|
||||||
|
for row in table.rows:
|
||||||
|
for cell in row.cells:
|
||||||
|
for paragraph in cell.paragraphs:
|
||||||
|
replace_in_paragraph(paragraph, replacements)
|
||||||
|
|
||||||
|
walk_document(new_doc, generated)
|
||||||
|
new_doc.save(new_path)
|
||||||
|
|
||||||
|
await send_file_to_user(bot, msg, str(new_path), prefix=f"📄 Сгенерированный документ по шаблону '{template_name}':", suffix="generated")
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.unlink(new_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Не удалось удалить временный файл {new_path}: {e}")
|
||||||
|
|
||||||
|
logger.info(f"🏁 [ГЕНЕРАЦИЯ] Завершено для шаблона '{template_name}'")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"💥 [ГЕНЕРАЦИЯ] Критическая ошибка: {e}")
|
||||||
|
if msg:
|
||||||
|
reply = msg.reply(f"❌ Ошибка при генерации документа: {e}")
|
||||||
|
if reply:
|
||||||
|
reply.send()
|
||||||
Reference in New Issue
Block a user