diff --git a/services/asterisk_bridge_service/voice_ai.py b/services/asterisk_bridge_service/voice_ai.py index 4f4b4c9..1eed1a8 100644 --- a/services/asterisk_bridge_service/voice_ai.py +++ b/services/asterisk_bridge_service/voice_ai.py @@ -311,7 +311,7 @@ def _resolve_handoff_extension( target_level: str | None = None, tenant_id: str | None = None, required_skills: list[str] | None = None, -) -> tuple[str, str, str | None, str | None]: +) -> tuple[str, str, str | None, str | None, str | None]: bridge = _bridge_app() queue_code = _queue_code_for_queue_id(target_queue_id) or str(fallback_queue_code or "").strip() if not queue_code: @@ -327,7 +327,7 @@ def _resolve_handoff_extension( required_skills=required_skills, ) if reserved_agent: - return queue_code, reserved_agent["extension"], level, resolved_tenant_id + return queue_code, reserved_agent["extension"], level, resolved_tenant_id, reserved_agent["agent_id"] raise HTTPException(status_code=409, detail=f"No available {level} agent right now") extension = bridge._transfer_target_map().get(queue_code) @@ -336,7 +336,7 @@ def _resolve_handoff_extension( status_code=400, detail=f"Unknown transfer queue_code: {queue_code}", ) - return queue_code, extension, None, None + return queue_code, extension, None, None, None def _resolve_handoff_channel(session, link: AsteriskCallLinkRow) -> str: @@ -546,11 +546,33 @@ def request_handoff(call_id: str, body: VoiceAIHandoffRequestIn, actor: dict) -> if voice_session is None: raise HTTPException(status_code=404, detail="Voice AI session not found") - queue_code, target_extension, resolved_level, resolved_tenant_id = _resolve_handoff_extension( + queue_code, target_extension, resolved_level, resolved_tenant_id, reserved_agent_id = _resolve_handoff_extension( body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id, fallback_queue_code=_queue_code_for_queue_id(link.queue_id), call_id=call_id, ) + escalation: EscalationRow | None = None + if reserved_agent_id and target_extension != "7100": + now_reserved = utc_now_iso() + escalation = EscalationRow( + escalation_id=new_id("esc"), + call_id=call_id, + tenant_id=resolved_tenant_id, + from_level=str(link.current_level or "L1"), + to_level=resolved_level, + reason_code="AI_HANDOFF", + required_skills_json="[]", + priority=3, + status="ringing", + real_agent_id=reserved_agent_id, + assigned_agent_id=target_extension, + attempted_agent_ids_json=json.dumps([reserved_agent_id], ensure_ascii=False), + requested_at=now_reserved, + ) + session.add(escalation) + session.flush() + _append_escalation_timeline(escalation, link, action="escalation.agent_reserved", extra={"agent_id": reserved_agent_id}) + _emit_escalation_event(event_type="AgentReserved", escalation=escalation, link=link, extra={"agent_id": reserved_agent_id}) handoff_metadata = body.metadata or {} actor_user = str(actor.get("user") or actor.get("sub") or "ai-voice-runtime").strip() actor_role = str(actor.get("role") or "admin").strip() or "admin" @@ -597,15 +619,33 @@ def request_handoff(call_id: str, body: VoiceAIHandoffRequestIn, actor: dict) -> target_extension, body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id, ) - ami_result = bridge._ami_action( - "Redirect", - { - "Channel": channel, - "Context": bridge._transfer_context(), - "Exten": target_extension, - "Priority": 1, - }, - ) + try: + ami_result = bridge._ami_action( + "Redirect", + { + "Channel": channel, + "Context": bridge._transfer_context(), + "Exten": target_extension, + "Priority": 1, + }, + ) + except Exception as exc: + if escalation is not None: + try: + routing_release_by_agent_id(reserved_agent_id) + except Exception: + LOGGER.warning("bridge.handoff_release_agent_failed call_id=%s agent_id=%s", call_id, reserved_agent_id) + escalation.status = "failed" + escalation.completed_at = utc_now_iso() + session.commit() + _append_escalation_timeline(escalation, link, action="escalation.transfer_failed", extra={"agent_id": reserved_agent_id, "error": str(exc)}) + _emit_escalation_event(event_type="TransferFailed", escalation=escalation, link=link, extra={"agent_id": reserved_agent_id, "error": str(exc)}) + raise HTTPException(status_code=502, detail=f"Failed to redirect call to agent: {exc}") from exc + + if escalation is not None: + set_routing_agent_status(reserved_agent_id, "RINGING") + _append_escalation_timeline(escalation, link, action="escalation.agent_ringing", extra={"agent_id": reserved_agent_id}) + _emit_escalation_event(event_type="AgentRinging", escalation=escalation, link=link, extra={"agent_id": reserved_agent_id}) now = utc_now_iso() link.voice_session_id = (