feat(voice): add native query_bitrix24 tool to Maya voice agent
This commit is contained in:
parent
a47d1eee77
commit
6c791c1618
1 changed files with 83 additions and 31 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue