feat(legal): interactive buttons after jurist answer (ask next / exit)
This commit is contained in:
parent
b582a579aa
commit
2924765624
1 changed files with 75 additions and 7 deletions
|
|
@ -78,12 +78,24 @@ async def send_split_message(message: Message, header: str, body: str):
|
||||||
part_header = f"<i>(Продолжение {idx}/{len(chunks)}):</i>\n\n"
|
part_header = f"<i>(Продолжение {idx}/{len(chunks)}):</i>\n\n"
|
||||||
|
|
||||||
part_text = f"{part_header}{format_telegram_html(chunk)}"
|
part_text = f"{part_header}{format_telegram_html(chunk)}"
|
||||||
|
# Прикрепляем инлайн-кнопки только к последней части ответа
|
||||||
|
reply_markup = get_legal_nav_keyboard() if idx == len(chunks) else None
|
||||||
try:
|
try:
|
||||||
await message.answer(part_text, parse_mode='HTML')
|
await message.answer(part_text, parse_mode='HTML', reply_markup=reply_markup)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"HTML send failed ({e}), falling back to plain text")
|
log.warning(f"HTML send failed ({e}), falling back to plain text")
|
||||||
plain_header = header if idx == 1 else f"(Продолжение {idx}/{len(chunks)}):"
|
plain_header = header if idx == 1 else f"(Продолжение {idx}/{len(chunks)}):"
|
||||||
await message.answer(f"{plain_header}\n\n{chunk}")
|
await message.answer(f"{plain_header}\n\n{chunk}", reply_markup=reply_markup)
|
||||||
|
|
||||||
|
def get_legal_nav_keyboard() -> InlineKeyboardMarkup:
|
||||||
|
"""Инлайн-кнопки навигации после ответа юриста."""
|
||||||
|
kb = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="📝 Задать следующий вопрос", callback_data="jurist_ask_next"),
|
||||||
|
InlineKeyboardButton(text="🚪 Выход из режима юриста", callback_data="jurist_exit")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=kb)
|
||||||
|
|
||||||
async def determine_user_role(user_id: int) -> str:
|
async def determine_user_role(user_id: int) -> str:
|
||||||
"""Определяет роль пользователя в системе."""
|
"""Определяет роль пользователя в системе."""
|
||||||
|
|
@ -120,12 +132,13 @@ async def cmd_legal_advisor(message: Message, state: FSMContext, bot: Bot):
|
||||||
if len(parts) > 1 and parts[0].casefold() in ("/юрист", "/jurist", "/вопрос"):
|
if len(parts) > 1 and parts[0].casefold() in ("/юрист", "/jurist", "/вопрос"):
|
||||||
inline_question = parts[1].strip()
|
inline_question = parts[1].strip()
|
||||||
if len(inline_question) > 3:
|
if len(inline_question) > 3:
|
||||||
status_msg = await message.answer("⏳ <i>Анализирую нормативную базу дома...</i>", parse_mode='HTML')
|
status_msg = await message.answer("⏳ <i>Анализирую нормы законодательства РФ...</i>", parse_mode='HTML')
|
||||||
result = await legal_engine.query(prompt=inline_question, role=role)
|
result = await legal_engine.query(prompt=inline_question, role=role)
|
||||||
await status_msg.delete()
|
await status_msg.delete()
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
header = "⚖️ <b>Юридический ответ:</b>"
|
header = "⚖️ <b>Юридический ответ:</b>"
|
||||||
await send_split_message(message, header, result["text"])
|
await send_split_message(message, header, result["text"])
|
||||||
|
await state.set_state(LegalQuestionState.waiting_for_question)
|
||||||
else:
|
else:
|
||||||
await message.answer(f"❌ <b>Ошибка:</b> {result.get('text')}", parse_mode='HTML')
|
await message.answer(f"❌ <b>Ошибка:</b> {result.get('text')}", parse_mode='HTML')
|
||||||
return
|
return
|
||||||
|
|
@ -159,6 +172,60 @@ async def cb_legal_advisor(callback: CallbackQuery, state: FSMContext):
|
||||||
await callback.message.answer(text, parse_mode='HTML')
|
await callback.message.answer(text, parse_mode='HTML')
|
||||||
await state.set_state(LegalQuestionState.waiting_for_question)
|
await state.set_state(LegalQuestionState.waiting_for_question)
|
||||||
|
|
||||||
|
# === КНОПКИ НАВИГАЦИИ ПОСЛЕ ОТВЕТА ЮРИСТА ===
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "jurist_ask_next")
|
||||||
|
async def cb_jurist_ask_next(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Пользователь хочет задать следующий вопрос юристу"""
|
||||||
|
await callback.answer()
|
||||||
|
role = await determine_user_role(callback.from_user.id)
|
||||||
|
if role not in ("admin", "lawyer"):
|
||||||
|
await callback.message.answer("🔒 <b>Доступ ограничен.</b>", parse_mode='HTML')
|
||||||
|
await state.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
await state.set_state(LegalQuestionState.waiting_for_question)
|
||||||
|
await callback.message.answer(
|
||||||
|
"⚖️ <b>Режим юриста активен.</b>\n\n"
|
||||||
|
"Напишите ваш следующий вопрос или отправьте голосовое сообщение:",
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "jurist_exit")
|
||||||
|
async def cb_jurist_exit(callback: CallbackQuery, state: FSMContext):
|
||||||
|
"""Выход из режима юриста с возвратом в главное меню"""
|
||||||
|
await callback.answer("Режим юриста завершён")
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
user_id = callback.from_user.id
|
||||||
|
try:
|
||||||
|
from keyboards.inline import get_main_menu
|
||||||
|
from database.models import InitiativeGroup
|
||||||
|
import config
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
stmt = select(User).where(User.user_id == user_id)
|
||||||
|
user = (await session.execute(stmt)).scalar_one_or_none()
|
||||||
|
ig_stmt = select(InitiativeGroup).where(InitiativeGroup.user_id == user_id, InitiativeGroup.is_active == True)
|
||||||
|
is_ig = (await session.execute(ig_stmt)).scalar_one_or_none() is not None
|
||||||
|
|
||||||
|
is_admin = (user.is_admin if user else False) or (user_id == config.ADMIN_USER_ID)
|
||||||
|
is_verified = user.verified if user else False
|
||||||
|
|
||||||
|
await callback.message.answer(
|
||||||
|
"🚪 <b>Вы вышли из режима юриста.</b>\n\nГлавное меню:",
|
||||||
|
reply_markup=get_main_menu(
|
||||||
|
verified=is_verified,
|
||||||
|
is_ig=is_ig,
|
||||||
|
is_admin=is_admin,
|
||||||
|
is_superadmin=(user_id == config.ADMIN_USER_ID)
|
||||||
|
),
|
||||||
|
parse_mode='HTML'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Failed to show main menu on jurist exit: {e}")
|
||||||
|
await callback.message.answer("🚪 <b>Вы вышли из режима юриста.</b>", parse_mode='HTML')
|
||||||
|
|
||||||
# === ОБРАБОТКА ТЕКСТОВЫХ ВОПРОСОВ ===
|
# === ОБРАБОТКА ТЕКСТОВЫХ ВОПРОСОВ ===
|
||||||
|
|
||||||
@router.message(LegalQuestionState.waiting_for_question, F.text)
|
@router.message(LegalQuestionState.waiting_for_question, F.text)
|
||||||
|
|
@ -186,7 +253,6 @@ async def process_text_legal_question(message: Message, state: FSMContext, bot:
|
||||||
|
|
||||||
if not result.get("success"):
|
if not result.get("success"):
|
||||||
await message.answer(f"❌ <b>Ошибка:</b> {result.get('text')}", parse_mode='HTML')
|
await message.answer(f"❌ <b>Ошибка:</b> {result.get('text')}", parse_mode='HTML')
|
||||||
await state.clear()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Защита от спама: если вопрос задан в групповом чате
|
# Защита от спама: если вопрос задан в групповом чате
|
||||||
|
|
@ -196,7 +262,8 @@ async def process_text_legal_question(message: Message, state: FSMContext, bot:
|
||||||
await bot.send_message(
|
await bot.send_message(
|
||||||
chat_id=user_id,
|
chat_id=user_id,
|
||||||
text=f"⚖️ <b>Юридический ответ:</b>\n\n{format_telegram_html(result['text'])}",
|
text=f"⚖️ <b>Юридический ответ:</b>\n\n{format_telegram_html(result['text'])}",
|
||||||
parse_mode='HTML'
|
parse_mode='HTML',
|
||||||
|
reply_markup=get_legal_nav_keyboard()
|
||||||
)
|
)
|
||||||
await message.reply(
|
await message.reply(
|
||||||
"💬 <b>Ответ сформирован и отправлен вам в личные сообщения с ботом</b>, чтобы не перегружать общий чат дома.",
|
"💬 <b>Ответ сформирован и отправлен вам в личные сообщения с ботом</b>, чтобы не перегружать общий чат дома.",
|
||||||
|
|
@ -212,7 +279,8 @@ async def process_text_legal_question(message: Message, state: FSMContext, bot:
|
||||||
header = "⚖️ <b>Юридический ответ:</b>"
|
header = "⚖️ <b>Юридический ответ:</b>"
|
||||||
await send_split_message(message, header, result["text"])
|
await send_split_message(message, header, result["text"])
|
||||||
|
|
||||||
await state.clear()
|
# Оставляем состояние активным, чтобы жилец мог сразу задать следующий вопрос без клика
|
||||||
|
await state.set_state(LegalQuestionState.waiting_for_question)
|
||||||
|
|
||||||
# === ОБРАБОТКА ГОЛОСОВЫХ ВОПРОСОВ (НАТИВНОЕ МУЛЬТИМОДАЛЬНОЕ РАСПОЗНАВАНИЕ) ===
|
# === ОБРАБОТКА ГОЛОСОВЫХ ВОПРОСОВ (НАТИВНОЕ МУЛЬТИМОДАЛЬНОЕ РАСПОЗНАВАНИЕ) ===
|
||||||
|
|
||||||
|
|
@ -253,6 +321,7 @@ async def process_voice_legal_question(message: Message, state: FSMContext, bot:
|
||||||
|
|
||||||
header = "⚖️ <b>Юридический ответ:</b>"
|
header = "⚖️ <b>Юридический ответ:</b>"
|
||||||
await send_split_message(message, header, result["text"])
|
await send_split_message(message, header, result["text"])
|
||||||
|
await state.set_state(LegalQuestionState.waiting_for_question)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"Voice processing error: {e}")
|
log.error(f"Voice processing error: {e}")
|
||||||
|
|
@ -262,4 +331,3 @@ async def process_voice_legal_question(message: Message, state: FSMContext, bot:
|
||||||
if voice_file_path and os.path.exists(voice_file_path):
|
if voice_file_path and os.path.exists(voice_file_path):
|
||||||
try: os.unlink(voice_file_path)
|
try: os.unlink(voice_file_path)
|
||||||
except: pass
|
except: pass
|
||||||
await state.clear()
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue