dograh/api/schemas/workflow_configurations.py
Abhishek d8cd34e8d2
feat: add vici dial controls from UI (#563)
* ViciDial Working

* feat(telephony): add FreeSWITCH provider to upstream-PBX seam

Generalize the upstream-PBX capture and control paths to dispatch by
provider. FreeSWITCH bridges in with X-PBX-* headers and is driven over
the Event Socket Library (uuid_kill / uuid_transfer) by the channel UUID,
alongside the existing VICIdial ra_call_control path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* add vici specific configs

* feat(telephony): env-based VICIdial config + address3 post-call routing

Move VICIdial agent/non-agent API and FreeSWITCH ESL connection settings
out of hardcoded POC values into environment variables so the same image
works against a PBX on another server.

Add VICIdial update_lead forwarding: X-VICI-UPDATE-LEAD_* extracted
variables are mapped to lead columns and pushed via the non-agent API
before a transfer, with reserved API-control params dropped.

Add hardcoded address3 disposition routing (Y/N -> in-group transfer,
else hang up) and a synchronous final variable extraction so the
transfer path sees the freshest conversation state. Skip upstream_pbx
capture on non-PJSIP channels to avoid Asterisk 500s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: run formatter

* feat: make vici configurable from UI

* chore: clean up PR

* fix: incorporate review comments

* chore: incorporate review comments

* chore: generate client

* chore: incporporate review comments

* chore: incorporate review comments

---------

Co-authored-by: Dograh POC <payment@dograh.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:57:34 +05:30

83 lines
3.1 KiB
Python

from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
DEFAULT_MAX_CALL_DURATION_SECONDS = 300
# Hard ceiling on configurable call duration. Must stay <= the concurrency
# rate limiter's stale_call_timeout (20 min): a call running past that has
# its slot purged as stale and the org concurrency limit under-counts.
MAX_CALL_DURATION_SECONDS = 1200
DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS = 10.0
DEFAULT_SMART_TURN_STOP_SECS = 2.0
DEFAULT_TURN_START_STRATEGY = "default"
DEFAULT_TURN_START_MIN_WORDS = 3
DEFAULT_PROVISIONAL_VAD_PAUSE_SECS = 1.5
DEFAULT_TURN_STOP_STRATEGY = "transcription"
DEFAULT_CONTEXT_COMPACTION_ENABLED = False
class ExternalPBXFieldMapping(BaseModel):
"""Map one gathered-context value to a provider-native field."""
context_path: str = Field(min_length=1, max_length=255)
destination_field: str = Field(pattern=r"^[A-Za-z][A-Za-z0-9_]{0,63}$")
@field_validator("context_path", mode="before")
@classmethod
def strip_context_path(cls, value: object) -> object:
return value.strip() if isinstance(value, str) else value
@field_validator("destination_field", mode="before")
@classmethod
def strip_destination_field(cls, value: object) -> object:
return value.strip() if isinstance(value, str) else value
class AmbientNoiseConfigurationDefaults(BaseModel):
model_config = ConfigDict(extra="allow")
enabled: bool = False
volume: float = 0.3
class WorkflowConfigurationDefaults(BaseModel):
model_config = ConfigDict(extra="allow")
@model_validator(mode="before")
@classmethod
def _treat_null_as_unset(cls, data):
# Stored configs (and older clients) carry explicit JSON nulls for
# keys the user never configured; dropping them lets the field
# defaults apply instead of failing validation.
if isinstance(data, dict):
return {k: v for k, v in data.items() if v is not None}
return data
ambient_noise_configuration: AmbientNoiseConfigurationDefaults = Field(
default_factory=AmbientNoiseConfigurationDefaults
)
max_call_duration: int = Field(
default=DEFAULT_MAX_CALL_DURATION_SECONDS,
gt=0,
le=MAX_CALL_DURATION_SECONDS,
)
max_user_idle_timeout: float = DEFAULT_MAX_USER_IDLE_TIMEOUT_SECONDS
smart_turn_stop_secs: float = DEFAULT_SMART_TURN_STOP_SECS
turn_start_strategy: Literal["default", "min_words", "provisional_vad"] = (
DEFAULT_TURN_START_STRATEGY
)
turn_start_min_words: int = DEFAULT_TURN_START_MIN_WORDS
provisional_vad_pause_secs: float = DEFAULT_PROVISIONAL_VAD_PAUSE_SECS
turn_stop_strategy: Literal["transcription", "turn_analyzer"] = (
DEFAULT_TURN_STOP_STRATEGY
)
dictionary: str = ""
context_compaction_enabled: bool = DEFAULT_CONTEXT_COMPACTION_ENABLED
external_pbx_field_mappings: list[ExternalPBXFieldMapping] = Field(
default_factory=list,
max_length=100,
)
def get_default_workflow_configurations() -> WorkflowConfigurationDefaults:
return WorkflowConfigurationDefaults()