feat(voice): add native query_bitrix24 tool to Maya voice agent

This commit is contained in:
Admin 2026-09-04 15:04:17 +04:00
parent a47d1eee77
commit 6c791c1618

View file

@ -4,6 +4,8 @@ import logging
import asyncio import asyncio
import sqlite3 import sqlite3
import subprocess import subprocess
import urllib.request
import json
from datetime import datetime from datetime import datetime
from aiogram import Router, F, Bot from aiogram import Router, F, Bot
from aiogram.types import Message, BufferedInputFile from aiogram.types import Message, BufferedInputFile
@ -18,8 +20,55 @@ router = Router()
WORKING_GEMINI_KEY = "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk" WORKING_GEMINI_KEY = "AIzaSyAJMfQ_mRZ1picRXVFNaeFJ7fpAkZxUKyk"
genai.configure(api_key=WORKING_GEMINI_KEY) genai.configure(api_key=WORKING_GEMINI_KEY)
# Вебхук Битрикс24 холдинга (ecomsystems.bitrix24.ru)
B24_WEBHOOK = "https://ecomsystems.bitrix24.ru/rest/1424/yqt0a18d9qwf0l4r/"
def query_bitrix24(action: str, task_id: int = 0, search_text: str = "") -> str:
"""Инструмент для работы с Битрикс24 холдинга (ecomsystems.bitrix24.ru).
Александр Тырин: ID 1424, Илья Гавришин (Генеральный директор): ID 1.
Доступные действия (action):
- 'my_tasks': список активных задач Александра Тырина.
- 'search_tasks': поиск задач по ключевому слову search_text в названии.
- 'task_details': детальная информация по задаче с ID task_id.
- 'task_comments': все комментарии к задаче с ID task_id (включая комментарии гендира и юристов).
"""
logger.info(f"🏢 [Bitrix24 Query] action={action}, task_id={task_id}, search_text='{search_text}'")
try:
if action == "my_tasks":
url = f"{B24_WEBHOOK}tasks.task.list.json"
payload = json.dumps({
"filter": {"RESPONSIBLE_ID": 1424, "!STATUS": 5},
"select": ["ID", "TITLE", "STATUS", "CREATED_DATE", "DEADLINE"]
}).encode('utf-8')
elif action == "search_tasks":
url = f"{B24_WEBHOOK}tasks.task.list.json"
payload = json.dumps({
"filter": {"%TITLE": search_text},
"select": ["ID", "TITLE", "STATUS", "RESPONSIBLE_ID", "CREATED_BY"]
}).encode('utf-8')
elif action == "task_details":
url = f"{B24_WEBHOOK}tasks.task.get.json"
payload = json.dumps({
"taskId": task_id,
"select": ["ID", "TITLE", "DESCRIPTION", "STATUS", "CREATED_BY", "RESPONSIBLE_ID"]
}).encode('utf-8')
elif action == "task_comments":
url = f"{B24_WEBHOOK}task.commentitem.getlist.json"
payload = json.dumps([task_id]).encode('utf-8')
else:
return f"Неизвестное действие: {action}"
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode('utf-8'))
res = data.get("result", {})
return json.dumps(res, ensure_ascii=False)[:3500]
except Exception as e:
logger.error(f"❌ Bitrix24 Error: {e}")
return f"Ошибка обращения к Битрикс24: {e}"
def execute_bash_command(command: str) -> str: def execute_bash_command(command: str) -> str:
"""Выполняет bash-команду на локальном Linux хосте (DockerServer) или по SSH на узлах сети (MacMini 192.168.10.167, Synology 192.168.10.105, MikroTik 192.168.10.215, NanoPi 192.168.10.1) и возвращает результат вывода.""" """Выполняет bash-команду на хосте или по SSH и возвращает результат вывода."""
logger.info(f"⚡️ [Voice Agent Command] Executing bash: {command}") logger.info(f"⚡️ [Voice Agent Command] Executing bash: {command}")
try: try:
res = subprocess.run( res = subprocess.run(
@ -65,21 +114,19 @@ def log_voice_to_db(user_id, username, msg_type, duration, text):
async def text_to_speech_bytes(text: str) -> bytes: async def text_to_speech_bytes(text: str) -> bytes:
"""Синтез качественного женского голосового ответа на русском через Edge TTS с глубоким тембром""" """Синтез качественного женского голосового ответа на русском через Edge TTS с глубоким тембром"""
# Если в тексте есть тег <voice>...</voice>, озвучиваем именно его
match = re.search(r"<voice>(.*?)</voice>", text, re.DOTALL | re.IGNORECASE) match = re.search(r"<voice>(.*?)</voice>", text, re.DOTALL | re.IGNORECASE)
if match: if match:
speech_text = match.group(1).strip() speech_text = match.group(1).strip()
else: else:
# Иначе берем выжимку до 350 символов
speech_text = text[:350] if len(text) > 350 else text speech_text = text[:350] if len(text) > 350 else text
# Очищаем от Markdown и спецсимволов для чистоты звучания # Очищаем Markdown для чистого звука
for ch in ["*", "#", "_", "`", "~", ">", "•", "🎯", "⚙️", "📋", "💡", "🗄️", "🚨", "⚠️", "✅", "❌"]: for ch in ["*", "#", "_", "`", "~", ">", "•", "🎯", "⚙️", "📋", "💡", "🗄️", "🚨", "⚠️", "✅", "❌"]:
speech_text = speech_text.replace(ch, "") speech_text = speech_text.replace(ch, "")
speech_text = re.sub(r"\[.*?\]\(.*?\)", "", speech_text).strip() speech_text = re.sub(r"\[.*?\]\(.*?\)", "", speech_text).strip()
if not speech_text: if not speech_text:
speech_text = "Приняла, Александр. Всё структурировано и зафиксировано." speech_text = "Приняла, Александр. Всё проанализировано и готово."
communicate = edge_tts.Communicate(speech_text, "ru-RU-SvetlanaNeural", pitch="-8Hz", rate="-3%") communicate = edge_tts.Communicate(speech_text, "ru-RU-SvetlanaNeural", pitch="-8Hz", rate="-3%")
out_path = f"/tmp/tts_{os.getpid()}_{int(datetime.now().timestamp())}.mp3" out_path = f"/tmp/tts_{os.getpid()}_{int(datetime.now().timestamp())}.mp3"
@ -92,7 +139,7 @@ async def text_to_speech_bytes(text: str) -> bytes:
return audio_bytes return audio_bytes
def process_audio_with_gemini(audio_path: str) -> str: def process_audio_with_gemini(audio_path: str) -> str:
"""Прямое транскрибирование аудио через Gemini Multimodal API""" """Прямое транскрибирование аудио через Gemini Multimodal Audio API"""
try: try:
logger.info(f"Выгрузка {audio_path} в Gemini Multimodal Audio API...") logger.info(f"Выгрузка {audio_path} в Gemini Multimodal Audio API...")
audio_file = genai.upload_file(path=audio_path) audio_file = genai.upload_file(path=audio_path)
@ -116,24 +163,31 @@ def process_audio_with_gemini(audio_path: str) -> str:
return "" return ""
async def execute_antigravity_agent(user_text: str) -> str: async def execute_antigravity_agent(user_text: str) -> str:
"""Обработка команды и мыслей агентом Майа с систематизацией и правом исполнения bash-команд""" """Обработка команды и мыслей агентом Майа с систематизацией, Битрикс24 и правом исполнения bash-команд"""
system_instruction = ( system_instruction = (
"Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра. " "Ты — Майа (Maya / Antigravity Lead Agent), суперинтеллектуальный помощник Александра с полными правами инженера. "
"Александр обращается к тебе голосом в Телеграмме или надиктованной аудиозаписью с диктофона. " "Александр обращается к тебе голосом в Телеграмме или надиктованной аудиозаписью с диктофона. "
"Твоя задача — быть идеальным личным инженером и аналитиком:\n\n" "Твои возможности:\n\n"
"1. СТРУКТУРИРОВАНИЕ МЫСЛЕЙ (если Александр надиктовал идеи, мысли, список дел или сумбурный поток):\n" "1. РАБОЧИЕ ЗАДАЧИ В БИТРИКС24:\n"
" Разложи всё по четким блокам:\n" " Если Александр просит проверить задачи, комментарии руководства, поручения в Битрикс24 — "
" 🎯 Суть: краткая главная мысль в 1-2 предложениях.\n" " ОБЯЗАТЕЛЬНО используй функцию query_bitrix24!\n"
" 📋 Задачи к выполнению (Action Items): конкретные пункты с галочками [ ].\n" " - Поиск задачи по ключевым словам: query_bitrix24(action='search_tasks', search_text='...')\n"
" 💡 Идеи в копилку / Бэклог: новые задумки, архитектурные гипотезы.\n" " - Список открытых задач Александра (ID 1424): query_bitrix24(action='my_tasks')\n"
" 🗄️ Куда зафиксировать: в какую заметку Obsidian (HomeLab или Work) это стоит сохранить.\n\n" " - Подробности задачи: query_bitrix24(action='task_details', task_id=...)\n"
"2. СИСТЕМНЫЕ КОМАНДЫ (если Александр просит проверить ресурсы, диски, сеть, контейнеры, запустить что-то):\n" " - Комментарии к задаче: query_bitrix24(action='task_comments', task_id=...)\n"
" ОБЯЗАТЕЛЬНО используй execute_bash_command! Выполни команду, проанализируй вывод и дай четкий отчет.\n" " Проанализируй суть, выдели позицию Ильи Гавришина (ID 1, гендир) и юристов, дай свой ясный вывод и предложи шаги.\n\n"
" Узлы сети: NanoPi R3S (root@192.168.10.1), Synology (matrixhasyou@192.168.10.105), MacMini (matrixhasyou@192.168.10.167), MikroTik (matrixhasyou@192.168.10.215).\n\n" "2. СИСТЕМНЫЕ КОМАНДЫ (Docker, сеть, диски, серверы):\n"
"3. ГОЛОСОВОЙ ОТКЛИК ДЛЯ ОЗВУЧКИ:\n" " Используй execute_bash_command для выполнения консольных команд. Узлы: NanoPi (192.168.10.1), Synology (192.168.10.105), MacMini (192.168.10.167).\n\n"
"3. СТРУКТУРИРОВАНИЕ МЫСЛЕЙ (если Александр надиктовал идеи или планы):\n"
" Разложи всё по блокам:\n"
" 🎯 Суть: главная мысль в 1-2 предложениях.\n"
" 📋 Задачи (Action Items): конкретные пункты [ ].\n"
" 💡 Идеи в копилку: новые мысли и гипотезы.\n"
" 🗄️ Фиксация в Obsidian: куда сохранить (HomeLab или Work).\n\n"
"4. ГОЛОСОВОЙ ОТКЛИК ДЛЯ ОЗВУЧКИ:\n"
" В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег <voice>...</voice>.\n" " В САМОМ КОНЦЕ ответа ОБЯЗАТЕЛЬНО добавь тег <voice>...</voice>.\n"
" Внутри него напиши короткое, живое, теплое и емкое резюме от Майи (1-3 предложения), которое будет озвучено вслух! " " Внутри него напиши короткое, емкое, живое и теплое резюме от Майи (1-3 предложения), которое будет озвучено вслух! "
" Внутри <voice> пиши только чистый русский текст без звездочек, эмодзи, списков и Markdown." " Внутри <voice> пиши только чистый русский текст без звездочек, эмодзи и Markdown."
) )
models_to_try = ['gemini-2.5-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite'] models_to_try = ['gemini-2.5-flash', 'gemini-3.5-flash', 'gemini-3.5-flash-lite']
@ -145,7 +199,7 @@ async def execute_antigravity_agent(user_text: str) -> str:
model = genai.GenerativeModel( model = genai.GenerativeModel(
model_name=model_name, model_name=model_name,
system_instruction=system_instruction, system_instruction=system_instruction,
tools=[execute_bash_command] tools=[query_bitrix24, execute_bash_command]
) )
chat = model.start_chat(enable_automatic_function_calling=True) chat = model.start_chat(enable_automatic_function_calling=True)
response = chat.send_message(user_text) response = chat.send_message(user_text)
@ -164,7 +218,6 @@ async def handle_voice_message(message: Message, bot: Bot):
if int(message.from_user.id) != int(ADMIN_USER_ID): if int(message.from_user.id) != int(ADMIN_USER_ID):
return return
# Определяем тип медиафайла
msg_type = "VOICE" msg_type = "VOICE"
duration = 0 duration = 0
filename = "audio.ogg" filename = "audio.ogg"
@ -192,7 +245,7 @@ async def handle_voice_message(message: Message, bot: Bot):
mime = (doc.mime_type or "").lower() mime = (doc.mime_type or "").lower()
allowed_exts = ('.mp3', '.m4a', '.ogg', '.wav', '.aac', '.flac', '.3gp', '.oga', '.wma') allowed_exts = ('.mp3', '.m4a', '.ogg', '.wav', '.aac', '.flac', '.3gp', '.oga', '.wma')
if not (mime.startswith("audio/") or fn.endswith(allowed_exts)): if not (mime.startswith("audio/") or fn.endswith(allowed_exts)):
return # Не аудиозапись, пропускаем для других хэндлеров return
file_id = doc.file_id file_id = doc.file_id
ext = os.path.splitext(doc.file_name or "rec.m4a")[1] or ".m4a" ext = os.path.splitext(doc.file_name or "rec.m4a")[1] or ".m4a"
filename = f"doc_{file_id}{ext}" filename = f"doc_{file_id}{ext}"
@ -201,7 +254,7 @@ async def handle_voice_message(message: Message, bot: Bot):
else: else:
return return
status_msg = await message.reply("🎙 <i>Слушаю и анализирую аудиозапись...</i>", parse_mode="HTML") status_msg = await message.reply("🎙 <i>Слушаю и выполняю команду...</i>", parse_mode="HTML")
await bot.send_chat_action(message.chat.id, "typing") await bot.send_chat_action(message.chat.id, "typing")
try: try:
@ -218,24 +271,23 @@ async def handle_voice_message(message: Message, bot: Bot):
if transcribed_text: if transcribed_text:
log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), transcribed_text) log_voice_to_db(message.from_user.id, message.from_user.username, msg_type, int(duration), transcribed_text)
await status_msg.edit_text(f"🗣 <b>Услышано:</b> «<i>{transcribed_text}</i>»\n\n⚙️ <b>Майа систематизирует и готовит ответ...</b>", parse_mode="HTML") await status_msg.edit_text(f"🗣 <b>Команда принята:</b> «<i>{transcribed_text}</i>»\n\n⚙️ <b>Майа работает над задачей...</b>", parse_mode="HTML")
await bot.send_chat_action(message.chat.id, "record_voice") await bot.send_chat_action(message.chat.id, "record_voice")
# 2. Исполнение Maya Lead Agent (систематизация + команды хоста) # 2. Исполнение Maya Lead Agent (Битрикс24, bash, анализ)
agent_response = await execute_antigravity_agent(transcribed_text) agent_response = await execute_antigravity_agent(transcribed_text)
# 3. Подготовка текста для отправки (вырезаем тег <voice> из визуального сообщения) # 3. Подготовка текста для отправки (вырезаем тег <voice>)
clean_text = re.sub(r"<voice>.*?</voice>", "", agent_response, flags=re.DOTALL).strip() clean_text = re.sub(r"<voice>.*?</voice>", "", agent_response, flags=re.DOTALL).strip()
formatted_text = f"🎯 <b>Майа // Структурированный отклик:</b>\n\n{clean_text}" formatted_text = f"🎯 <b>Майа // Отчет исполнения:</b>\n\n{clean_text}"
# Разделяем длинный текст Telegram если нужно (TG limit 4096)
if len(formatted_text) > 4000: if len(formatted_text) > 4000:
await status_msg.edit_text(formatted_text[:4000], parse_mode="HTML") await status_msg.edit_text(formatted_text[:4000], parse_mode="HTML")
await message.reply(formatted_text[4000:], parse_mode="HTML") await message.reply(formatted_text[4000:], parse_mode="HTML")
else: else:
await status_msg.edit_text(formatted_text, parse_mode="HTML") await status_msg.edit_text(formatted_text, parse_mode="HTML")
# 4. Синтез и отправка голосовой заметки (Voice Note) # 4. Синтез и отправка голосового ответа
try: try:
voice_bytes = await text_to_speech_bytes(agent_response) voice_bytes = await text_to_speech_bytes(agent_response)
if voice_bytes: if voice_bytes: