feat: agent stream for cloudonix OPBX (#261)

* feat: agent stream for cloudonix OPBX

* feat: make cloudonix app name optional

* feat: create application while configuring telephony config

* fix: get telephony configuration from stamped workflow run

* fix: fix vobiz hangup URL
This commit is contained in:
Abhishek 2026-05-02 15:53:58 +05:30 committed by GitHub
parent 5cfdbeff02
commit 7fd3b96470
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1529 additions and 545 deletions

View file

@ -24,27 +24,35 @@ from typing import Any, Dict, List, Optional, Tuple, Type
from loguru import logger
from api.db import db_client
from api.db.models import TelephonyConfigurationModel
from api.db.models import TelephonyConfigurationModel, WorkflowRunModel
from api.services.telephony import registry
from api.services.telephony.base import TelephonyProvider
async def load_telephony_config_by_id(
telephony_configuration_id: int,
organization_id: int,
) -> Dict[str, Any]:
"""Load and normalize the config row by primary key.
"""Load and normalize the config row by primary key, scoped to the org.
Returns a dict in the shape each provider class expects in its constructor
(provider name + provider-specific credentials + ``from_numbers`` list of
raw address strings).
raw address strings). Raises ``ValueError`` if the config doesn't exist
or doesn't belong to ``organization_id`` — the org scope is what makes
this safe to expose to user-driven request flows.
"""
if not telephony_configuration_id:
raise ValueError("telephony_configuration_id is required")
if not organization_id:
raise ValueError("organization_id is required")
row = await db_client.get_telephony_configuration(telephony_configuration_id)
row = await db_client.get_telephony_configuration_for_org(
telephony_configuration_id, organization_id
)
if not row:
raise ValueError(
f"Telephony configuration {telephony_configuration_id} not found"
f"Telephony configuration {telephony_configuration_id} not found "
f"for organization {organization_id}"
)
return await _normalize_with_phone_numbers(row)
@ -68,6 +76,9 @@ async def find_telephony_config_for_inbound(
) -> Optional[Tuple[int, Dict[str, Any]]]:
"""Match an inbound webhook to one of the org's configs of the detected
provider. Returns ``(config_id, normalized_config)`` or None.
Always scoped to ``organization_id`` never matches across orgs even if
two orgs happen to have credentials with the same account_id.
"""
spec = registry.get_optional(provider_name)
if not spec:
@ -96,10 +107,10 @@ async def find_telephony_config_for_inbound(
matched = next(
(c for c in candidates if c.is_default_outbound), candidates[0]
)
else:
elif account_id:
for cand in candidates:
stored = (cand.credentials or {}).get(field)
if stored and account_id and stored == account_id:
if stored and stored == account_id:
matched = cand
break
@ -112,11 +123,32 @@ async def find_telephony_config_for_inbound(
async def get_telephony_provider_by_id(
telephony_configuration_id: int,
organization_id: int,
) -> TelephonyProvider:
config = await load_telephony_config_by_id(telephony_configuration_id)
config = await load_telephony_config_by_id(
telephony_configuration_id, organization_id
)
return _instantiate(config)
async def get_telephony_provider_for_run(
workflow_run: WorkflowRunModel,
organization_id: int,
) -> TelephonyProvider:
"""Resolve the provider for a given workflow run.
Prefers ``initial_context.telephony_configuration_id`` stamped at run
creation by ``/initiate-call``, ``_create_inbound_workflow_run``, the
campaign dispatcher, and ``public_agent``. Falls back to the org's
default config so legacy runs created before the multi-config migration
still resolve.
"""
cfg_id = (workflow_run.initial_context or {}).get("telephony_configuration_id")
if cfg_id:
return await get_telephony_provider_by_id(cfg_id, organization_id)
return await get_default_telephony_provider(organization_id)
async def get_default_telephony_provider(organization_id: int) -> TelephonyProvider:
config = await load_default_telephony_config(organization_id)
return _instantiate(config)
@ -149,7 +181,9 @@ async def load_credentials_for_transport(
Raises ValueError when the resolved config is for a different provider.
"""
if telephony_configuration_id:
config = await load_telephony_config_by_id(telephony_configuration_id)
config = await load_telephony_config_by_id(
telephony_configuration_id, organization_id
)
else:
config = await load_default_telephony_config(organization_id)