"""Дирижёр — главный оркестратор VoIdeaAI. Принимает голосовой ввод пользователя, определяет намерение, направляет ролевому агенту, верифицирует ответ, возвращает пользователю. Самообучение через логирование, рейтинг и историю успешных кейсов. """ import json import time from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.agents.base import AgentResult, AgentStatus, BaseAgent from app.agents.conductor_storage import ( auto_tune_user, check_suggested_command, get_recent_history, get_similar_successful, log_interaction, ) from app.agents.role_agents import ( ALL_ROLE_AGENTS, VERIFICATION_PROMPT, RoleAgent, run_role_agent, ) from app.agents.vad import should_process_audio from app.agents.wake_word import strip_wake_word, has_wake_word from app.services.llm_service import chat_completion from app.services.pipeline_service import PipelineService from app.services.session_service import create_session, update_session_title class ConductorAgent(BaseAgent): name = "Дирижёр" version = "1.0.0" description = ( "Главный оркестратор. Принимает голосовой/текстовый ввод пользователя, " "определяет намерение, направляет специализированному агенту, " "верифицирует ответ и возвращает пользователю. Самообучение через рейтинг." ) async def run(self, context: dict[str, Any] | None = None) -> AgentResult: return AgentResult( success=True, message="Дирижёр: запущен. Используйте /api/v1/voice/chat для взаимодействия.", ) async def process( self, user_input: str, db: AsyncSession | None = None, user_id: str | None = None, session_id: str | None = None, vad_enabled: bool | None = None, wake_word_detected: bool | None = None, audio_duration_ms: int | None = None, pipeline_mode: str | None = None, ) -> dict[str, Any]: """Process user input through the full conductor pipeline. Args: user_input: User's text input db: Optional DB session for self-learning user_id: Optional user ID for history session_id: Optional session ID. Auto-creates if not provided. vad_enabled: Whether VAD was used client-side wake_word_detected: Whether wake word was detected client-side audio_duration_ms: Duration of audio input in ms pipeline_mode: Pipeline mode override ("fast" | "full" | "off") Returns: Dict with response, agent_name, confidence, verification_status, interaction_id, session_id """ start = time.time() pipeline_service = PipelineService(db) if db else None stages_log: list[dict] = [] # ── 0. VAD filter ── if audio_duration_ms is not None: should_process, skip_reason = should_process_audio( audio_duration_ms, {"enabled": vad_enabled if vad_enabled is not None else True} ) if not should_process: if pipeline_service: await pipeline_service.record_stat( user_id=user_id, stage="vad", passed=False, reason=skip_reason, duration_ms=0, ) return { "response": "", "agent_name": "", "agent_description": "", "confidence": 0, "verification_status": "skipped", "processing_time_ms": 0, "interaction_id": "", "session_id": session_id or "", "suggested_command": None, } # ── 1. Wake word stripping ── if wake_word_detected: cleaned = strip_wake_word(user_input) if cleaned: user_input = cleaned # ── 2. Авто-создание сессии ── is_new_session = False if db and user_id and not session_id: session = await create_session(db, user_id) session_id = str(session.id) is_new_session = True # ── Подготовка контекста с историей сессии ── agent_names = [ {"name": a.name, "description": a.description} for a in ALL_ROLE_AGENTS ] context_parts = [] if db and user_id: history = await get_recent_history(db, user_id, limit=5) if history: context_parts.append("Недавние обсуждения:\n" + "\n".join( f"[{h['agent']}]: {h['input']} → {h['response'][:100]}" for h in history )) similar = await get_similar_successful(db, user_input) if similar: context_parts.append("Похожие успешные кейсы:\n" + "\n".join( f"Было: {s['input'][:100]}, Ответ: {s['response'][:100]}" for s in similar )) llm_context = "\n\n".join(context_parts) if context_parts else None # ── 3. Выбор агента ── route_start = time.time() selected_agent = await self._route(user_input, agent_names, llm_context) if not selected_agent: selected_agent = "Бизнес-аналитик" route_elapsed = (time.time() - route_start) * 1000 agent = next( (a for a in ALL_ROLE_AGENTS if a.name == selected_agent), ALL_ROLE_AGENTS[0], ) stages_log.append({"stage": "routing", "passed": True, "duration_ms": route_elapsed}) if pipeline_service: await pipeline_service.record_stat( user_id=user_id, stage="routing", passed=True, duration_ms=int(route_elapsed), ) # ── 4. Генерация ответа ── response = await run_role_agent(agent, user_input, llm_context) if not response: response = "Не удалось обработать запрос. Проверьте API ключи." # ── 5. Верификация ответа ── verify_start = time.time() verification = await self._verify(response, user_input) verify_elapsed = (time.time() - verify_start) * 1000 confidence = verification.get("confidence", 50) issues = verification.get("issues", []) corrected = verification.get("corrected", "") if corrected: response = corrected verification_status = "issues_found" elif confidence >= 80: verification_status = "verified" elif confidence >= 50: verification_status = "warning" else: verification_status = "needs_clarification" response = ( "Извините, я не до конца уверен в ответе. " "Не могли бы вы уточнить свой запрос?\n\n" f"Вот что я понял: {response[:300]}" ) stages_log.append({"stage": "verification", "passed": confidence >= 50, "duration_ms": verify_elapsed}) if pipeline_service: await pipeline_service.record_stat( user_id=user_id, stage="verification", passed=confidence >= 50, reason=None if confidence >= 50 else "low_confidence", duration_ms=int(verify_elapsed), ) elapsed = (time.time() - start) * 1000 # ── 6. Логирование ── interaction_id = "" if db: interaction_id = await log_interaction( db=db, user_id=user_id, session_id=session_id, input_text=user_input[:1000], detected_intent=selected_agent, selected_agent=selected_agent, response_text=response, processing_time_ms=elapsed, confidence=confidence, verification_status=verification_status, was_auto_routed=True, context={"issues": issues, "stages": stages_log} if issues else None, ) # ── 7. Title generation для новой сессии ── if db and session_id and is_new_session: title_start = time.time() title = await self._generate_title(user_input) title_elapsed = (time.time() - title_start) * 1000 await update_session_title(db, session_id, title) if pipeline_service: await pipeline_service.record_stat( user_id=user_id, stage="title_generation", passed=True, duration_ms=int(title_elapsed), ) # ── 8. Auto-tuning (каждые ~5 взаимодействий) ── if db and user_id: suggested_command = await check_suggested_command(db, user_id) return { "response": response, "agent_name": selected_agent, "agent_description": agent.description, "confidence": confidence, "verification_status": verification_status, "processing_time_ms": round(elapsed, 1), "interaction_id": interaction_id, "session_id": session_id or "", "suggested_command": suggested_command, } async def _route( self, user_input: str, agent_names: list[dict[str, str]], context: str | None = None, ) -> str | None: system = "Ты — дирижёр умных ассистентов. Определи лучшего агента для ответа." if context: system += f"\n\nКонтекст:\n{context}" agent_list = "\n".join(f"- {a['name']}: {a['description']}" for a in agent_names) system += f"\n\nДоступные агенты:\n{agent_list}\n\nОтветь ТОЛЬКО именем агента." messages = [ {"role": "system", "content": system}, {"role": "user", "content": user_input}, ] result = await chat_completion(messages, temperature=0.3, max_tokens=64) if not result: return None result = result.strip().strip('"').strip("'") valid = {a["name"] for a in agent_names} return result if result in valid else None async def _verify( self, response: str, original_input: str, ) -> dict[str, Any]: messages = [ {"role": "system", "content": VERIFICATION_PROMPT}, { "role": "user", "content": ( f"Запрос пользователя: {original_input}\n\n" f"Ответ агента: {response}" ), }, ] result = await chat_completion(messages, temperature=0.2, max_tokens=1024) if not result: return {"status": "verified", "confidence": 80, "issues": [], "corrected": ""} try: cleaned = result.strip() if cleaned.startswith("```"): cleaned = cleaned.split("\n", 1)[-1].rsplit("\n", 1)[0] return json.loads(cleaned) except (json.JSONDecodeError, KeyError): return {"status": "verified", "confidence": 80, "issues": [], "corrected": ""} async def _generate_title(self, user_input: str) -> str: messages = [ {"role": "system", "content": ( "Ты — ассистент, который придумывает короткие заголовки для обсуждений. " "Ответь одним предложением (до 7 слов), отражающим суть запроса." )}, {"role": "user", "content": f"Придумай заголовок для обсуждения этого запроса:\n{user_input}"}, ] result = await chat_completion(messages, temperature=0.3, max_tokens=30) if result: return result.strip().strip('"').strip("'")[:255] return "Новое обсуждение" async def health_check(self) -> bool: return True