Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
"""Дирижёр — главный оркестратор 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
|
||||
Reference in New Issue
Block a user