Files
call-center/services/voice_adapter_service/app.py
T
Yera AllandClaude Opus 4.6 6798320209 fix: remove dead code duplicates, add SQL LIMIT across all services
- Remove duplicate function definitions with hardcoded "AI-оператор" strings
  (ai_voice_runtime, ai_orchestrator, voice_name_config, voice.py)
- Remove unreachable dead code after return in ai_voice_runtime
- Add SQL LIMIT to 17 unbounded queries across 12 services to prevent OOM
- Move Python-side filtering to SQL WHERE in reporting_service
- Downgrade 19 logger.warning to logger.info for normal-flow events in media_runtime

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 12:04:26 +05:00

129 lines
4.3 KiB
Python

from __future__ import annotations
import json
import os
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from services.shared.core import Role, new_id, utc_now_iso
from services.shared.db import get_session
from services.shared.models import HealthResponse, VoiceEventIn, VoiceEventOut
from services.shared.security import get_actor
from services.shared.sql_init import init_sql_schema
from services.shared.sql_models import VoiceEventRow
app = FastAPI(title="voice-adapter-service", version="1.0.0")
init_sql_schema()
def _trusted_service_subjects() -> set[str]:
raw = os.getenv("VOICE_ADAPTER_TRUSTED_SERVICE_SUBJECTS", "svc:asterisk-bridge,svc:ivr-service")
return {item.strip() for item in raw.split(",") if item.strip()}
def _is_trusted_service_actor(actor: dict) -> bool:
return (
actor.get("auth_source") == "service"
and str(actor.get("sub") or "").strip() in _trusted_service_subjects()
)
def _require_voice_event_ingest(actor: dict = Depends(get_actor)) -> dict:
if actor.get("auth_source") == "service":
if _is_trusted_service_actor(actor):
return actor
raise HTTPException(status_code=403, detail="Untrusted service subject")
if actor.get("role") in {Role.ADMIN.value, Role.SUPERVISOR.value, Role.OPERATOR.value}:
return actor
raise HTTPException(status_code=403, detail="Insufficient role")
def _require_voice_event_read(actor: dict = Depends(get_actor)) -> dict:
if actor.get("auth_source") == "service":
if _is_trusted_service_actor(actor):
return actor
raise HTTPException(status_code=403, detail="Untrusted service subject")
if actor.get("role") in {Role.ADMIN.value, Role.SUPERVISOR.value, Role.ANALYST.value}:
return actor
raise HTTPException(status_code=403, detail="Insufficient role")
def _to_out(row: VoiceEventRow) -> VoiceEventOut:
return VoiceEventOut(
event_id=row.event_id,
event_type=row.event_type,
call_id=row.call_id,
interaction_id=row.interaction_id,
source_event_id=row.source_event_id,
payload=json.loads(row.payload_json or "{}"),
created_at=row.created_at,
)
def _load_existing_by_source_event(session, source_event_id: str | None) -> VoiceEventRow | None:
if not source_event_id:
return None
return session.execute(
select(VoiceEventRow).where(VoiceEventRow.source_event_id == source_event_id)
).scalar_one_or_none()
@app.get("/health", response_model=HealthResponse)
def health() -> HealthResponse:
return HealthResponse(status="ok", service="voice-adapter-service")
@app.post("/integrations/voice/events", response_model=VoiceEventOut)
def ingest_voice_event(
payload: VoiceEventIn,
_: dict = Depends(_require_voice_event_ingest),
) -> VoiceEventOut:
session = get_session()
try:
existing = _load_existing_by_source_event(session, payload.source_event_id)
if existing is not None:
return _to_out(existing)
row = VoiceEventRow(
event_id=new_id("vev"),
event_type=payload.event_type,
call_id=payload.call_id,
interaction_id=payload.interaction_id,
source_event_id=payload.source_event_id,
payload_json=json.dumps(payload.payload, ensure_ascii=False),
created_at=utc_now_iso(),
)
session.add(row)
try:
session.commit()
except IntegrityError:
session.rollback()
existing = _load_existing_by_source_event(session, payload.source_event_id)
if existing is not None:
return _to_out(existing)
raise
session.refresh(row)
return _to_out(row)
finally:
session.close()
@app.get("/integrations/voice/events", response_model=list[VoiceEventOut])
def list_voice_events(
limit: int = 100,
_: dict = Depends(_require_voice_event_read),
) -> list[VoiceEventOut]:
session = get_session()
try:
rows = session.execute(
select(VoiceEventRow).order_by(VoiceEventRow.id.desc()).limit(max(limit, 1))
).scalars().all()
return [_to_out(r) for r in reversed(rows)]
finally:
session.close()