from __future__ import annotations from fastapi import HTTPException from sqlalchemy import select from services.shared.core import utc_now_iso from services.shared.db import get_session from services.shared.models import ( EscalationOut, EscalationRequestIn, HealthResponse, VoiceAICallStateUpdateIn, VoiceAIHandoffRequestIn, VoiceAISummaryOut, VoiceCallBlindTransferIn, VoiceCallClaimIn, VoiceLiveCallOut, ) from services.shared.sql_models import AsteriskCallLinkRow, AsteriskEventLogRow from services.asterisk_bridge_service import call_control def _bridge_app(): import services.asterisk_bridge_service.app as bridge_app return bridge_app def list_live_calls(include_ended: bool = False, limit: int = 100) -> list[VoiceLiveCallOut]: bridge = _bridge_app() session = get_session() try: stmt = select(AsteriskCallLinkRow).order_by(AsteriskCallLinkRow.id.desc()) if not include_ended: stmt = stmt.where(AsteriskCallLinkRow.status != "ended") rows = session.execute(stmt).scalars().all() result: list[VoiceLiveCallOut] = [] for row in rows[: max(limit, 1)]: if not row.interaction_id: continue if not include_ended and bridge._call_is_ended(row): continue result.append(bridge._live_call_to_out(session, row)) return result finally: session.close() def list_recent_calls(limit: int = 20) -> list[VoiceLiveCallOut]: bridge = _bridge_app() session = get_session() try: rows = session.execute( select(AsteriskCallLinkRow) .where(AsteriskCallLinkRow.status == "ended") .order_by(AsteriskCallLinkRow.id.desc()) .limit(max(limit * 4, limit, 20)) ).scalars().all() now = bridge.datetime.now(bridge.timezone.utc) window_seconds = bridge._recent_calls_window_seconds() result: list[tuple[bridge.datetime, VoiceLiveCallOut]] = [] for row in rows: if not row.interaction_id: continue payload = bridge._live_call_to_out(session, row) transition_dt = bridge._parse_iso(payload.last_transition_at or row.ended_at or row.updated_at) if transition_dt is None: continue if (now - transition_dt).total_seconds() > window_seconds: continue result.append((transition_dt, payload)) result.sort(key=lambda item: item[0], reverse=True) return [item for _, item in result[: max(limit, 1)]] finally: session.close() def claim_live_call(call_id: str, body: VoiceCallClaimIn, actor: dict): return call_control.claim_live_call(call_id, body, actor) def hangup_live_call(call_id: str, actor: dict): return call_control.hangup_live_call(call_id, actor) def blind_transfer_live_call(call_id: str, body: VoiceCallBlindTransferIn, actor: dict): return call_control.blind_transfer_live_call(call_id, body, actor) def list_live_call_actions(call_id: str, limit: int, actor: dict): return call_control.list_live_call_actions(call_id, limit, actor) def get_live_call_ai_summary(call_id: str) -> VoiceAISummaryOut | None: bridge = _bridge_app() return bridge._voice_ai_summary_for_call(call_id) def escalate_live_call(call_id: str, body: EscalationRequestIn, actor: dict) -> EscalationOut: bridge = _bridge_app() return bridge._create_escalation(call_id, body, actor) def voice_ai_handoff_call(call_id: str, body: VoiceAIHandoffRequestIn, actor: dict) -> VoiceLiveCallOut: bridge = _bridge_app() return bridge._request_voice_ai_handoff(call_id, body, actor) def voice_ai_update_call_state(call_id: str, body: VoiceAICallStateUpdateIn, actor: dict) -> VoiceLiveCallOut: bridge = _bridge_app() return bridge._update_voice_ai_call_state(call_id, body, actor) def health() -> HealthResponse: return HealthResponse(status="ok", service="asterisk-bridge-service") def asterisk_status(): bridge = _bridge_app() snap = bridge._STATE.snapshot() status = "ok" if bridge._bridge_enabled() else "disabled" return bridge.AsteriskBridgeStatusOut( status=status, ami_connected=bool(snap["ami_connected"]), ami_host=bridge._ami_host() or None, last_event_at=snap["last_event_at"], queue_codes_loaded=sorted(bridge._queue_map().keys()), sftp_enabled=bridge._sftp_enabled(), bridge_auth_mode=bridge._bridge_auth_mode(), callcontrol_enabled=bridge._callcontrol_enabled(), webrtc_enabled=bridge._webrtc_enabled(), webrtc_ws_url=bridge._webrtc_ws_url() or None, ) def browser_softphone_config(actor: dict): bridge = _bridge_app() return bridge._browser_softphone_config_for_actor(actor) def list_asterisk_events(status: str | None = None, limit: int = 100): bridge = _bridge_app() session = get_session() try: stmt = select(AsteriskEventLogRow).order_by(AsteriskEventLogRow.id.desc()) if status: stmt = stmt.where(AsteriskEventLogRow.forward_status == status) rows = session.execute(stmt).scalars().all() return [bridge._to_out(row) for row in rows[:limit]] finally: session.close() def get_asterisk_event(bridge_event_id: str): bridge = _bridge_app() session = get_session() try: row = session.execute( select(AsteriskEventLogRow) .where(AsteriskEventLogRow.bridge_event_id == bridge_event_id) ).scalar_one_or_none() if row is None: raise HTTPException(status_code=404, detail="Asterisk bridge event not found") return bridge._to_out(row) finally: session.close() def retry_asterisk_event(bridge_event_id: str): bridge = _bridge_app() if not bridge._claim_bridge_event_for_processing(bridge_event_id): session = get_session() try: row = bridge._load_bridge_event_row(session, bridge_event_id) if row is None: raise HTTPException(status_code=404, detail="Asterisk bridge event not found") return bridge._to_out(row) finally: session.close() row = bridge._process_claimed_bridge_event(bridge_event_id) if row is None: raise HTTPException(status_code=404, detail="Asterisk bridge event not found") return bridge._to_out(row) def reconnect_asterisk(): bridge = _bridge_app() bridge._STATE.request_reconnect() return asterisk_status()