# L1 -> L2 Routing Engine (Phase 1) Implements a real Agent Pool + Routing Engine for voice escalation from Voice AI (L1) to live L2 operators, replacing the previous static single-extension redirect target. ## What changed - `agents` table (`services/routing_service/engine.py`): agents are rows with `level`, `tenant_ids`, `skills`, `status`, `max_concurrent_calls`, `enabled` - not a hardcoded list. Selection filters by level/tenant/skills and picks the longest-idle, least-loaded match. Reservation is an atomic `UPDATE ... WHERE status='AVAILABLE'` (rowcount-checked), so two concurrent calls can never reserve the same agent. - `escalations` table: one row per escalation attempt (`from_level`, `to_level`, `reason_code`, `required_skills`, `priority`, `status`). - `routing_rules` table: seeded data (not code) describing which level-to-level transitions are allowed; `L3` and multi-tenant rules can be added later without code changes. - `asterisk_call_links` gained `tenant_id`, `current_level`, `required_skills_json`, `priority`. - `POST /asterisk/live-calls/{call_id}/escalations` (asterisk-bridge-service): explicit escalation entrypoint. `voice_ai.request_handoff` (used by the AI runtime) now also uses the Agent Pool automatically for any `queue_code` configured in `ASTERISK_QUEUE_LEVEL_MAP_JSON`; every other queue_code keeps the old static `ASTERISK_TRANSFER_TARGET_MAP_JSON` behavior unchanged. - `routing-service` gained `/agents` CRUD + `/internal/routing/reserve-agent` and `/internal/routing/release-agent`, called by asterisk-bridge-service the same way it already calls interaction-service/ai-voice-runtime-service (bridge-issued bearer token, `ROUTING_SERVICE_URL`). - Agent is released back to `AVAILABLE` when the call ends (`process_call_ended` in `bridge_processing.py`), which also closes the matching open `escalations` row. ## Config for a given DID/queue ``` ASTERISK_QUEUE_LEVEL_MAP_JSON={"":{"level":"L2","tenant_id":"konturai"}} ``` Any `queue_code` not listed here keeps behaving exactly as before (static `ASTERISK_TRANSFER_TARGET_MAP_JSON` redirect) - this is additive, not a replacement of the legacy mechanism. Seed `agents` rows via `POST /agents` (admin/supervisor). Level `L2`, `extension` must match a real SIP endpoint (currently only `2001`/`2002` exist on the telecom server). ## Known gap - no-answer retry Today the Routing Engine picks and reserves one agent and issues a single AMI `Redirect` to their extension; if that extension does not answer, the caller currently depends on whatever the dialplan does at that extension (unchanged from before this change). Automatic "agent didn't answer -> release and pick next" requires either: 1. A small dialplan addition at the target extension's context: `Dial` with a fixed timeout and, on failure, a `UserEvent` back into the bridge (mirrors the existing `MVPCCAIFallbackToHuman` pattern) so `asterisk_bridge_service` can call `/internal/routing/reserve-agent` again with the failed agent in `exclude_agent_ids`, or 2. Switching from `Redirect` to AMI `Originate` with `Timeout` and reacting to `OriginateResponse`. Left out of Phase 1 deliberately: changing dialplan behavior without full visibility into what a given extension's context currently does on no-answer risks silently dropping a live customer call. Needs a follow-up MR with an explicit, reviewed dialplan change. ## Not in Phase 1 - L3 (technical specialists) - `agents.level='L3'` and `routing_rules` already support it; enabling it is a data-only follow-up once real L3 agents exist. - SLA config / callback (ТЗ sections 20-21). - Metrics (ТЗ section 38).