mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-25 12:01:04 +02:00
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>
This commit is contained in:
parent
688773d3b3
commit
c8d7584f9f
4 changed files with 352 additions and 57 deletions
|
|
@ -99,6 +99,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
|
||||
|
|
@ -504,6 +508,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 upstream-PBX
|
||||
transfer that maps extracted variables into the VICIdial update_lead
|
||||
call before handing the customer off.
|
||||
|
||||
Idempotent: only the first call does work. The upstream-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
|
||||
|
|
@ -739,13 +766,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)
|
||||
|
|
|
|||
|
|
@ -561,38 +561,81 @@ class CustomToolManager:
|
|||
upstream = (workflow_run.initial_context or {}).get("upstream_pbx")
|
||||
if upstream:
|
||||
from api.services.telephony.upstream_pbx import (
|
||||
transfer_upstream_call,
|
||||
collect_update_lead_fields,
|
||||
route_upstream_after_call,
|
||||
update_upstream_lead,
|
||||
)
|
||||
|
||||
# Play the transfer message first so its TTS overlaps the
|
||||
# extraction latency below.
|
||||
await self._play_config_message(config)
|
||||
ok = await transfer_upstream_call(upstream, destination)
|
||||
# Mark the run so the end-of-call hangup does NOT also hang up
|
||||
# the now-transferred customer (see ARIHangupStrategy).
|
||||
await db_client.update_workflow_run(
|
||||
run_id=self._engine._workflow_run_id,
|
||||
gathered_context={"upstream_transferred": True},
|
||||
|
||||
# Extract variables right before the transfer so any
|
||||
# X-VICI-UPDATE-LEAD_* values reflect the final conversation
|
||||
# state, then forward them to the upstream PBX's update_lead
|
||||
# BEFORE handing the customer off. This lets a workflow plumb
|
||||
# arbitrary, conversation-derived fields into the VICIdial
|
||||
# lead. Best-effort: never blocks the transfer.
|
||||
await self._engine.perform_final_variable_extraction()
|
||||
update_fields = collect_update_lead_fields(
|
||||
self._engine._gathered_context
|
||||
)
|
||||
await function_call_params.result_callback(
|
||||
{
|
||||
"status": "success" if ok else "failed",
|
||||
"action": "upstream_transfer",
|
||||
"message": (
|
||||
"Transferring your call now."
|
||||
if ok
|
||||
else "I'm sorry, I couldn't complete the transfer."
|
||||
),
|
||||
},
|
||||
properties=properties,
|
||||
logger.info(
|
||||
f"[transfer] update_lead fields from extracted "
|
||||
f"variables: {update_fields}"
|
||||
)
|
||||
if ok:
|
||||
# Give the upstream PBX time to redirect the customer out
|
||||
# of the conference toward the destination BEFORE we drop
|
||||
# dograh's own leg -- otherwise the teardown races the
|
||||
# redirect and cancels the customer's call to the
|
||||
# destination (it never rings).
|
||||
await asyncio.sleep(4)
|
||||
# Tear down dograh's own legs; the customer has moved to the
|
||||
# upstream PBX side.
|
||||
if update_fields:
|
||||
await update_upstream_lead(upstream, update_fields)
|
||||
|
||||
# Hardcoded address3 routing (see route_upstream_after_call):
|
||||
# "Y"/"N" transfer the customer into in-group
|
||||
# dograhtest1/dograhtest2; anything else hangs the customer up
|
||||
# instead of transferring. The destination configured on the
|
||||
# transfer node is intentionally ignored for upstream calls.
|
||||
action, ok = await route_upstream_after_call(
|
||||
upstream, update_fields
|
||||
)
|
||||
if action == "transfer":
|
||||
# Mark the run so the end-of-call hangup does NOT also hang
|
||||
# up the now-transferred customer (see ARIHangupStrategy).
|
||||
await db_client.update_workflow_run(
|
||||
run_id=self._engine._workflow_run_id,
|
||||
gathered_context={"upstream_transferred": True},
|
||||
)
|
||||
await function_call_params.result_callback(
|
||||
{
|
||||
"status": "success" if ok else "failed",
|
||||
"action": "upstream_transfer",
|
||||
"message": (
|
||||
"Transferring your call now."
|
||||
if ok
|
||||
else "I'm sorry, I couldn't complete the transfer."
|
||||
),
|
||||
},
|
||||
properties=properties,
|
||||
)
|
||||
if ok:
|
||||
# Give the upstream PBX time to redirect the customer
|
||||
# out of the conference toward the in-group BEFORE we
|
||||
# drop dograh's own leg -- otherwise the teardown races
|
||||
# the redirect and cancels the customer's call (it
|
||||
# never rings).
|
||||
await asyncio.sleep(4)
|
||||
else:
|
||||
# address3 wasn't Y/N: route_upstream_after_call already
|
||||
# hung up the customer leg. Leave upstream_transferred
|
||||
# unset so the end-of-call teardown re-confirms the hangup
|
||||
# (a no-op if it already succeeded, a retry if it didn't).
|
||||
await function_call_params.result_callback(
|
||||
{
|
||||
"status": "success",
|
||||
"action": "upstream_hangup",
|
||||
"message": "Ending the call now.",
|
||||
},
|
||||
properties=properties,
|
||||
)
|
||||
# Tear down dograh's own legs; the customer leg has already
|
||||
# been handled on the upstream PBX side.
|
||||
await self._engine.end_call_with_reason(
|
||||
EndTaskReason.END_CALL_TOOL_REASON.value,
|
||||
abort_immediately=True,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue