diff --git a/handlers/voice.py b/handlers/voice.py
index bf9eca7..a379767 100644
--- a/handlers/voice.py
+++ b/handlers/voice.py
@@ -281,35 +281,35 @@ async def transcribe_audio(audio_path: str) -> str:
os.remove(tmp_wav)
return ""
-async def execute_antigravity_agent(user_text: str) -> str:
- """Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд"""
- system_instruction = (
- "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра с полными правами инженера.\n"
- "Александр обращается к тебе голосом в Телеграмме (на ходу, на велосипеде, за рулем, спонтанный поток мыслей с паузами, оговорками и повторами).\n\n"
- "ТВОЯ ГЛАВНАЯ ЦЕЛЬ — ПРЕВРАТИТЬ СПОНТАННЫЙ ПОТОК РЕЧИ В КРИСТАЛЬНО ЧИСТУЮ СТРУКТУРУ И ВЫПОЛНИТЬ ТРЕБУЕМЫЕ ДЕЙСТВИЯ.\n\n"
- "ОБЯЗАТЕЛЬНЫЙ ШАБЛОН ТВОЕГО ТЕКСТОВОГО ОТВЕТА (СТРОГО СОБЛЮДАЙ ЭТУ СТРУКТУРУ В КАЖДОМ ОТВЕТЕ!):\n\n"
- "🎯 Суть:\n"
- "[Очищенная от слов-паразитов, междометий и сумбура суть сказанного в 1-3 емких предложениях]\n\n"
- "📋 Задачи к выполнению (Action Items):\n"
- "• [ ] [Конкретная задача 1]\n"
- "• [ ] [Конкретная задача 2]\n"
- "(Если задач к исполнению не было — напиши: «Конкретных задач не поставлено — зафиксировано как идея/мысли»)\n\n"
- "💡 Идеи в копилку / Бэклог:\n"
- "• [Инсайт, гипотеза или идея на будущее]\n"
- "(Если идей не было — напиши: «—»)\n\n"
- "🗄️ Действия и фиксация:\n"
- "• [ОБЯЗАТЕЛЬНО: Вызови save_to_obsidian и укажи: ✅ Сохранено в Obsidian: раздел/подкаталог/файл.md]\n"
- "• [Если Александр просил проверить или сделать что-то — вызови query_bitrix24 или execute_bash_command и напиши конкретный результат выполнения!]\n\n"
- "ПРАВИЛА:\n"
- "1. ЛЮБЫЕ мысли, идеи, планы, рассуждения ОБЯЗАТЕЛЬНО сохраняй в Obsidian через tool save_to_obsidian(section, subfolder, filename, title, content, tags).\n"
- " - section: 'HomeLab', 'Work' или 'Свалка'.\n"
- " - subfolder: 'AI_Swarm', 'Infrastructure', 'Bicycle_Ebike', 'Housing_LKM37', 'Visma', 'Bitrix24', 'General'.\n"
- " - filename: говорящее имя на латинице (например: 'ebike_controller_upgrade.md').\n"
- " - content: структурированный текст заметки.\n"
- " КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО писать в чат только фразу «я сохранила в файл»! Ты ОБЯЗАНА полностью вывести блоки 🎯 Суть, 📋 Задачи, 💡 Идеи в сам ответ, чтобы Александр видел структуру прямо перед глазами!\n"
- "2. Если Александр попросил действие (Битрикс24, свободное место, контейнеры, сервера) — вызови инструмент (query_bitrix24 / execute_bash_command) и выведи результат в блок «🗄️ Действия и фиксация».\n"
- "3. В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег ... с короткой (1-2 предложения), теплой голосовой выжимкой для Шаде без Markdown и эмодзи."
- )
+async def structure_thoughts(user_text: str) -> str:
+ """Этап 2: Превращение сумбурного потока мыслей в кристальную структуру с метаданными для Obsidian"""
+ prompt = f"""
+Ты — Майа, личный аналитик и инженер Александра.
+Александр наговорил голосовое сообщение на ходу. В речи могут быть паузы, оговорки, повторы, слова-паразиты, сумбур.
+
+Твоя задача — кристально структурировать надиктованное по строгой схеме:
+
+🎯 Суть:
+[Главное зерно мысли в 1-2 емких, четких предложениях, полностью очищенных от мусорных слов]
+
+📋 Задачи к выполнению:
+* [ ] [Конкретная задача 1]
+* [ ] [Конкретная задача 2]
+(Если задач нет — напиши: «Конкретных задач не поставлено — зафиксировано как идея/мысли»)
+
+💡 Идеи в копилку / Бэклог:
+* [Инсайт, гипотеза или идея на перспективу]
+(Если идей нет — напиши: «—»)
+
+ОБЯЗАТЕЛЬНО добавь в конце метаданные для сохранения в Obsidian:
+
+
+И добавь тег с короткой теплой выжимкой для синтеза речи (1-2 предложения без markdown):
+Текст выжимки от первого лица для озвучки
+
+Сырой текст речи Александра:
+«{user_text}»
+"""
models_to_try = [
'gemini-2.5-flash',
@@ -317,30 +317,69 @@ async def execute_antigravity_agent(user_text: str) -> str:
'gemini-2.5-pro',
'gemini-3.5-flash'
]
- last_error = None
- def _sync_run_agent(m_name: str, u_text: str) -> str:
+ for model_name in models_to_try:
+ try:
+ logger.info(f"🧠 [Maya Structuring] Запуск с моделью: {model_name}")
+ def _sync_gen(m_name, p):
+ model = genai.GenerativeModel(m_name)
+ resp = model.generate_content(p)
+ return resp.text.strip() if resp and resp.text else ""
+
+ res = await asyncio.to_thread(_sync_gen, model_name, prompt)
+ if res:
+ return res
+ except Exception as e:
+ logger.warning(f"⚠️ [Maya Structuring] Сбой на модели {model_name}: {e}")
+ await asyncio.sleep(0.5)
+
+ return (
+ f"🎯 Суть:\n{user_text}\n\n"
+ f"📋 Задачи к выполнению:\n* [ ] Разобрать надиктованное голосовое сообщение\n\n"
+ f"💡 Идеи в копилку / Бэклог:\n—\n\n"
+ f"\n"
+ f"Я зафиксировала твою мысль и сохранила её в Obsidian."
+ )
+
+async def execute_voice_commands_if_any(user_text: str) -> str:
+ """Этап 3: Проверка и выполнение реальных инженерных команд (Битрикс24, bash, серверы)"""
+ system_instruction = (
+ "Ты — Майа, системный инженер и исполнитель команд Александра.\n"
+ "Проанализируй текст Александра. Если он содержит ЯВНЫЕ команды к исполнению прямо сейчас "
+ "(например: проверить задачи в Битрикс24, посмотреть статус сервера, проверить диск/память/докер, выполнить команду):\n"
+ "1. Немедленно вызови соответствующий инструмент (query_bitrix24 или execute_bash_command).\n"
+ "2. Дай четкий, емкий инженерный результат выполнения.\n"
+ "Если же текст Александра — это ПРОСТО мысли, рассуждения, идеи или планы на будущее (без прямого приказа выполнить что-то прямо сейчас) — "
+ "верни СТРОГО ОДНО СЛОВО: НЕТ_КОМАНД"
+ )
+
+ models_to_try = [
+ 'gemini-2.5-flash',
+ 'gemini-flash-latest',
+ 'gemini-2.5-pro'
+ ]
+
+ def _sync_cmd(m_name, u_txt):
model = genai.GenerativeModel(
model_name=m_name,
system_instruction=system_instruction,
- tools=[query_bitrix24, execute_bash_command, save_to_obsidian]
+ tools=[query_bitrix24, execute_bash_command]
)
chat = model.start_chat(enable_automatic_function_calling=True)
- resp = chat.send_message(u_text)
+ resp = chat.send_message(u_txt)
return resp.text.strip() if resp and resp.text else ""
for model_name in models_to_try:
try:
- logger.info(f"🚀 [Maya Agent] Запуск с моделью: {model_name}")
- response_text = await asyncio.to_thread(_sync_run_agent, model_name, user_text)
- if response_text:
- return response_text
+ logger.info(f"⚡️ [Maya Commands] Проверка наличия команд ({model_name})...")
+ res = await asyncio.to_thread(_sync_cmd, model_name, user_text)
+ if res:
+ return res
except Exception as e:
- logger.warning(f"⚠️ [Maya Agent] Сбой на модели {model_name}: {e}")
- last_error = e
+ logger.warning(f"⚠️ [Maya Commands] Ошибка {model_name}: {e}")
await asyncio.sleep(0.5)
- return f"❌ Ошибка исполнения Maya Agent: {last_error}"
+ return "НЕТ_КОМАНД"
@router.message(F.voice | F.video_note | F.audio | F.document)
async def handle_voice_message(message: Message, bot: Bot):
@@ -390,13 +429,13 @@ async def handle_voice_message(message: Message, bot: Bot):
size_mb = round(file_size / (1024 * 1024), 1)
await message.reply(
f"⚠️ Файл слишком большой ({size_mb} МБ).\n\n"
- "Telegram Bot API жестко ограничивает скачивание ботами файлов размером до 20 МБ.\n"
- "💡 Решение: Сконвертируй запись диктофона в AAC/M4A/MP3 с более низким битрейтом (например, 64 kbps mono) или надиктуй голосовыми сообщениями прямо в чат!",
+ "Telegram Bot API ограничивает скачивание ботами файлов размером до 20 МБ.\n"
+ "💡 Решение: Сконвертируй запись диктофона в AAC/M4A/MP3 (64 kbps mono) или надиктуй голосовыми в чат!",
parse_mode="HTML"
)
return
- status_msg = await message.reply("🎙 Слушаю и выполняю команду...", parse_mode="HTML")
+ status_msg = await message.reply("🎙 Слушаю аудиозапись...", parse_mode="HTML")
await bot.send_chat_action(message.chat.id, "typing")
try:
@@ -404,7 +443,7 @@ async def handle_voice_message(message: Message, bot: Bot):
file_info = await bot.get_file(file_id)
await bot.download_file(file_info.file_path, local_audio_path)
- # 1. Прямая расшифровка через локальный Whisper M4 (с фоллбэком на Gemini)
+ # 1. Расшифровка через Gemini Audio API (с нормализацией ffmpeg)
transcribed_text = await transcribe_audio(local_audio_path)
if os.path.exists(local_audio_path):
@@ -413,34 +452,56 @@ async def handle_voice_message(message: Message, bot: Bot):
if transcribed_text:
log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), transcribed_text)
- safe_transcribed = html.escape(transcribed_text)
+ # ===== ЭТАП 1: Показываем сырой текст как есть (без эффектов) =====
+ safe_raw = html.escape(transcribed_text)
await status_msg.edit_text(
- f"🗣 Вы сказали:\n{safe_transcribed}\n\n"
- f"🧠 Майа структурирует мысли и выполняет действия...",
+ f"🗣 Услышано:\n«{safe_raw}»\n\n"
+ f"🧠 Майа структурирует мысли и вникает в суть...",
parse_mode="HTML"
)
- await bot.send_chat_action(message.chat.id, "record_voice")
+ await bot.send_chat_action(message.chat.id, "typing")
- # 2. Исполнение Maya Lead Agent (Битрикс24, bash, анализ)
- agent_response = await execute_antigravity_agent(transcribed_text)
+ # ===== ЭТАП 2: Структурирование мыслей через модель =====
+ structured_raw = await structure_thoughts(transcribed_text)
- # 3. Подготовка текста для отправки (вырезаем тег )
- clean_text = re.sub(r".*?", "", agent_response, flags=re.DOTALL).strip()
- formatted_text = (
- f"🗣 Вы сказали:\n{safe_transcribed}\n\n"
- f"🎯 Майа // Структурированный отклик:\n\n"
- f"{clean_text}"
+ # Извлекаем теги метаданных и озвучки
+ m_obs = re.search(
+ r"",
+ structured_raw
)
-
- if len(formatted_text) > 4000:
- await status_msg.edit_text(formatted_text[:4000], parse_mode="HTML")
- await message.reply(formatted_text[4000:], parse_mode="HTML")
- else:
- await status_msg.edit_text(formatted_text, parse_mode="HTML")
+ obs_section = m_obs.group(1) if m_obs else "HomeLab"
+ obs_subfolder = m_obs.group(2) if m_obs else "General"
+ obs_filename = m_obs.group(3) if m_obs else f"note_{int(datetime.now().timestamp())}.md"
+ obs_title = m_obs.group(4) if m_obs else "Голосовая мысль"
- # 4. Синтез и отправка голосового ответа
+ # Очищаем структурированный текст для отображения в Telegram
+ clean_structure = re.sub(r"", "", structured_raw, flags=re.DOTALL)
+ clean_structure = re.sub(r".*?", "", clean_structure, flags=re.DOTALL).strip()
+
+ # Редактируем статусное сообщение: сырой текст заменяется на красивую структуру!
+ formatted_structure = f"🎯 Майа // Структурированный отклик:\n\n{clean_structure}"
+ if len(formatted_structure) > 4000:
+ await status_msg.edit_text(formatted_structure[:4000], parse_mode="HTML")
+ else:
+ await status_msg.edit_text(formatted_structure, parse_mode="HTML")
+
+ # ===== ЭТАП 3: Физические действия (Obsidian + Команды) отдельным сообщением! =====
+ # 1. Запись в Obsidian
+ obsidian_res = save_to_obsidian(obs_section, obs_subfolder, obs_filename, obs_title, clean_structure)
+
+ # 2. Выполнение команд (если были)
+ commands_result = await execute_voice_commands_if_any(transcribed_text)
+
+ # 3. Отправка отчета о действиях ОТДЕЛЬНЫМ сообщением
+ action_report = f"🗄️ Действия и фиксация:\n• {obsidian_res}"
+ if commands_result and commands_result.strip() != "НЕТ_КОМАНД":
+ action_report += f"\n\n⚙️ Результат выполнения:\n{commands_result.strip()}"
+
+ await message.reply(action_report, parse_mode="HTML")
+
+ # ===== 4. Голосовой отклик голосом Шаде =====
try:
- voice_bytes = await text_to_speech_bytes(agent_response)
+ voice_bytes = await text_to_speech_bytes(structured_raw)
if voice_bytes:
voice_file = BufferedInputFile(voice_bytes, filename="reply_voice.mp3")
await message.reply_voice(voice=voice_file, caption="🎙 Голосовой отклик Майи")