Initial commit: VoIdeaAI - voice-first AI idea assistant

This commit is contained in:
2026-05-13 12:51:42 +03:00
commit 688d043dad
421 changed files with 47915 additions and 0 deletions
+307
View File
@@ -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