291 lines
8.8 KiB
Python
291 lines
8.8 KiB
Python
"""Voice transcription and chat API routes for VoIdeaAI."""
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
import asyncio
|
|
|
|
from app.agents.conductor_agent import ConductorAgent
|
|
from app.agents.conductor_storage import rate_interaction, get_session_history
|
|
from app.agents.role_agents import ALL_ROLE_AGENTS
|
|
from app.core.dependencies import get_current_user, get_db
|
|
from app.models.conductor import ConductorInteraction
|
|
from app.models.user import User
|
|
from app.schemas.voice import (
|
|
ChatRequest,
|
|
ChatResponse,
|
|
CommandCreate,
|
|
CommandResponse,
|
|
CreateSessionRequest,
|
|
RateRequest,
|
|
SaveIdeaRequest,
|
|
SaveIdeaResponse,
|
|
SessionResponse,
|
|
)
|
|
from app.services.command_service import (
|
|
create_command,
|
|
delete_command,
|
|
list_commands,
|
|
)
|
|
from app.services.idea_service import IdeaService
|
|
from app.services.session_service import (
|
|
create_session,
|
|
delete_session,
|
|
get_session,
|
|
list_sessions,
|
|
update_session_idea,
|
|
)
|
|
from app.services.punctuation_service import restore_punctuation
|
|
from app.services.whisper_service import transcribe
|
|
|
|
router = APIRouter()
|
|
_conductor = ConductorAgent()
|
|
|
|
|
|
@router.get("/stream/{interaction_id}")
|
|
async def stream_response(
|
|
interaction_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""SSE endpoint that streams the response text for a given interaction in chunks."""
|
|
from sqlalchemy import select
|
|
|
|
result = await db.execute(
|
|
select(ConductorInteraction).where(ConductorInteraction.id == interaction_id)
|
|
)
|
|
interaction = result.scalar_one_or_none()
|
|
if not interaction:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Interaction not found")
|
|
if str(interaction.user_id) != str(user.id):
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied")
|
|
|
|
response_text = interaction.response_text or ""
|
|
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
async def event_stream():
|
|
chunk_size = 50
|
|
for i in range(0, len(response_text), chunk_size):
|
|
chunk = response_text[i:i + chunk_size]
|
|
yield f"data: {chunk}\n\n"
|
|
await asyncio.sleep(0.02)
|
|
yield "data: [DONE]\n\n"
|
|
|
|
return StreamingResponse(
|
|
event_stream(),
|
|
media_type="text/event-stream",
|
|
headers={
|
|
"Cache-Control": "no-cache",
|
|
"Connection": "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/transcribe")
|
|
async def transcribe_audio(file: UploadFile):
|
|
audio_data = await file.read()
|
|
if not audio_data:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Empty audio file",
|
|
)
|
|
|
|
text = await transcribe(audio_data, file.filename or "audio.webm")
|
|
if not text:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="Transcription failed. Check AI provider API key.",
|
|
)
|
|
|
|
text = restore_punctuation(text)
|
|
|
|
return {"text": text}
|
|
|
|
|
|
@router.post("/chat", response_model=ChatResponse)
|
|
async def chat(
|
|
body: ChatRequest,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await _conductor.process(
|
|
user_input=body.text,
|
|
db=db,
|
|
user_id=str(user.id),
|
|
session_id=body.session_id,
|
|
vad_enabled=body.vad_enabled,
|
|
wake_word_detected=body.wake_word_detected,
|
|
audio_duration_ms=body.audio_duration_ms,
|
|
pipeline_mode=body.pipeline_mode,
|
|
)
|
|
return ChatResponse(**result)
|
|
|
|
|
|
@router.get("/sessions", response_model=list[SessionResponse])
|
|
async def list_user_sessions(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
status_filter: str | None = None,
|
|
):
|
|
sessions = await list_sessions(db, str(user.id), status=status_filter)
|
|
return [
|
|
SessionResponse(
|
|
id=str(s.id),
|
|
title=s.title,
|
|
status=s.status,
|
|
idea_id=str(s.idea_id) if s.idea_id else None,
|
|
created_at=s.created_at,
|
|
updated_at=s.updated_at,
|
|
)
|
|
for s in sessions
|
|
]
|
|
|
|
|
|
@router.get("/sessions/{session_id}", response_model=SessionResponse)
|
|
async def get_session_detail(
|
|
session_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
session = await get_session(db, session_id)
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
if str(session.user_id) != str(user.id):
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
return SessionResponse(
|
|
id=str(session.id),
|
|
title=session.title,
|
|
status=session.status,
|
|
idea_id=str(session.idea_id) if session.idea_id else None,
|
|
created_at=session.created_at,
|
|
updated_at=session.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/sessions/{session_id}/history")
|
|
async def get_session_history_endpoint(
|
|
session_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
session = await get_session(db, session_id)
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
if str(session.user_id) != str(user.id):
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
return await get_session_history(db, session_id)
|
|
|
|
|
|
@router.delete("/sessions/{session_id}")
|
|
async def delete_user_session(
|
|
session_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
session = await get_session(db, session_id)
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
if str(session.user_id) != str(user.id):
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
ok = await delete_session(db, session_id)
|
|
if not ok:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.post("/rate")
|
|
async def rate(
|
|
body: RateRequest,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
success = await rate_interaction(db, body.interaction_id, body.rating)
|
|
if not success:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Interaction not found",
|
|
)
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.post("/save-idea", response_model=SaveIdeaResponse)
|
|
async def save_idea(
|
|
body: SaveIdeaRequest,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
session = await get_session(db, body.session_id)
|
|
if not session:
|
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
if str(session.user_id) != str(user.id):
|
|
raise HTTPException(status_code=403, detail="Access denied")
|
|
|
|
history = await get_session_history(db, body.session_id)
|
|
title = session.title or "Новое обсуждение"
|
|
|
|
dialogue_lines = [
|
|
f"Пользователь: {h['input']}\n{ h['agent']}: {h['response']}"
|
|
for h in history
|
|
]
|
|
content = "\n\n".join(dialogue_lines) if dialogue_lines else title
|
|
|
|
idea_service = IdeaService(db)
|
|
tags = ["voice", "ai-assisted"]
|
|
idea = await idea_service.create(
|
|
user_id=str(user.id),
|
|
title=title[:255],
|
|
content=content,
|
|
tags=tags,
|
|
is_public=False,
|
|
)
|
|
ok = await update_session_idea(db, body.session_id, str(idea.id))
|
|
|
|
return SaveIdeaResponse(
|
|
idea_id=str(idea.id),
|
|
title=idea.title,
|
|
exported=False,
|
|
)
|
|
|
|
|
|
@router.get("/commands", response_model=list[CommandResponse])
|
|
async def list_user_commands(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await list_commands(db, str(user.id))
|
|
|
|
|
|
@router.post("/commands", response_model=CommandResponse)
|
|
async def create_user_command(
|
|
body: CommandCreate,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await create_command(
|
|
db, str(user.id), body.phrase, body.action, body.agent_name
|
|
)
|
|
|
|
|
|
@router.delete("/commands/{command_id}")
|
|
async def delete_user_command(
|
|
command_id: str,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
ok = await delete_command(db, command_id, str(user.id))
|
|
if not ok:
|
|
raise HTTPException(status_code=404, detail="Command not found")
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.get("/agents", response_model=list[dict])
|
|
async def list_role_agents():
|
|
return [
|
|
{"name": a.name, "description": a.description}
|
|
for a in ALL_ROLE_AGENTS
|
|
]
|