feat: шаблоны показываются в main.py при нажатии 3, sd_module_handler только обрабатывает выбор
This commit is contained in:
+26
-49
@@ -23,7 +23,7 @@ if not sd_workflow_logger.handlers:
|
|||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
sd_workflow_logger.addHandler(handler)
|
sd_workflow_logger.addHandler(handler)
|
||||||
# ----------------------------------
|
# ----------------------------------
|
||||||
|
|
||||||
# Абсолютный импорт конфигурации
|
# Абсолютный импорт конфигурации
|
||||||
spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "config.py"))
|
spec = importlib.util.spec_from_file_location("custom_config", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "config.py"))
|
||||||
custom_config = importlib.util.module_from_spec(spec)
|
custom_config = importlib.util.module_from_spec(spec)
|
||||||
@@ -48,10 +48,10 @@ from utils.texts import (
|
|||||||
from utils.texts import UNKNOWN_MAIN_CMD_TEXT
|
from utils.texts import UNKNOWN_MAIN_CMD_TEXT
|
||||||
# 🔌 Импортируем централизованную функцию сбора статистики из main
|
# 🔌 Импортируем централизованную функцию сбора статистики из main
|
||||||
from utils.stats_logger import log_menu_stats
|
from utils.stats_logger import log_menu_stats
|
||||||
|
|
||||||
# Таймаут ожидания вложений после текста (секунды)
|
# Таймаут ожидания вложений после текста (секунды)
|
||||||
ATTACHMENT_WAIT_TIMEOUT = 2.0
|
ATTACHMENT_WAIT_TIMEOUT = 2.0
|
||||||
|
|
||||||
SD_DISPATCHER_SYSTEM_PROMPT = (
|
SD_DISPATCHER_SYSTEM_PROMPT = (
|
||||||
"Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\n"
|
"Ты — старший ИИ-диспетчер Service Desk холдинга. Твоя задача — сформулировать краткую и понятную тему ИТ-заявки по-русски (от 3 до 7 слов) на основе текста пользователя.\n"
|
||||||
"ПРАВИЛА:\n"
|
"ПРАВИЛА:\n"
|
||||||
@@ -59,7 +59,7 @@ SD_DISPATCHER_SYSTEM_PROMPT = (
|
|||||||
"2. Отвечать на английском языке ЗАПРЕЩЕНО.\n"
|
"2. Отвечать на английском языке ЗАПРЕЩЕНО.\n"
|
||||||
"3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n"
|
"3. Тебе категорически запрещено решать проблему или писать мануалы по настройке.\n"
|
||||||
"4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n"
|
"4. Не пиши префиксы 'Тема:', 'Заголовок:' в ответе.\n"
|
||||||
"5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.")
|
"5. Если в тексте только приветствие или мат — выведи ровно одно слово: СПАМ.")
|
||||||
|
|
||||||
# Глушим системные предупреждения
|
# Глушим системные предупреждения
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
@@ -138,14 +138,14 @@ async def generate_smart_subject(text: str) -> str:
|
|||||||
OLLAMA_SUBJECT_MODEL = custom_config.OLLAMA_SUBJECT_MODEL
|
OLLAMA_SUBJECT_MODEL = custom_config.OLLAMA_SUBJECT_MODEL
|
||||||
def has_chinese(t): return bool(re.search(r'[\u4e00-\u9fff]', str(t)))
|
def has_chinese(t): return bool(re.search(r'[\u4e00-\u9fff]', str(t)))
|
||||||
if not text or len(text.split()) < 2: return "Заявка из КЛЕВЕР"
|
if not text or len(text.split()) < 2: return "Заявка из КЛЕВЕР"
|
||||||
|
|
||||||
user_msg = f"Сформулируй краткую тему для следующей ИТ-заявки:\n{text}"
|
user_msg = f"Сформулируй краткую тему для следующей ИТ-заявки:\n{text}"
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": SD_DISPATCHER_SYSTEM_PROMPT},
|
{"role": "system", "content": SD_DISPATCHER_SYSTEM_PROMPT},
|
||||||
{"role": "user", "content": user_msg}
|
{"role": "user", "content": user_msg}
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = {"model": OLLAMA_SUBJECT_MODEL, "messages": messages, "temperature": 0.0}
|
payload = {"model": OLLAMA_SUBJECT_MODEL, "messages": messages, "temperature": 0.0}
|
||||||
sd_workflow_logger.info(f"📝 [Subject Gen] Payload: {payload}")
|
sd_workflow_logger.info(f"📝 [Subject Gen] Payload: {payload}")
|
||||||
@@ -221,7 +221,7 @@ async def process_and_upload_file(file_id: str, filename: str, ticket_id: str):
|
|||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
if downloaded_path and os.path.exists(downloaded_path):
|
if downloaded_path and os.path.exists(downloaded_path):
|
||||||
os.remove(downloaded_path)
|
os.remove(downloaded_path)
|
||||||
|
|
||||||
def _extract_text_from_content(content):
|
def _extract_text_from_content(content):
|
||||||
"""Извлечение текста из msg.content."""
|
"""Извлечение текста из msg.content."""
|
||||||
@@ -318,10 +318,10 @@ async def _wait_for_attachments_and_create(user_id, session):
|
|||||||
await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT)
|
await asyncio.sleep(ATTACHMENT_WAIT_TIMEOUT)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
return # Таймер сброшен, прерываем выполнение текущей таски
|
return # Таймер сброшен, прерываем выполнение текущей таски
|
||||||
|
|
||||||
if session.get("step") != "waiting_for_attachments":
|
if session.get("step") != "waiting_for_attachments":
|
||||||
return
|
return
|
||||||
|
|
||||||
sd_workflow_logger.info(f"⏰ [SD] Timeout reached. Initiating ticket creation for user {user_id}")
|
sd_workflow_logger.info(f"⏰ [SD] Timeout reached. Initiating ticket creation for user {user_id}")
|
||||||
msg = session.get("msg")
|
msg = session.get("msg")
|
||||||
login = session.get("login")
|
login = session.get("login")
|
||||||
@@ -334,46 +334,23 @@ async def sd_module_handler(msg: Message):
|
|||||||
sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}")
|
sd_workflow_logger.info(f"🚀 [SD Workflow Start] User: {user_id}")
|
||||||
if states.get_state(user_id) != "SD_MODE":
|
if states.get_state(user_id) != "SD_MODE":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# Инициализация шаблонов при входе в SD_MODE
|
|
||||||
if user_id not in sd_sessions:
|
|
||||||
sd_sessions[user_id] = {"step": "choose_template", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}, "template_menu_text": ""}
|
|
||||||
# Загружаем шаблоны если их ещё нет
|
|
||||||
if not sd_sessions[user_id].get("templates"):
|
|
||||||
templates = await get_sd_templates()
|
|
||||||
if templates:
|
|
||||||
sd_sessions[user_id]["templates"] = templates
|
|
||||||
sd_sessions[user_id]["template_names"] = {str(i+1): str(t["id"]) for i, t in enumerate(templates)}
|
|
||||||
template_menu_text = ""
|
|
||||||
for idx, t in enumerate(templates):
|
|
||||||
num = idx + 1
|
|
||||||
digit = f"{num}⃣" if num <= 9 else "0⃣" if num == 10 else str(num)
|
|
||||||
template_menu_text += f"{digit} - {t['name']}\n"
|
|
||||||
sd_sessions[user_id]["template_menu_text"] = template_menu_text.strip()
|
|
||||||
sd_workflow_logger.info(f"[Template] Loaded {len(templates)} templates for user {user_id}")
|
|
||||||
await msg.answer(
|
|
||||||
f"Выберите нужный шаблон:\n\n"
|
|
||||||
f"{template_menu_text}",
|
|
||||||
parse_mode="html"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
msg_text = _extract_text_from_content(msg.content)
|
msg_text = _extract_text_from_content(msg.content)
|
||||||
is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT"
|
is_attachment = hasattr(msg.type, "name") and msg.type.name == "ATTACHMENT"
|
||||||
inline_attachments = _extract_attachments_from_content(msg.content, is_attachment)
|
inline_attachments = _extract_attachments_from_content(msg.content, is_attachment)
|
||||||
|
|
||||||
sd_workflow_logger.info(f"🚀 [SD Debug] msg.type={msg.type}, content_type={type(msg.content).__name__}, msg_text='{msg_text}', is_attachment={is_attachment}, inline_attachments={len(inline_attachments)}")
|
sd_workflow_logger.info(f"🚀 [SD Debug] msg.type={msg.type}, content_type={type(msg.content).__name__}, msg_text='{msg_text}', is_attachment={is_attachment}, inline_attachments={len(inline_attachments)}")
|
||||||
|
|
||||||
msg.handled = True
|
msg.handled = True
|
||||||
login = user_id.split("@")[0] if "@" in user_id else user_id
|
login = user_id.split("@")[0] if "@" in user_id else user_id
|
||||||
cmd = msg_text.lower()
|
cmd = msg_text.lower()
|
||||||
|
|
||||||
# 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ
|
# 🔄 НОРМАЛИЗАЦИЯ КНОПОК ВК-ЭМОДЗИ
|
||||||
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||||||
if cmd == emoji_num:
|
if cmd == emoji_num:
|
||||||
cmd = raw_num
|
cmd = raw_num
|
||||||
break
|
break
|
||||||
|
|
||||||
# --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) ---
|
# --- ВЫХОД (Кнопки 0 и 9 возвращают в общее меню) ---
|
||||||
if cmd in ["0", "9", "/start", "меню"]:
|
if cmd in ["0", "9", "/start", "меню"]:
|
||||||
log_menu_stats(user_id, "Service Desk", "Выход в главное меню")
|
log_menu_stats(user_id, "Service Desk", "Выход в главное меню")
|
||||||
@@ -381,12 +358,12 @@ async def sd_module_handler(msg: Message):
|
|||||||
sd_sessions.pop(user_id, None)
|
sd_sessions.pop(user_id, None)
|
||||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||||
return
|
return
|
||||||
|
|
||||||
if user_id not in sd_sessions:
|
if user_id not in sd_sessions:
|
||||||
sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}}
|
sd_sessions[user_id] = {"step": "need_text", "files_queue": [], "post_create_queue": [], "templates": [], "template_names": {}}
|
||||||
session = sd_sessions[user_id]
|
session = sd_sessions[user_id]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- ШАГ 0: ВЫБОР ШАБЛОНА ---
|
# --- ШАГ 0: ВЫБОР ШАБЛОНА ---
|
||||||
if session["step"] == "choose_template":
|
if session["step"] == "choose_template":
|
||||||
if cmd in ["skip", "пропустить", "без шаблона"]:
|
if cmd in ["skip", "пропустить", "без шаблона"]:
|
||||||
@@ -419,26 +396,26 @@ async def sd_module_handler(msg: Message):
|
|||||||
if not msg_text.strip() and not inline_attachments:
|
if not msg_text.strip() and not inline_attachments:
|
||||||
await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html")
|
await msg.answer(SD_UNKNOWN_CMD_TEXT, parse_mode="html")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Если только вложение без текста — сохраняем и ждем текст
|
# Если только вложение без текста — сохраняем и ждем текст
|
||||||
if not msg_text.strip() and inline_attachments:
|
if not msg_text.strip() and inline_attachments:
|
||||||
session["files_queue"].extend(inline_attachments)
|
session["files_queue"].extend(inline_attachments)
|
||||||
sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text")
|
sd_workflow_logger.info(f"📎 [SD] Saved {len(inline_attachments)} attachment(s), waiting for text")
|
||||||
await msg.answer(SD_TEXT_REQUIRED, parse_mode="html")
|
await msg.answer(SD_TEXT_REQUIRED, parse_mode="html")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Если пришел текст (с вложениями или без)
|
# Если пришел текст (с вложениями или без)
|
||||||
if inline_attachments:
|
if inline_attachments:
|
||||||
session["files_queue"].extend(inline_attachments)
|
session["files_queue"].extend(inline_attachments)
|
||||||
sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue")
|
sd_workflow_logger.info(f"📎 [SD] Added {len(inline_attachments)} inline attachment(s) to queue")
|
||||||
|
|
||||||
# Настраиваем параметры сессии для ожидания
|
# Настраиваем параметры сессии для ожидания
|
||||||
session["step"] = "waiting_for_attachments"
|
session["step"] = "waiting_for_attachments"
|
||||||
session["msg_text"] = msg_text
|
session["msg_text"] = msg_text
|
||||||
session["msg"] = msg
|
session["msg"] = msg
|
||||||
session["user_id"] = user_id
|
session["user_id"] = user_id
|
||||||
session["login"] = login
|
session["login"] = login
|
||||||
|
|
||||||
# Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота
|
# Сразу выдаем ОДНО сообщение пользователю, чтобы он видел реакцию бота
|
||||||
await msg.answer(SD_CREATING, parse_mode="html")
|
await msg.answer(SD_CREATING, parse_mode="html")
|
||||||
|
|
||||||
@@ -448,7 +425,7 @@ async def sd_module_handler(msg: Message):
|
|||||||
session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session))
|
session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session))
|
||||||
sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...")
|
sd_workflow_logger.info(f"⏳ [SD] Timer started: waiting {ATTACHMENT_WAIT_TIMEOUT}s for potentially more attachments...")
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) ---
|
# --- ШАГ 2: РЕЖИМ ОЖИДАНИЯ ДОП. ВЛОЖЕНИЙ (Сброс таймаута) ---
|
||||||
if session["step"] == "waiting_for_attachments":
|
if session["step"] == "waiting_for_attachments":
|
||||||
if inline_attachments:
|
if inline_attachments:
|
||||||
@@ -457,14 +434,14 @@ async def sd_module_handler(msg: Message):
|
|||||||
if msg_text.strip():
|
if msg_text.strip():
|
||||||
session["msg_text"] = session["msg_text"] + " " + msg_text.strip()
|
session["msg_text"] = session["msg_text"] + " " + msg_text.strip()
|
||||||
sd_workflow_logger.info(f"📝 [SD] Additional text appended")
|
sd_workflow_logger.info(f"📝 [SD] Additional text appended")
|
||||||
|
|
||||||
# Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед)
|
# Перезапускаем таймер (пользователь активен, сдвигаем окно создания вперед)
|
||||||
if "timer_task" in session and not session["timer_task"].done():
|
if "timer_task" in session and not session["timer_task"].done():
|
||||||
session["timer_task"].cancel()
|
session["timer_task"].cancel()
|
||||||
session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session))
|
session["timer_task"] = asyncio.create_task(_wait_for_attachments_and_create(user_id, session))
|
||||||
sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s")
|
sd_workflow_logger.info(f"🔄 [SD] Timer reset due to user activity. Waiting another {ATTACHMENT_WAIT_TIMEOUT}s")
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") ---
|
# --- ШАГ 3: ЗАЯВКА СОЗДАЕТСЯ (Перехват файлов из "черной дыры") ---
|
||||||
if session["step"] == "creating_ticket":
|
if session["step"] == "creating_ticket":
|
||||||
if inline_attachments:
|
if inline_attachments:
|
||||||
@@ -473,7 +450,7 @@ async def sd_module_handler(msg: Message):
|
|||||||
session["post_create_queue"].extend(inline_attachments)
|
session["post_create_queue"].extend(inline_attachments)
|
||||||
sd_workflow_logger.info(f"📥 [SD Hole-Fix] Captured {len(inline_attachments)} file(s) DURING ticket creation API call.")
|
sd_workflow_logger.info(f"📥 [SD Hole-Fix] Captured {len(inline_attachments)} file(s) DURING ticket creation API call.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ ---
|
# --- ШАГ 4: ПРИЕМ ДОП. ФАЙЛОВ К УЖЕ СОЗДАННОЙ ЗАЯВКЕ ---
|
||||||
if session["step"] == "ticket_created":
|
if session["step"] == "ticket_created":
|
||||||
if inline_attachments:
|
if inline_attachments:
|
||||||
@@ -489,4 +466,4 @@ async def sd_module_handler(msg: Message):
|
|||||||
await msg.answer(sd_file_upload_error(), parse_mode="html")
|
await msg.answer(sd_file_upload_error(), parse_mode="html")
|
||||||
elif msg_text.strip():
|
elif msg_text.strip():
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user