Merge pull request 'feat: L1->L2 agent pool and routing engine for voice escalation' (#4) from feature/l1-l2-routing-engine into main
deploy / deploy (push) Successful in 32s

This commit was merged in pull request #4.
This commit is contained in:
2026-08-29 08:31:40 +00:00
17 changed files with 1388 additions and 7 deletions
+136
View File
@@ -0,0 +1,136 @@
import json
from sqlalchemy import select
from services.routing_service import engine as routing_engine
from services.shared.core import new_id, utc_now_iso
from services.shared.db import get_session
from services.shared.sql_init import init_sql_schema
from services.shared.sql_models import AgentRow
def _make_agent(session, *, level="L2", tenant_ids=None, skills=None, status="AVAILABLE", enabled=True):
now = utc_now_iso()
row = AgentRow(
agent_id=new_id("agt"),
tenant_ids_json=json.dumps(tenant_ids or []),
extension="2001",
endpoint=None,
display_name="Test Agent",
level=level,
skills_json=json.dumps(skills or []),
status=status,
max_concurrent_calls=1,
enabled=enabled,
created_at=now,
updated_at=now,
)
session.add(row)
session.commit()
session.refresh(row)
return row
def test_reserve_agent_picks_available_agent_of_requested_level():
init_sql_schema()
session = get_session()
try:
agent_l2 = _make_agent(session, level="L2")
_make_agent(session, level="L3")
reserved = routing_engine.reserve_agent(
session,
call_id="call-1",
level="L2",
tenant_id=None,
required_skills=[],
)
assert reserved is not None
assert reserved.agent_id == agent_l2.agent_id
assert reserved.status == "RESERVED"
assert reserved.current_call_id == "call-1"
finally:
session.close()
def test_reserve_agent_does_not_double_reserve():
init_sql_schema()
session = get_session()
try:
_make_agent(session, level="L2")
first = routing_engine.reserve_agent(session, call_id="call-a", level="L2", tenant_id=None, required_skills=[])
second = routing_engine.reserve_agent(session, call_id="call-b", level="L2", tenant_id=None, required_skills=[])
assert first is not None
assert second is None
finally:
session.close()
def test_reserve_agent_filters_by_tenant_and_skills():
init_sql_schema()
session = get_session()
try:
wrong_tenant = _make_agent(session, level="L2", tenant_ids=["other"])
no_skill = _make_agent(session, level="L2", tenant_ids=["konturai"], skills=[])
matching = _make_agent(session, level="L2", tenant_ids=["konturai"], skills=["billing"])
reserved = routing_engine.reserve_agent(
session,
call_id="call-skill",
level="L2",
tenant_id="konturai",
required_skills=["billing"],
)
assert reserved is not None
assert reserved.agent_id == matching.agent_id
assert reserved.agent_id != wrong_tenant.agent_id
assert reserved.agent_id != no_skill.agent_id
finally:
session.close()
def test_release_agent_by_call_id_makes_agent_available_again():
init_sql_schema()
session = get_session()
try:
agent = _make_agent(session, level="L2")
reserved = routing_engine.reserve_agent(session, call_id="call-z", level="L2", tenant_id=None, required_skills=[])
assert reserved is not None
released = routing_engine.release_agent_by_call_id(session, call_id="call-z")
assert released is not None
assert released.agent_id == agent.agent_id
assert released.status == "AVAILABLE"
assert released.current_call_id is None
assert released.calls_handled_count == 1
again = routing_engine.reserve_agent(session, call_id="call-again", level="L2", tenant_id=None, required_skills=[])
assert again is not None
assert again.agent_id == agent.agent_id
finally:
session.close()
def test_reserve_agent_excludes_disabled_and_excluded_ids():
init_sql_schema()
session = get_session()
try:
disabled = _make_agent(session, level="L2", enabled=False)
excluded = _make_agent(session, level="L2")
available = _make_agent(session, level="L2")
reserved = routing_engine.reserve_agent(
session,
call_id="call-exc",
level="L2",
tenant_id=None,
required_skills=[],
exclude_agent_ids=[excluded.agent_id],
)
assert reserved is not None
assert reserved.agent_id == available.agent_id
assert reserved.agent_id != disabled.agent_id
assert reserved.agent_id != excluded.agent_id
finally:
session.close()