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>
This commit is contained in:
Abhishek 2026-07-20 21:57:34 +05:30 committed by GitHub
parent 2f7b47a1b4
commit d8cd34e8d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 2886 additions and 102 deletions

View file

@ -98,6 +98,10 @@ class PipecatEngine:
self._gathered_context: dict = {}
self._user_response_timeout_task: Optional[asyncio.Task] = None
self._pending_extraction_tasks: set[asyncio.Task] = set()
# True once a final (synchronous) extraction has run, so the end-of-call
# and upstream-transfer paths don't redundantly re-extract the same
# terminal state.
self._final_extraction_done: bool = False
# Will be set later in initialize() when we have
# access to _context
@ -507,6 +511,29 @@ class PipecatEngine:
f"Incomplete: {incomplete}"
)
async def perform_final_variable_extraction(self) -> None:
"""Flush in-flight + current-node variable extraction synchronously.
Awaits any background extractions still running from previous nodes,
then runs the current node's extraction inline so callers that need the
freshest extracted variables before acting can rely on them -- e.g.
end_call_with_reason before disposing the call, or an external-PBX
transfer that maps extracted variables into a provider lead update
call before handing the customer off.
Idempotent: only the first call does work. The external-PBX transfer
runs this just before forwarding update_lead, so the subsequent
end_call_with_reason would otherwise re-extract the same terminal state.
"""
if self._final_extraction_done:
logger.debug("Final variable extraction already performed; skipping")
return
self._final_extraction_done = True
await self._await_pending_extractions()
await self._perform_variable_extraction_if_needed(
self._current_node, run_in_background=False
)
async def _setup_llm_context(self, node: Node) -> None:
"""Common method to set up LLM context"""
# Set OTel span name for tracing
@ -742,13 +769,8 @@ class PipecatEngine:
EndTaskReason.PIPELINE_ERROR.value,
EndTaskReason.VOICEMAIL_DETECTED.value,
):
# Await any in-flight background extractions from previous nodes
await self._await_pending_extractions()
# Perform final variable extraction synchronously before ending
await self._perform_variable_extraction_if_needed(
self._current_node, run_in_background=False
)
# Flush in-flight + current-node extractions synchronously before ending
await self.perform_final_variable_extraction()
frame_to_push = (
CancelFrame(reason=reason) if abort_immediately else EndFrame(reason=reason)
@ -766,6 +788,20 @@ class PipecatEngine:
call_tags.append(call_disposition)
self._gathered_context["call_tags"] = call_tags
# Hangup strategies run while serializing the terminal frame. Persist
# the final extracted values first so external-PBX adapters can apply
# workflow lead-field mappings before terminating the customer leg.
try:
await db_client.update_workflow_run(
run_id=self._workflow_run_id,
gathered_context=self._gathered_context,
)
except Exception as exc:
# Call teardown must never be held hostage by an enrichment write.
logger.warning(
f"Could not persist final gathered context before hangup: {exc}"
)
logger.debug(
f"Finishing run with reason: {reason}, disposition: {call_disposition} "
f"queueing frame {frame_to_push}"