From f1a5aa40d693679c3dc7aa635432b1be27ab8159 Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Sat, 4 Apr 2026 19:35:52 +0530 Subject: [PATCH] feat: add pre call fetch configuration --- api/services/pipecat/event_handlers.py | 44 ++++++ api/services/pipecat/pre_call_fetch.py | 115 +++++++++++++++ api/services/pipecat/run_pipeline.py | 24 +++ api/services/workflow/dto.py | 4 + api/services/workflow/pipecat_engine.py | 2 - .../workflow/pipecat_engine_custom_tools.py | 51 +------ api/services/workflow/workflow.py | 3 + .../test_pipecat_engine_context_update.py | 1 + api/tests/test_pipecat_engine_end_call.py | 9 ++ ...cat_engine_node_switch_with_user_speech.py | 1 + api/tests/test_pipecat_engine_tool_calls.py | 1 + ...test_pipecat_engine_variable_extraction.py | 1 + ...t_tts_endframe_with_audio_write_failure.py | 2 + api/tests/test_user_idle_handler.py | 1 + .../test_user_muting_during_bot_speech.py | 3 + api/utils/hold_audio.py | 61 +++++++- docs/docs.json | 3 + docs/images/embedding-configurations.png | Bin 0 -> 59229 bytes docs/voice-agent/knowledge-base.mdx | 53 +++++++ docs/voice-agent/pre-call-data-fetch.mdx | 139 ++++++++++++++++++ docs/voice-agent/tools/introduction.mdx | 39 +++++ ui/src/components/flow/DocumentSelector.tsx | 44 ++++-- ui/src/components/flow/ToolSelector.tsx | 4 +- ui/src/components/flow/nodes/StartCall.tsx | 95 +++++++++++- ui/src/components/flow/types.ts | 4 + ui/src/constants/documentation.ts | 6 + 26 files changed, 644 insertions(+), 66 deletions(-) create mode 100644 api/services/pipecat/pre_call_fetch.py create mode 100644 docs/images/embedding-configurations.png create mode 100644 docs/voice-agent/knowledge-base.mdx create mode 100644 docs/voice-agent/pre-call-data-fetch.mdx create mode 100644 docs/voice-agent/tools/introduction.mdx diff --git a/api/services/pipecat/event_handlers.py b/api/services/pipecat/event_handlers.py index 55bc0b4..70d8935 100644 --- a/api/services/pipecat/event_handlers.py +++ b/api/services/pipecat/event_handlers.py @@ -1,3 +1,5 @@ +import asyncio + from loguru import logger from api.db import db_client @@ -13,6 +15,7 @@ from api.services.pipecat.tracing_config import get_trace_url from api.services.workflow.pipecat_engine import PipecatEngine from api.tasks.arq import enqueue_job from api.tasks.function_names import FunctionNames +from api.utils.hold_audio import play_hold_audio_loop from pipecat.frames.frames import Frame, LLMContextFrame, TTSSpeakFrame from pipecat.pipeline.task import PipelineTask from pipecat.processors.audio.audio_buffer_processor import AudioBufferProcessor @@ -28,6 +31,7 @@ def register_event_handlers( in_memory_logs_buffer: InMemoryLogsBuffer, pipeline_metrics_aggregator: PipelineMetricsAggregator, audio_config=AudioConfig, + pre_call_fetch_task: asyncio.Task | None = None, ): """Register all event handlers for transport and task events. @@ -58,6 +62,9 @@ def register_event_handlers( async def maybe_trigger_initial_response(): """Start the conversation after both pipeline_started and client_connected events. + If a pre-call fetch is in progress, plays a ringer while waiting for the + response, then merges the result into the call context before proceeding. + If the start node has a greeting configured, play it directly via TTS. Otherwise, trigger an LLM generation for the opening message. """ @@ -68,6 +75,43 @@ def register_event_handlers( ): ready_state["initial_response_triggered"] = True + # Wait for pre-call fetch if in progress, playing ringer meanwhile + if pre_call_fetch_task is not None: + if not pre_call_fetch_task.done(): + logger.info( + "Pre-call fetch still in progress, playing ringer while waiting" + ) + stop_ringer = asyncio.Event() + sample_rate = audio_config.pipeline_sample_rate or 16000 + ringer_task = asyncio.create_task( + play_hold_audio_loop(task, stop_ringer, sample_rate) + ) + try: + fetch_result = await pre_call_fetch_task + finally: + stop_ringer.set() + await ringer_task + else: + fetch_result = pre_call_fetch_task.result() + + if fetch_result: + engine._call_context_vars.update(fetch_result) + try: + await db_client.update_workflow_run( + workflow_run_id, + initial_context={**engine._call_context_vars}, + ) + except Exception as e: + logger.error(f"Failed to persist pre-call fetch context: {e}") + logger.info( + f"Pre-call fetch complete, merged keys: " + f"{list(fetch_result.keys())}" + ) + + # Set the start node now (after pre-call fetch data is merged) + # so that render_template() has the complete _call_context_vars. + await engine.set_node(engine.workflow.start_node_id) + greeting = engine.get_start_greeting() if greeting: logger.debug( diff --git a/api/services/pipecat/pre_call_fetch.py b/api/services/pipecat/pre_call_fetch.py new file mode 100644 index 0000000..7776111 --- /dev/null +++ b/api/services/pipecat/pre_call_fetch.py @@ -0,0 +1,115 @@ +"""Pre-call HTTP data fetch for StartCall node. + +Executes an HTTP request before a voice call starts to enrich the +call context with data from external systems (CRM, ERP, etc.). +""" + +from typing import Any, Dict, Optional + +import httpx +from loguru import logger + +from api.db import db_client +from api.utils.credential_auth import build_auth_header + +PRE_CALL_FETCH_TIMEOUT_SECONDS = 10 + + +async def execute_pre_call_fetch( + *, + url: str, + credential_uuid: Optional[str], + call_context_vars: Dict[str, Any], + workflow_id: int, + organization_id: int, +) -> Dict[str, Any]: + """Execute a POST request to fetch data before a call starts. + + Sends a standardized payload with call metadata (agent_id, from/to numbers). + The response JSON is returned as a dict to be merged into initial_context. + + Returns: + Response JSON dict on success, empty dict on any failure. + Never raises. + """ + # Build standardized payload + payload = { + "event": "call_inbound", + "call_inbound": { + "agent_id": workflow_id, + "from_number": call_context_vars.get("caller_number", ""), + "to_number": call_context_vars.get("called_number", ""), + }, + } + + # Build headers + headers: Dict[str, str] = {"Content-Type": "application/json"} + + if credential_uuid: + try: + credential = await db_client.get_credential_by_uuid( + credential_uuid, organization_id + ) + if credential: + headers.update(build_auth_header(credential)) + else: + logger.warning( + f"Pre-call fetch: credential {credential_uuid} not found" + ) + except Exception as e: + logger.error(f"Pre-call fetch: failed to resolve credential: {e}") + + logger.info(f"Pre-call fetch: POST {url}") + + try: + async with httpx.AsyncClient(timeout=PRE_CALL_FETCH_TIMEOUT_SECONDS) as client: + response = await client.post(url, headers=headers, json=payload) + + try: + response_data = response.json() + except Exception: + response_data = {} + + if response.is_success: + if not isinstance(response_data, dict): + logger.warning( + "Pre-call fetch: response is not a JSON object, skipping" + ) + return {} + + # Extract dynamic_variables from Retell-compatible response + # Supports: {call_inbound: {dynamic_variables: {...}}} + # or: {dynamic_variables: {...}} + dynamic_vars = {} + call_inbound = response_data.get("call_inbound") + if isinstance(call_inbound, dict): + dynamic_vars = call_inbound.get("dynamic_variables", {}) + elif "dynamic_variables" in response_data: + dynamic_vars = response_data["dynamic_variables"] + + if not isinstance(dynamic_vars, dict): + dynamic_vars = {} + + logger.info( + f"Pre-call fetch: success ({response.status_code}), " + f"dynamic_variables keys: {list(dynamic_vars.keys())}" + ) + return dynamic_vars + else: + logger.warning( + f"Pre-call fetch: HTTP {response.status_code} - " + f"{response.text[:200]}" + ) + return {} + + except httpx.TimeoutException: + logger.error( + f"Pre-call fetch: timed out after {PRE_CALL_FETCH_TIMEOUT_SECONDS}s" + ) + return {} + except httpx.RequestError as e: + logger.error(f"Pre-call fetch: request failed: {e}") + return {} + except Exception as e: + logger.error(f"Pre-call fetch: unexpected error: {e}") + return {} diff --git a/api/services/pipecat/run_pipeline.py b/api/services/pipecat/run_pipeline.py index df9b02f..82ea3d1 100644 --- a/api/services/pipecat/run_pipeline.py +++ b/api/services/pipecat/run_pipeline.py @@ -24,6 +24,7 @@ from api.services.pipecat.pipeline_engine_callbacks_processor import ( PipelineEngineCallbacksProcessor, ) from api.services.pipecat.pipeline_metrics_aggregator import PipelineMetricsAggregator +from api.services.pipecat.pre_call_fetch import execute_pre_call_fetch from api.services.pipecat.realtime_feedback_observer import ( RealtimeFeedbackObserver, register_turn_log_handlers, @@ -622,6 +623,28 @@ async def _run_pipeline( ReactFlowDTO.model_validate(workflow.workflow_definition_with_fallback) ) + # Pre-call fetch: fire early so it runs concurrently with remaining setup + pre_call_fetch_task = None + start_node = workflow_graph.nodes.get(workflow_graph.start_node_id) + if ( + start_node + and start_node.pre_call_fetch_enabled + and start_node.pre_call_fetch_url + ): + logger.info( + f"Pre-call fetch enabled for workflow run {workflow_run_id}, " + f"firing request to {start_node.pre_call_fetch_url}" + ) + pre_call_fetch_task = asyncio.create_task( + execute_pre_call_fetch( + url=start_node.pre_call_fetch_url, + credential_uuid=start_node.pre_call_fetch_credential_uuid, + call_context_vars=merged_call_context_vars, + workflow_id=workflow_id, + organization_id=workflow.organization_id, + ) + ) + # Create in-memory logs buffer early so it can be used by engine callbacks in_memory_logs_buffer = InMemoryLogsBuffer(workflow_run_id) @@ -952,6 +975,7 @@ async def _run_pipeline( in_memory_logs_buffer=in_memory_logs_buffer, pipeline_metrics_aggregator=pipeline_metrics_aggregator, audio_config=audio_config, + pre_call_fetch_task=pre_call_fetch_task, ) register_audio_data_handler(audio_buffer, workflow_run_id, in_memory_audio_buffer) diff --git a/api/services/workflow/dto.py b/api/services/workflow/dto.py index 3fc3119..31d9a35 100644 --- a/api/services/workflow/dto.py +++ b/api/services/workflow/dto.py @@ -59,6 +59,10 @@ class NodeDataDTO(BaseModel): detect_voicemail: bool = False delayed_start: bool = False delayed_start_duration: Optional[float] = None + # Pre-call fetch (start node only) + pre_call_fetch_enabled: bool = False + pre_call_fetch_url: Optional[str] = None + pre_call_fetch_credential_uuid: Optional[str] = None tool_uuids: Optional[List[str]] = None document_uuids: Optional[List[str]] = None trigger_path: Optional[str] = None diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index 1b56c0d..bf5c0d9 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -162,8 +162,6 @@ class PipecatEngine: if self._context_compaction_enabled: self._context_summarization_manager = ContextSummarizationManager(self) - await self.set_node(self.workflow.start_node_id) - logger.debug(f"{self.__class__.__name__} initialized") except Exception as e: logger.error(f"Error initializing {self.__class__.__name__}: {e}") diff --git a/api/services/workflow/pipecat_engine_custom_tools.py b/api/services/workflow/pipecat_engine_custom_tools.py index b0dd1c0..eecb338 100644 --- a/api/services/workflow/pipecat_engine_custom_tools.py +++ b/api/services/workflow/pipecat_engine_custom_tools.py @@ -14,7 +14,6 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional from loguru import logger -from api.constants import APP_ROOT_DIR from api.db import db_client from api.enums import ToolCategory, WorkflowRunMode from api.services.telephony.call_transfer_manager import get_call_transfer_manager @@ -28,11 +27,10 @@ from api.services.workflow.tools.custom_tool import ( execute_http_tool, tool_to_function_schema, ) -from api.utils.hold_audio import load_hold_audio +from api.utils.hold_audio import play_hold_audio_loop from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.frames.frames import ( FunctionCallResultProperties, - OutputAudioRawFrame, TTSSpeakFrame, ) from pipecat.services.llm_service import FunctionCallParams @@ -539,7 +537,11 @@ class CustomToolManager: # Start hold music as background task hold_music_task = asyncio.create_task( - self.play_hold_music_loop(hold_music_stop_event, sample_rate) + play_hold_audio_loop( + self._engine.task, + hold_music_stop_event, + sample_rate, + ) ) # Wait for transfer completion using Redis pub/sub @@ -666,44 +668,3 @@ class CustomToolManager: # Unknown action, treat as generic success logger.warning(f"Unknown transfer action: {action}, treating as success") await function_call_params.result_callback(result) - - async def play_hold_music_loop( - self, stop_event: asyncio.Event, sample_rate: int = 8000 - ): - """Play hold music in a loop until stop event is triggered. - - Args: - stop_event: Event to stop the hold music loop - sample_rate: Sample rate for the hold music (default 8000Hz for Twilio) - """ - try: - # Path to hold music file based on sample rate - hold_music_file = ( - APP_ROOT_DIR / "assets" / f"transfer_hold_ring_{sample_rate}.wav" - ) - hold_audio_data = load_hold_audio(hold_music_file, sample_rate) - num_samples = len(hold_audio_data) // 2 - duration = int(num_samples / sample_rate) - - logger.info(f"Starting hold music loop with file: {hold_music_file}") - - while not stop_event.is_set(): - # Queue the hold audio frame - frame = OutputAudioRawFrame( - audio=hold_audio_data, - sample_rate=sample_rate, - num_channels=1, - ) - await self._engine.task.queue_frame(frame) - - # Wait for the audio to play or until stopped - try: - await asyncio.wait_for(stop_event.wait(), timeout=duration + 1.5) - break # Stop event was set - except asyncio.TimeoutError: - pass # Continue looping - - logger.info("Hold music loop stopped") - - except Exception as e: - logger.error(f"Error in hold music loop: {e}") diff --git a/api/services/workflow/workflow.py b/api/services/workflow/workflow.py index 3278685..fe5a9ec 100644 --- a/api/services/workflow/workflow.py +++ b/api/services/workflow/workflow.py @@ -82,6 +82,9 @@ class Node: self.delayed_start_duration = data.delayed_start_duration self.tool_uuids = data.tool_uuids self.document_uuids = data.document_uuids + self.pre_call_fetch_enabled = data.pre_call_fetch_enabled + self.pre_call_fetch_url = data.pre_call_fetch_url + self.pre_call_fetch_credential_uuid = data.pre_call_fetch_credential_uuid self.data = data diff --git a/api/tests/test_pipecat_engine_context_update.py b/api/tests/test_pipecat_engine_context_update.py index 30dd4b8..8ef0e0e 100644 --- a/api/tests/test_pipecat_engine_context_update.py +++ b/api/tests/test_pipecat_engine_context_update.py @@ -201,6 +201,7 @@ async def run_pipeline_and_capture_context( async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) diff --git a/api/tests/test_pipecat_engine_end_call.py b/api/tests/test_pipecat_engine_end_call.py index 78bb020..7ef7ffe 100644 --- a/api/tests/test_pipecat_engine_end_call.py +++ b/api/tests/test_pipecat_engine_end_call.py @@ -287,6 +287,7 @@ class TestEndCallViaNodeTransition: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) @@ -390,6 +391,7 @@ class TestEndCallViaNodeTransition: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) @@ -488,6 +490,7 @@ class TestEndCallViaCustomTool: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) @@ -579,6 +582,7 @@ class TestEndCallViaCustomTool: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) @@ -656,6 +660,7 @@ class TestEndCallViaClientDisconnect: async def initialize_and_disconnect(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) # Wait for initial generation to complete @@ -746,6 +751,7 @@ class TestEndCallRaceConditions: async def initialize_and_race(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) # Wait for initial generation @@ -858,6 +864,7 @@ class TestEndCallRaceConditions: nonlocal disconnect_called await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) # Wait for the end_call tool to be called @@ -951,6 +958,7 @@ class TestEndCallExtractionBehavior: async def initialize_and_end(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) # Wait for initial generation @@ -1076,6 +1084,7 @@ class TestEndCallExtractionBehavior: async def initialize_and_end(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) # Wait for initial generation diff --git a/api/tests/test_pipecat_engine_node_switch_with_user_speech.py b/api/tests/test_pipecat_engine_node_switch_with_user_speech.py index efad8fd..205baa1 100644 --- a/api/tests/test_pipecat_engine_node_switch_with_user_speech.py +++ b/api/tests/test_pipecat_engine_node_switch_with_user_speech.py @@ -294,6 +294,7 @@ class TestNodeSwitchWithUserSpeech: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) # Start the LLM generation - user speech will be injected # automatically when FunctionCallResultFrame #1 is seen await engine.llm.queue_frame(LLMContextFrame(engine.context)) diff --git a/api/tests/test_pipecat_engine_tool_calls.py b/api/tests/test_pipecat_engine_tool_calls.py index ea4d222..001de29 100644 --- a/api/tests/test_pipecat_engine_tool_calls.py +++ b/api/tests/test_pipecat_engine_tool_calls.py @@ -131,6 +131,7 @@ async def run_pipeline_with_tool_calls( # Small delay to let runner start await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) # Run both concurrently diff --git a/api/tests/test_pipecat_engine_variable_extraction.py b/api/tests/test_pipecat_engine_variable_extraction.py index 8a51614..140c5e8 100644 --- a/api/tests/test_pipecat_engine_variable_extraction.py +++ b/api/tests/test_pipecat_engine_variable_extraction.py @@ -176,6 +176,7 @@ class TestVariableExtractionDuringTransitions: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) diff --git a/api/tests/test_tts_endframe_with_audio_write_failure.py b/api/tests/test_tts_endframe_with_audio_write_failure.py index fa0dba6..d4eb92f 100644 --- a/api/tests/test_tts_endframe_with_audio_write_failure.py +++ b/api/tests/test_tts_endframe_with_audio_write_failure.py @@ -227,6 +227,7 @@ class TestTTSPauseWithAudioWriteFailure: async def initialize_and_end_call(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) # Start LLM generation - this will trigger TTS await engine.llm.queue_frame(LLMContextFrame(engine.context)) @@ -346,6 +347,7 @@ class TestTTSPauseWithAudioWriteFailure: async def initialize_and_observe(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) diff --git a/api/tests/test_user_idle_handler.py b/api/tests/test_user_idle_handler.py index 4548ca7..7c80e7d 100644 --- a/api/tests/test_user_idle_handler.py +++ b/api/tests/test_user_idle_handler.py @@ -274,6 +274,7 @@ class TestUserIdleHandler: async def initialize_engine(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) await engine.llm.queue_frame(LLMContextFrame(engine.context)) await asyncio.gather(run_pipeline(), initialize_engine()) diff --git a/api/tests/test_user_muting_during_bot_speech.py b/api/tests/test_user_muting_during_bot_speech.py index 6fd6b78..ee14234 100644 --- a/api/tests/test_user_muting_during_bot_speech.py +++ b/api/tests/test_user_muting_during_bot_speech.py @@ -266,6 +266,7 @@ class TestUserMutingDuringBotSpeech: async def run_test(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) # Trigger first LLM completion await engine.llm.queue_frame(LLMContextFrame(engine.context)) @@ -356,6 +357,7 @@ class TestUserMutingDuringBotSpeech: async def run_test(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) # Trigger first LLM completion await engine.llm.queue_frame(LLMContextFrame(engine.context)) @@ -451,6 +453,7 @@ class TestUserMutingDuringBotSpeech: async def run_test(): await asyncio.sleep(0.01) await engine.initialize() + await engine.set_node(engine.workflow.start_node_id) # Trigger first LLM completion await engine.llm.queue_frame(LLMContextFrame(engine.context)) diff --git a/api/utils/hold_audio.py b/api/utils/hold_audio.py index e77f24f..c914082 100644 --- a/api/utils/hold_audio.py +++ b/api/utils/hold_audio.py @@ -1,15 +1,19 @@ """ -Hold audio utility for loading and caching hold music files. +Hold audio utility for loading, caching, and playing hold music files. This module provides functionality to load hold music audio files at specific sample rates -with caching to improve performance during multiple calls. +with caching to improve performance during multiple calls, and a reusable loop that queues +audio frames until a stop event is set. """ +import asyncio from typing import Dict, Optional, Tuple import numpy as np from loguru import logger +from pipecat.frames.frames import OutputAudioRawFrame + try: import soundfile as sf except ModuleNotFoundError as e: @@ -92,3 +96,56 @@ def get_cache_info() -> Dict[str, int]: "cached_files": len(_hold_audio_cache), "total_cache_size": sum(len(data) for data in _hold_audio_cache.values()), } + + +async def play_hold_audio_loop( + task, + stop_event: asyncio.Event, + sample_rate: int = 16000, + hold_music_file: Optional[str] = None, +) -> None: + """Play hold/ring-back audio in a loop until *stop_event* is set. + + This is a shared helper used by call-transfer hold music and the + pre-call data fetch ringer. The caller is responsible for creating + the ``asyncio.Event`` and setting it when playback should stop. + + Args: + task: A ``PipelineTask`` (or anything with ``queue_frame``). + stop_event: Set this event to terminate the loop. + sample_rate: Target sample rate for audio playback. + hold_music_file: Path to a WAV file. When *None* the default + ``transfer_hold_ring_{sample_rate}.wav`` asset is used. + """ + if hold_music_file is None: + from api.constants import APP_ROOT_DIR + + hold_music_file = str( + APP_ROOT_DIR / "assets" / f"transfer_hold_ring_{sample_rate}.wav" + ) + + hold_audio_data = load_hold_audio(hold_music_file, sample_rate) + if not hold_audio_data: + logger.warning(f"Hold audio loop: failed to load {hold_music_file}, skipping") + return + + num_samples = len(hold_audio_data) // 2 # 16-bit PCM = 2 bytes per sample + duration = num_samples / sample_rate + + logger.debug(f"Hold audio loop: playing at {sample_rate}Hz") + try: + while not stop_event.is_set(): + frame = OutputAudioRawFrame( + audio=hold_audio_data, + sample_rate=sample_rate, + num_channels=1, + ) + await task.queue_frame(frame) + try: + await asyncio.wait_for(stop_event.wait(), timeout=duration + 1.5) + break + except asyncio.TimeoutError: + pass + except Exception as e: + logger.error(f"Hold audio loop: error: {e}") + logger.debug("Hold audio loop: stopped") diff --git a/docs/docs.json b/docs/docs.json index 89a76f1..3a036e8 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -56,9 +56,11 @@ "voice-agent/editing-a-workflow", "voice-agent/pre-recorded-audio", "voice-agent/template-variables", + "voice-agent/pre-call-data-fetch", { "group": "Tools", "pages": [ + "voice-agent/tools/introduction", { "group": "Built-in Tools", "pages": [ @@ -74,6 +76,7 @@ } ] }, + "voice-agent/knowledge-base", { "group": "Nodes", "pages": [ diff --git a/docs/images/embedding-configurations.png b/docs/images/embedding-configurations.png new file mode 100644 index 0000000000000000000000000000000000000000..13346fb298b00d6d169abf936e93717be3b9670d GIT binary patch literal 59229 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
~!>gcYGKvVOA^Z9zH}V$%7k)hf zh(Gq{VIsbP^!@M z=?m`)A84QqD)+hMXcS1FF;h~hw5t2~W9XTga<_c|lIW~)G>y?OCPYyeTFpk&P1JRr zf3&6~Fm+qkFZBIU=0qykt}q28F3kJn@VeS$ZQlR)86bl$1UBFE2S;)imm|;x zK&53oQxAXeg#dq1J(NCNs!^f$av0Mh+ie4}=7+j=lj^m1McXx-ZTsDqJAix{m1S25=omSo`P4TYlAN@HYV1aJq)R_m4<|cSD&#PPuws zGr*&a0^=EHdmeMw%o|25Tt4grhxj?ub_7@73#{k+=q6^%I;E#(<@z`zsnW=q>6=7 z0kvj{BY;F_7&RO(mv)IyE9(Fd@r&tpZ%@aAuSMysMt!M2ZCXZ4YsPqzM}SmztMaBD zf)b^5>N}u3F%_t=n$yFTa0M#Sw8T$JikRN_FR9+cnE~G zEC3%F{rE7y8Iuu5A(@WM)cE~5%Y5g2n7Ls;gVXw>O~<3lbde%y2_OIcUct^*f+D9R zv2!9n>CNty=7RHTiXSSOOTRokWT4F8l<}f z5lQJT0Rd?cknRpi=|)08x&(Yj_qX5goO7M)I)Bce^N;ObYd!OsdCzf= zF>Zl3f*U{qXbfFYxB&EgnR0E$2mapH+2*$ROmQ@+p^^fL_HJ)Z+X_5S_ltzaITUFp z5&yNX-N6c>0AUIe!<4s0%keZ9gN*G$JS!1;$D)r0SZY~}Mp?>IhTlShMZQMwm-rlJ36jH6NjHd+u^uee2i@7c9EO#8t9&Lk%}NB2e0S`&#~LpzkYKDi2GYqekr zE1&(p7;9XxYpXpezM2IxExV23w500$Xb&EJ7CP*suAhSRbX$`}adqna_qE3yY$bal zNX+p*N?-_2W(%=Ybzz^p0t2+`S?<${!mQI5|4?ISD21yGVRT)}7iZJ10SI(vJ12<- z2_HLPGdrU)1Cap8Y3^1XWFh0w5ytmipDc`K=Y4zAPsS6kx~%62a>(^INbgs=yfz-} z*^K9fx*WaSRMYLf78vp3*&-~!-VXS?2a73T85%XZ>`WI(h2gu3j79g10;%j!GDjzC zu}T_m8RfIBc}^N|BHkCw|ExLhG{{o6ZVq&n_4usJ=E`S3#UO!~Tc0j@!*#^%)AKn% zX&(Yv1p6iIsvIwn>UTM*$!8rnyen@^*MULOq0d8)A$(Pt^6PY2tI?@9=Zj-umoRn^ zx+z+|?MhduOYFF}e+b|b2)d+`i@{5cJq+7VR^WE-Q9K>fIXT1bWlvBg|0f8w@`rO~ z`IxNTN1IH9`ZbKT>AmeGNVZzEB!t}ceX3T_k;8FBr9Y$)@^$#}hdcprWRygD4;A6< z7QI*|@N6PO$0em=TWJaj3O6KTU7>u7>e(Lc*RId(|HcMvBPW8F-3~m8v1Xar$?>dl z&Jbf>>CNQ|P=xH6pTuAl9sUqgQ-}qktj+@o+3XmW`q_@()P>kMQBe=3vf9`PUpbh0 z%j$y}>INwRR1$n4Zwr9Qv3RnQN~Fp9{QEVI-j8GmE?CUl6e6KIBk8A8vx>2f)mII1 zke{zciH4V20YKzj&rgD}X8BOvy7Vc0`}et;LL+iKPMcZwO9>~??J4Vt&qpvB*1#6b z4vvjh;ahWuBjkjX)gb zN{qeznTjm5(v|1a;~e*sivIHcC}2}^aHzy6#GZ4xxU~)?auV~pLDjD@igb_0T>eB@ zs^i2$AbWlf6-yqm{Pk-(S)_KU%KO4V95~&y;crse6wv*a{eppdcS|071jiwe{J|nK z38Q4(8sdHH8KBHHmWIC^)P6_xv9;!kHbIGd}&x))Rv-j)G&2gqJbaPbi8`w06Q9h<_sDC+$aW zZIO)|g4x&k0lV>#Ke)Q5U`i3*O|EdwBvvGj zyW!Cx(w)-hL71;Us$CVRWRax^i%T9{D<9q}zaUI0LjUJd@dV)xeoUzJaE-Y$*2nbf zx!jGU7>p0S>O;cUyptE^&&I91;Av{JLPFF~h?5*xH4q|xoojvW!$GK4MIk;nRtnSj z{3vRQMNXVr30q=B;h!m090wQH0*~CUN$kX8s=$~A4x)j5(JB3ZU4OFgBV!H)2wyeI zOO)r^$eBVn)Vb@z`9D&o-$z(H<(-q!X7h*cm5V3LhMq}^afeUzOaX5GY z$n536vv_L*1r)yl`C ztyf`f>pBE_Pa#s<+fiNGf4T~UfKT}Ei_%p3`BpbwVW6N@=#+aA6shO z%hM=AjmAX$Pe-(+7GDFotVI(d#<5+<0o*QaqIM4Dv-n^030gY++*XtC^IRA-zX5GN z4`5a?Y5siU&@;$fqd0%aMPwWqPpSTLoi#!{YP-E=+Z2*_{{~TJA_cbJ zhm)g6c6@Ty(g(_LbuB?Q(~7Dd;sS7-n$1=kx%aazC??PTsGHF@JNVijnp8kyjLK3Q zJqHxFL(!BH*hfWg;?RhEKv-2f2aeReyX(*EtW3QD1I8Ih^^@h|=u)6Kf^j&4gpYUF zpM%5r;$_$lNX>GPzdrEe-)~smJq4G#xzlGgW^-=kn{IXiXuOr@cJ*^+W`IYTZg9QxtBH<%PSBtSrwF>TUmgka)>U?P6tn3r!_fVAPa=J^B;fqkmQ$3 zs|}30Tp6*Dfkb^HI24CL3}{-`3-#|DNcjxuxhd z+t$Xd&#f(uAV%D&C19E{juJM@1giYWpCuKo^UB$PxRo_omG^pl>L(NmZf3e=ww%`b zi3U9(m&~R^{_1fC!bDExHz1)7ebpuGE%bsfKrgb3C=sGJcxN0qkUjNb% z+{$HzI3z}#(``OJ6YnGtkHD=Z6=(!!ohYM#GIAE=)O)A#tfxWhK65}||ACzYV1+cd z*EtTvKLkXf%&I+Sg7W%#D>I1XVk!i}&3d*){#&n`k&I!E-Ur6>_ukHfcd)L^9%s`= z2Onfw=vDL$Mkh`E-b^;r2GU%mrbD6#^C3Y;@3QxdrK9SnzCVZ`eftdvl+C2UfuU8Yq#kk z;E&Y=7hxnS&-W7MioQWuza=USq>4m1HC8w1UgBEA;v0$WVMReQhG&g{Sx+^$XQK2F zYtfZ=5=*s)yX{xJug%!V5_P&V?LZ5IOhxwWF>SMT2YNYYRKMM+Xb|9~spWBzeJbM} z=1s`e6WORDa-R2YYWf&lgYlH%)-A8jer|wCTU#F0@R{$I-$dZtrVr7rL@i=j2coRx zaUDGiOrC}OvG^+D8qlP1L+rOXhfXw>$nyYXg13Y?`G8iptX$29p;d&X`{w56(%uv8 z?EU#m29`Q1CO9Tk4@zC8w6RDBxbjW2| zZ${q@Tx_+K(^cH;lJs1w_5|6qgJi12jU;QC5 za6Jp+KN{lN3w(8ZefpuBCL;4Wz?Eb8E9Os={tc8;#F zD%NgztCMCJt1{P6&XEbRbDZZb3!l|nm(?Y<+$WHaXYc7>PcpE>V_5XZil?97jH33W zI*Z$yb=+DK^xTH*UeuY5`X0Tr9}&5xJhwN)yqZS2ajzT)Kk)dW$wG zSySs7t&5K%={aY;YT)X)bVQqCBgYmYzf+#(;djIf;{4#{ls70$Gi_Ig3O%fyj_aoC zG6Q~5zrmdccjAjg18zsJL@EitL=(72C*g9-d}#WTYwk8RC0$2{3M<{s{#+Rwxek|q zU;JUaf8epdFeNmXp1H3l?D3b2TfT4>ExCrcxx!>O0aQoe<~Qd>&S32rm((B-c+^P? zr|^JDgB($Q8HD=galf{w2KTFsF;FWuGd)>f7hPvg(%r&J`AS`@=tD#2Z2gXV++;pI zeuIa+gE$biS`9oz(#-D$!EPOxgLjkK1>VqPNN|YF1s#Fgt34|)EfEm?#4yIdX2WcT z7ZJ*aPyDKhKue(%gF*Gk|IqB-anE50UVx?-uEXK0@9%olV_0w<)7axuz?%j%eS6a(+SE(#v&cx3o=eW(dxjE~tJsTveg-op zJbyel8FTW&(q(nFDrB4K)T!qNOG8rAiVU21t{(1F3Elbk zKav*dMLI*tZ;Iv$n{ir*jI%D(Q{eVJiSh%BE0) z&=7D;hY+X@v3~^i3&|sOz^4#+T4Kf<_!Cn&ZaQqf=ons`MUFEv&~4u063M;)jrdK_ z5ORri!>=HOum>(LACvUmJ(VaGYB=S02RmeGK24F&^~Z*hx~$NLfQMyGuT7)IpccI{ zq9&R}e=LFQurfd5Hsk6x;*&+fP%*+rzCP(ihxWFt8G|`W4B7GVwI00OHvbpn^%*8; zn|1l|tIU8PGI&9fn5L+r!YN$eb6ybB>X#Rbl4#2=8>lHz_5(XtqL!X~C-j><*jw)33-po*9fqOX|uwvCIj zvxSM-mGhz^r`?;1nXiGMswyE=bYFjW`O_5AX-6e{KNL>vk)fUVk$RtfdlM3KV}eWY z2zH0P{e=%Ut44#Hl$nK4%I$Y))n}C*2qF1G!(wKu{iN~xF3<*j}@=H zP~R!w%(SJSF6_5z5R|?=I8NYBA|G&H&p7-{cd z^xSsXY`5W~dvKWZ+;NnQm-2Rr!$pH9on?AV@G_mxE@vx|TlmA$nDFyQ{dBeki|+@i z{GPA$ee#GpSm^f0ny!6=BCqqP!57#|7CqiVsJUm)Qa zGCwbUyE@*rmV#NuV5=P^f~q^X=uOXT-0+Rx8n4_?5Ye*`)Boi`MksgO>t%H#9C)8p zL>${dr)Yo*EVl;V(o9jN^X?0>y25jQQhMu4M$#3ZFi9tBUn+8mh(Z^q#r(t=HW<13 zSm1;mOValUUc*64Qdjiyc*i`dGquHIw?aI2q$#+3)2xz8Vja$-FJP&POU6~G`@eTO3lS$|B34A*Z_N<`nc$JTFSmi?N3$mZLpGO6JnOsi##1& zX)Su`oyDE-LFVX`rPDaL{s*%SClIo9@=^Hw2RWTJg9^igQs$a0M)}xn%mk5b!pjDv z$@aWRIl3{rs+AzxA|-E~ll#NSrO)9NjaKBJ>n_Okk`)5baS#YAI~qKxB;R!%_PDTj z>V+F`jbPbgb+7q<^1@v|SvR!TERWOB#`zp@u>=XqbXGL{5HJQ;^ z@!|F`R>#&Ub7qvtN3nBG~knU$Mv2&gViRok0b$L^t@Y}1y5U(3?D(8Ssn zW$q+J@iJokh9mvPu`F=mU!UEk#mGNJOh~`uXV)%?4eU>l?er9rmjdTUlllcqRvk-* z8?2==Rv*b@#T`FtXY^~h)^*$(pQ79sh%_E+*PGb8?MRhclc756meWnU`o&PkK+-xD zy2%6lCA>C3jLfbSP=CI;Sf?$J3O{4!iB2#!*Ph9I@`UG!>Z^|PXrCt;D+4H}KjhnQ zx?075spRN$>L)A89DAJ!h~DNoul2uTeDhf*oWD@=!ZgRkNoZ&^zu#oHq4wjEHVpQ~ zq=t<#u2sItJg3Ky&N#_e><0LQk$xMMv&`)=pj))Sd3RNaDF1xYUb6vxtF^{Li~=7P zyQZ+R9dev1TW_B3D7`z||CIDUE_%5BVy&B*LGO)uHXkbJ@SNH`UWk4CjB6}Xy+ha4 zadRcIdFPc%VR~cj*7nzNsg|0RNK-^N89>S;3%*Z;oxttF9M`mf{F?n)6gEjZm8yCU zyaHkcgPcH81Pm|XgAcQOdmryDjkMgrlW`Zh&Dg_V3yD#qj@J@jwoW=}>nMxuv~?Vr z=-3sWv`Om4$r;c>CoHq&u32%m+Gbv@+TKtgiLKLUc-Ttp9>+;8zk+4`OizAPi7}8! z+SY!4U*Ef)zxro$|BBQXyCEBY-mJw{y+fh(K@Se)q@5#%G4N}Bj|tZROe5rQ;Gs%s zPQ$xzHxK(G;XKiZ`0~>~&8*Ct`ecxw^EB%Zvhqy7-jf-OOi>3ANIb!vVPS;@@@kN< zYm;Zgyc1vT@apU_Zi1bG8sz|Ul2FxKGb#jU?1 zO+j%xmFfaK?ow6#r9s*M=;(f@8yUC&x-n2H@r3lhfO1T~d2mLR{eSv}7bP0m!5qDb zg*bnD_IK*8PC?9hH1?z=zBYDc^y2~W|49X|ei@=PLd4MFlbtzFQLrr%5WWMj^mcsv%;qK>R2h{@s3Vlm0TqW<^PA?8mH8>g)+D^WwSZC1q*W~eIq{I|@pSqciWtP?L z{m^`UrUJ~c3Y90+|N0Q*RP1H|tC z|HA+KUb4{f)>MzEru& zx9@BgmKc)1jxsIG7rm&0R~-=m0MW)shIN5r%8QBNbbc=07Ue&8ivKjwS0x(=I!~;_ z@L7fek#X`$&mH8io)sivyNR)OyKRkk*Mh7rgy73^3$uXDUOw~R0$EI{YzX@VR1NjXoUgd&$*^EtqE`}cJG-j@ z6H*#QM}lGdzX%!(upy40(=Yyq=AgN?EAD(bTHU1jU$g{Cio5^4K6Rn>X*R^(`op1z zfa%?TOseuzR>v(Eu!7AhL{17WbhQyj6o7xMqiVH*O{56wP zYUi{R;GBj3%#<%1UxUx0Bg;_|kFFv7PgbMYA1-X=V=9le+2#u{>7uK(@PyOcsq^wo zM~Vy7>%kJr&ilj8{7<{&W#CLB+=oKtOv*QvL$ z6nOdF5V*RDTpN@v8AWRh&IRxSQQ#2}|E7SWbEZFlpV)J%R;3I*vG@us0mf#YQ|T&1 zrN1{hs%VlsrDlL4!Us;cTAW>wIG#+i*i5V`FUEs0oW0@!>B|>pAO&l-2<+KDlyf|d zi`BZ$6$g;@sbdd|iHr0Y30F`4UnY`jFdT{wFk9aJ@(w_+ngh>B1ukMA-YGTzsQOv; zgh)dapxE+i#^pWfN^f-vB$>D}R*IgQwc+JW2hROPL41)+RNxTGo`108HEF`F-96uW zYXM`hg1>KMbM8sPV;;7lsU(x1r>SAjTgh^T^oQCEI=q_R*HZ`BEteisUzWH8A$8#x z+7laxacoFB^UCCSEQ9aZFPklKHVT+UC=&eLAL|?-3HlWP@N+0(?(|<5$%%|dQVaN> zf8j{ddjn3(a)l?9e@{z4dFsyvN+iOa#p42H{@Y)xFiP7(-ibGD%b7R z(ZU7`z{AWyuD-mdVA?tk;Bsy`0~ob-OY^%RVN+l}n(1!41)0m%16S%Up%3#`C6o5w zAXp9L_mDKK><(zT!Dv`nltn`GdiiyBz&hrSI zC-b9_i{&|>nhfN>F2jw0kG;wBNFv~y~`s&2}_U$duRS}^7eBMc;G#y zry^&ZTft0;HxcAs!VqVZ=~x4_ywe)vk9yAEBltNW0D4jA0i{=k1FWL;qSz=lycB5t z&FZGKxFBvzv8G0WqMR4&CWc}p8!+K^D$m(IVevGFH{y`OO4lCtkt^NGi4CG^q(x3f|o22stG@eC6o@a)B2t$A@WJU>G#ybp2 zJ6E(?cT6#Hw?xju=<{i~4!a*eYW;vE&aEZy^TgUD1SfXAiz+vEVUjL{*jbfOH;1wXxbBhA4} zm<703I#v%{e|%Z0{49Ah`2%nflpn^W*)LbBL@WlLJ9~0WE*S5xifKM|(x3N! z5~Vdb8wpI8OUSE7Vh_zBW>u7bAA`o!Km6-Pr(|&GeVh&8oRPZZy1;U5^MGzc z(F8LyrUs}h(tt82^MX9%;Q92 z7n$7*dMx$w1K(}N709(ly1xD@atS#ffK0@frq^SPK;$$D`53&$WzbeY#98;gzQ`xu z>zRW5020`6IZIz5p*+|mvWG-2;zSZ0&TZxo447409VR5*jXOqOAPYX|&ILlR$+|N!)-4>06 zm98A+$*=i0QUOX#+c@K~9&t(t{(^gMoOw+2#nm5^ulUNZF^ zly6nNWY#SNb}h$RHNYYuh16|!j8%K~{x2E$fk0r4VLFagEPae+*hYj?M4q|WUax&0IT9Qz1Yc>tkcas}&>CJn(D0X3jzUk!aAC62D0u=7QS zJ;@5}O~cVgV;TV97ZdS+3twOdG|E;ooK0 z0mTpaz*Ut85TqJN-ITvQ-l7jdYI7dRNNfQ)X3q+PAdjr`GARxT2Of}l8bn)7_4My% z@m5jetzuT+@H$;eYU0~AEx1bsJe$B?awM6gS$Q`JJhP*C)S+gxF#mP*oqJ3};CPG@t`JQSH!R)MmyS@|CldX|rFK%87sT>Qn?C&@1)0r~ zNQ2=7-dwCoLe7|K#4%6^`QA5N?#OShJE-e|2*aZLKGIc3J7WA$GPlk@k0gsV%b|J6 zv1L;k&Q03j;Nl?YRuAbq_{CoHuF92lj%MriF9s!LhhaL=9=gYYD$F$umdcMddJwiC zlZujT$IROl2K9YDRJUjN8CRTy&Ecaolk400~Sm+DazG zhloMOMOw-&@0yQ}XPKy^if5^$lIpj38>E zv5jWIO@csAKr?w7U+EE{h!QVOA1`(8IMeXbgH05sO_a_~223Kzt><2JY@gH)r}GTb zR{gl)xiMF+u848$^QP1#b>yHvgUO2+GYO&Zm^^shZ*o8A$gm?y^;WUhyx|M)-Ru4o zxa&7T%vqdF-218?vjHIuZU#jiR>v|2m%)eKkB|(0__*!Y+;AMC`D`Y$*_ApDkxRVp zDe=2*YOQ=liRI@+4Wj2o56R;qMp!9Qb?i4&9az+c7Tq)3dqiaEB!v`0ID}35qQjXfzi}S7 z2q86iS!j1S$gO=q`cS1vpVH7w=GcWbJ+9(QT#BSEv}jDBI*C;9sg-JRNEMFA+|K8D z0j}x+l76R4eE+MSb*!ZNm#bV8objxJFosRmS7FWAC9R(K0|gU@1;_B7X}`dV6*g<& zJ4_2WP^Rey-VSs7Ow@$q5O=i~&$CB$I5Y3S^LtvAfn#OHk6j zV!35rIYbx-5f745mUB63q9zsoSeNh(WyH}r`JAoz?Xb_gH&a~fjUxO`J7hgX&9)L5 z@tg`Dbu%&|0e=vQHTrHqKgR$*KAVq$D&6`)T7Q=pL_Ao<=mrG|$ZC?TUYk9xWVajv z6fFUw+-x36xn^L8Qo7O`P^NZScCj;dba`h-ke&X>1eLC=6kn2RTNvh#y5~;uWM`8YjW-9 zWy&Y;^T)QpEzOp)w_vcS8W4I83!HNu)fzaNSDdK1paqZ&gRq~?61LiR)sHJ$N&HdR zNvxkGww{+0n@GUzJhYocb2R8Ki{9zH?9~<4Ry16E^pwXht31vz6FxyiWGM%+ z{ke8vktQXs2r68pO!8gl`=#&IR(&0Z2A_p?&K#14v9&omwmGZKNRqCq(P$6PCduiY z+rDNv6QZj;ytt#BQ_gbCf&XH$9(j9@IWnl2fOV1?CMPce1bE{PPm7?NDW-xivLi3f zkN0v>S?*0o-a2084`2*hjsuznWe3!cr1d)Un%;H1QYH?N+a%VhFJ~jWLqN)1-bE8c zg|p5i=NHDrwf*RH$g!8yyam(2ndXoazs^|#zvsQRiz576IS|ovSe7vVy=g(vWh~ZL ze#g7_?pJ!!%-Qq5{I1$;A1kvaE18`Rn7vGeUsUz}0SOpm;qb+&2+WKTD*E1NiRU95 zc9z5jR?(I?(;jPg?#wrQd&UrKMfo{e7;&mYZ0`?D<7b0Z$%jPQL}%xWjl}?e=OnlZ zsXUy2cn(Fp@#A59td{<@+W0*8OM3}9rizX~XhC)$d`QRJx_~4}_(LV%PfXr<2E%iQ zN)1R~*I6lqU+3+3(;h2Y^Ol_@Du;Cy~T_ zi>iRPR#(q}3|fFPqVY;BzaHdQZ=w7_wz4~r4iq0{CcMd+=6Y3tH;}qW&QyvsnBAnz zFCo6lK@|S5ClP0O>OttX_tTfX+xyF<&x3g)@JPs58UKL@`@-S0(8(f)Oo&SmFN0Ob z0T!8eTa-fiq}T_Uoq$@l57b@SyZZ@vauBXQV3RwBn0wb==`N@lqcym7rDI#L--8Jd z=&wvxfh)HlhP6t5llny!7G7BCzp-I~HvD`hNfg|k*Oz5zizr25lIT`rIa1q*IW%}` z@BX48;_%7Zfu&-qb4|OYVv^(Gb1xJ87F>< z*Zm(PQC=?FJS=ge5`lpGTJWW!rtw$U($ECX4R@~prX_?|0p=;xMG|$tAxEe^+4qoM zqGdkwkTv93D44h^2tEC5GxKZCgDg$rog3EsSD;0Vt*BFQf7hX(72W4@M;vFd(|xlU zRz0O7r1kK66@ai^aT`nX7^^NwW*0y_`=Wg<<9}Y9F-WXLjFRND%bgmPuLH9Hsq3=x z;clgD74Q4!ZW9k39ET)ZQ+Zk9 zOysFSZjBP2v-yZAkE8fs9sh*Uug4F;CM=S4_}vl#Cn~RfrY2YGeP&Cb_Ve3@+#)?L z)LH(S(eG7ZB=y$zk=hnUHIjpg{B>UkAF7W#!3x79vlStk=R~Ah-9LLuan`48+xg^N zeBIen?&a6~e6G@%+$Q1vZ`OuXfa?%HBvy@V$r1P`fAWOK&s3+b8Y6q3RQoeAqk&4A z)ds_rw*<$w2NnYp=-Gi{!6!uj>e))!Bm9knug}cW;5|o)*Nfg6C}*S&$iqvcpV(UE|+kkz24sWZ76b!9%4w0!?M-AMfq{Sr>Rxa8G+kVM#rA0}$2R?50b< zy4=#uM)5uump~bLh7clF1$8X1W5Qo&Zz@$6cqCZ$v*5^$N(zR)4i+8UGOrtNZUnT4 z#IATWFa$`B`87oI@3IOJtHUAuXn|4VWhl;6e-=X;rdK}zM92&e{5p0GS#-S;ep?>> z=cVIG{e}fy8RarSGvS+2fMo)-D>-yA{?7tPMIJ&u32fo9R3A(0)33G1^KSdTsF{s~ z8vwfdg{l_OKd+{#^tEejIjzK-$oeLQtI%Ta%6pz$;-w5o;E5eWp}v<_G5$ZJkDCIg zRkq+wYF>Y1lSg!M`HIVWf6;&DtHS8pf^15J-ZGfv|5-!*o)^~Y|Lb;E5S3l3CTToahZDVR(!c5;NmfIDBtXH^=M)9^4rPwP|9@ls_u}?;0FWjW4}-2 z@R*i9m4;|AOkX&73b;E+CV`*2@)h=UH(~DIqm~!Uk2XwcMz>S0Djmd+^=u^IVD69M zViM`m@4K3tG%hwiz1iK!Gs5&0^Trm- zTQ_f3XZrmO@F?OGF`;*^?=Iiy1e>K?bMJkp4YQS_9`dv{3DiZ0VV-5cQAK3Iez)lN zGr%H#Yata084!UVo19D7bicK zmrp&rySpFe)2sbLN}sDqDw`JD`_O@ac5eBtQuoe+@E%{c#GCb8+e(*iyltDwM@+zm#WOmsx+f$26%; zz->Y)EKnT1ovYG{Cazu&)Hg7_A}b*bDr~3FzNmEq(X(ueHVNAVS|8JqOs< z9$3TKK56=iNVwkc)pMbL{VO**8OnF@s1Ksvfyx(Fiz=%1Q z5nF(4-iyyLFtfqzb4qLgM-zqvEfFG_ z7B~g?%;o^Aj@v@Z>Y9cuLt8wKiy3RXGP|-5_kr8(6o>GvY^7~gH@+*N|IY&?-VW3X z)SGhG*_tj(y9Pzd>Rm@NOn@eY7u2(4+vc4!!O2oS?+-cz1nR?FpMkppU?YeQ|CHgj zWC;vQD}Q|Ouf?_%y}hb|z@ql@Jg-dbZG;&}g#tk0)E_vA=nuYMHc`ZxTx&;1n(kwM zzCJv#Te9_SRw=Ni*30Wr=#BhYsHE34iCU+IoWpnX>2>W(tT-|lzYLsPZ@P7L4^!pF z8v%pG=VBrYN6>NY~ zp%N6=TSl!0%<67X5b!WrsP5gzX&`8b^Be=P>VA|;hQL%<2SpWGYAT=o{si+&K1O6I zWSNNc>*d?)ER}-Q)I3r9FCV{hCqj!E8~fFEo5~zkFu74`Zo!QbTj8&20M6})5+N^u zWC{+bPAl@+-D50|K#tR$0^TdNUSq2_9(@oEhHYVu|Dirk!LvZ`tJFxuNmi~*Y9eRL zVtT7h%Ju4I*6zrZ)`tY2Q+Z_0o2jy^+K_C@I~a+WeNSN_wXbdGJSKMl3m=G=3eIB~ zBW$*eXm=yumx^BQ))X^A65#@pNg(5XQm0}U#o2fy1VA$VkNrzBR7x*w0qW#UQ@$Oz z{8Y=p%Oz_DbclhAEC!#^M94EaIk~CMs7T9YP!P2w_8RDck!l%gqh4^o!Ihg zT)~@L={mD3b&>QI%#EIqS`#mQ>r;P)qgU&;n6r?k668ccej+QOl)OZKNPZ2u*2&-7 z;+d$%?Db0}YKzVglSR0 z`F*4S`(BvYkPYoxX6`3Sg^a=OBm-|tu*5Um6z&^H*}e6%+)tsKWOKH^flV}48|u#k zhevZDv&?;{p4&AK@J;MEkw+?932wITK@%2UfZ@~5dRy?kgR|1%+~13t5}Zgh)D#td z!AqhD$^t28ttI z<*RSgFM92)KjT`Z%h=lwCRS@UPS7mrIByfAKRG2lH9-g6#;*hpzlq@)_Mn;fb6}5& zb=m+Kx-uW@j}R&ibMM0y(cyi4=Fs*U7;Aq#06Kz)Y>jpQ;ch{i@}+lid4m z;9Y$!;O590Gq|^9H6{~hCI&+iqo7B|d_+T^#-bCHm<+fT;ZghVeaRPWY9idECIiA3 z(M`iLn1VNqbsgFfzMye91efaL2!HCMl|7nU!-vr7d4QYX#O)#w;Hld;@)RnOH)yG1 zH{$7m{1nI5EEGvZv)&PYx#LU?>X(K`kO)qD;yoFqewM}ObMx90;Ig|wK7PEM@`PJg zsuV3MZ!Ai)Fg0yqNvF2bujMjN@2oK)!FB(;TK&-Vmkzqq?s;88f(EQQ!~#X$h>w~# z-paOH9qv`=caYY2??~#{eK4SruRx6LL>C;wZrQSXeGO5%9+6ZxcZ~n#5HVtL|ZbJmE33Lx!mO> zuxraWx{>Xr8Du3EoTp>e!*lfy zb<2z>LU1rjp@JCYPcBnpO$D>JJ#U~ht`FbO#4A&JDVFG3oAdFQ0IOfU-43s7-X)z* z(@Enc^AO8^z?6|hc93Ny=y`Z}2ev6)jEn7zd2@75nQ|pA24}7-MWphLH<<@+e9s0ca(Pnpg4};EqbY8H(Sb6s^EN^yf2BZ1m7qo*GsFHx6w|z2;_|K=k=|4}h1Scds z|0%yE%&Vl+G|BpaYrey%7R%zQ6#V=Xq`aAhyjW_>zk#o>SSDt4+Ul0D=bTR2^FqyB zQzorhQJIL!#{C*@LhzD)FTop+(@J~%4LMQ#ec|V`=IiSMnq3u|TW1sr?Uyw#nbC4B zEREOTk#Lzoo7#DW@&0*bEI)Lhu~gmMn59w6(TQx~bbfPfeo>oDjZ{nczy@qca!3ap zJ2bv1=MIjJRW9E@w}Y6^m}UGvpKjmcdqJ=c-C+t#i)3De9)Mz&xO;c}xJAIrzzca} z|D2c~RdL6h$KEnyB0X5^=MKuHN(t3-=V2Z%Eo^fmKYV?R@@{O{Pb6-$WrB+PyQAo! zajp$AH^uq+`O{U#GN4*Oj8c}!5ZYUMeGPg zRKBt$kK5(NvP(;cQh9B+$^9|S@8CgehZp%h+jtuwmx9ABv?vSgUW(FTlFLbI>HI0r zC_WG9HC(SYzf@{Qf2xuhD5j7`Oix4gnb6B1XUCb@6FxaEQTQb0WAzs*w&(RjfXLeQ z{Ta>kkjG&DV_SK|+_8y~*R`)=^fsl(OpMg<^;PIa#~)LW`6-@w{VZ?4vA`6|lgcHfQbR#N7SEQ- zdB^|{#xT%V*O%K|>srezqP3zG-ud=QEZcUfplokWq-A;X^(kpBD!p>0DeGrh?f9S> z=PXQ~5ESSa)rxS|--DY}nJX(o<5RT<$Dad!FLVT$M^T=4NvrhoQv?5VNAIMK;k_#| z;qmgJd^{Z6oG_8M3<@Nq^pi1S1sAOX`k52Nq2Nbi#5Oxjam@_uE!d&?+$bh?tw|?> z@pw52wQR}3bdq6S-tJ~Dn=F62)zahVBv~fM(w8*R3h8f{HayXl)B*6EQ>UhtvL;h- zP?y0a{64P14`GIg_n9b@-@S=nnSxMCu-Ox1XKTZ_(AZ5W z%M2E7YV+3%Ad5{~brpB@u;rq1d_((2@#?*bRMcfk%+zg!!Qu#pe?@nC+%(o1W;@m8d@Z zGMNLF5MeMgCJX_8*ekfds$i|-DOUYs##eJv8jc+M1AJY6R|6wnK< zgiyb*Dj9bJf*Uvv$>dHMoCa+IPa0)6@-pS#f=8!4?RI4I75jOW45$@_ zaRxcH*dtJ73Kc3xIxW9!rI%#AVEelQSMI^A`aHTi%(GFS8{?*PM0;gxxeiun{VbYCaGw(*;kHl>NYW!`ZCfu$v$?68c^*frpx*k$Daq+; z;7?w2chhh&1^&C(kHO54jRPaCWJpsXXING@T@AVFfYJ;ap1_oQkIm;?L6fS5s1+h9 z^pd*O0@BVD{g&m&7p947Z^~#ZwlS7>_m43Ry|tzrU)=h3n7>Xh4Pwo z;p#?d6m$R7TG5l)n!*4SmJv`3<8bzZ4~M^ZT0G3VFa-HF zT>^I4$M4hETl#!fr$FL-wj5M{cfw1F4I5Ix`b5s-4r0L07f;t^P}ikdFfAL$lw(-( zH%MjSm6F-pBSkJOJb~o=N@5Tc{#_kh1gyP@i0@!5j#d;|Njx4!W1{Y!;V2*2kwWH4 zD!FMtKN^6ELv{(OdEd6X!YhFFY6oKwWu7KDTF} zM2!-k%6$+Wq2CFuoexYsfuI;3vW*&zfnODjMs*I~9xvZQem~~fW0k-VX>RbC1=J$o!X&#=6isn zcgpq&6k4{=AmPuXc<3KiT)7=bssXsyE!jAba3^t(+n7tT&SM zT4afj=7L*h4~VZ%*uKXbQ0)F%Xas16Eie*r1<=X@1O);3NlnxJ1?P2`nnc;wT3x+RtS=y$b)KFZ)%l*N}%wYv)SAFE5p_<9#!JYA|~OS){`J~nFphQw{% zP}<*!i6qM&K`#15H2Hn&@5`2RDajPj`0rtTH>3HG4GPR&?wucPUIrt!)aljRq>%3D zz6PdPk8a;+A;09BPSXvo|77R1|Ini{y6ofOK>gCm!lh7cSxg;Z^j(l_0B40&``y%+ zWr(!UFq8Ss)tIM|?FJy?l?~RG`OKrAoZsdDwG6442bV0uFGNFeQuLo##>?g2a2|Ue zl4%2_`Zdx7q!5R=R(sWhtlq8DP*FBi-FUHUjoXI4h)ITJlf+#C0fSskY&gz{U0Fj} z_JP{T*8(jjs2A&_ZD)(9r_D= zk4XkVOB^hYJl$Vf@W&F*2VQKNUPQnTadiS9Mi$*%$dLP2cCo=s-xt>f+s*Uu9*oAS zWM$S}>g!4-duhBi;==N_`E*Lyv|;?b-dpMz zC}C?mTptsbqfV+0CiCcnyffNbyBc-2-7pVk_^oh+KSq)8jTC-sTcnL1^)lG6m)}o# z5R>R>i(iBd8IfqNJY-0=GxXphO#U)*kB@3^V!jZ?6G<$w6%iX>lSJ`$-G)^17A@p>3=97X(?%QDcU%hgN@b(#u9Te&6~; zl*SPo{ea>F{c#+zSFBjf;JZbP^^4^9S?WFoYK@hac~UuSb1pAOKytD~c`A$}A_oB< zDIW-p?ltcojfzz0%)GsD45do>NWxu>7Y?(^T0{*Pdx%^q*6=-9(JsMHcH*a+au>o3 zTI6M*k6O1E=`LFiVqGl12=#Gp9X?H}@2~2e zZC4gsT9t13^7=?g5b)bR!c8~n>UW;fnG#-osngH61h+dDL)Msri~gNlpWB-+K?Xhq zk`B0ySCwBss3Lh8c-LzGvhtG+(@$G_68iS-+v30{st(9Zp`{cnniRGEs~(r7X=V;J zedr;8&N&1A3g`< z)nZr_jX)9k@-8G zFFMcj{kitP^uO{wFLk;-pRea*KaS&m+>iS~`g*(m186!~1L^Bg{)Iw$r})>OS*>x_ zs=cS?8=i`4-Q)&g`4Ed%h-VYn$$eEAH1 zNRlUhYm!h0zHeAAtDwIwGv6JKe7NEH?;1x-;j9*~G$%9pTWYATyrP&#U36nsxU-1F{`i=jb>dTZO4HZo_H* z3QabJ8JioCu`2z2 z{IItarq^>rw%GFKxx%G<=r&~&=S6KnhU&{p&N-hr-Q&}L5~W`nswXHVn~Fx=9R;S% zsN+7Q2CQ1Y_}CcTM3%kqw1-kgu7_OEd7FSKs5G@ToSwb?QB7`gMS47MgYqWZiQLQp z)|a*3{y*NNhjth;cavd}A^0Q_k?90eMjAx`zhJm=GD%2)fy;#RZC%}EHWCp>z81Xp zq0W3(?a0w|wg)x3dfw%P$5A*eev}1vo+Pk z2=V093Z+KA>g~8fTk5a9e6>|#yyqlT$F34I)kpw*<+vhNBNxSf?~<5j zor{~v=rAQV2YWwnOuAdNF2{mJ#jKwXotq(%^a~WzcFlu6weIfuk9J}4pMHHK-ui?k z>fjoth9;3_3i;hD_&Qjbif-ih0$qE=oq{ThO6na<*pdNx z2MrJcF|7mMUo4FB73vCwwWoC3=&OqGgbJ3(O{IjH@D~zVE+Rcu z(x=d07(y~?@~$?2EAC|Wl9o%*8h!INzmbuV(N#x`4R+@g1(v(h?Z41Up|b~VyOUYp zsFT&$-3SFN9-^9;JcBmFlbB_Cf-)y-#Kq0P`*)2`M zh#ft@lcdhvqzX=K4pX@INMXAVZLfgyd4((e3CGU8K(!Jni>2<%{?kB*8kpk>SJS96 z+nt;{vU_$j_rTNoDM4U?%ZsDqG#HwdCPf?We(&)eCPyPu>FPTci(z)v*+igboit~Z ziCdigeKrYhJrZonxs{IO7ktji%=39X#=qU8=>`P<;8+J8{1~q9wbqDRl^C<9$?_O> zFRG}x+@48#U)UUnGIJ$BTZFakh&Iqd+#blbMQiHnT25h6CbrrIplg8wtqY2kgQFN! zMgMrPg`e&H?LJ^V-knE-TeKb!uoMy$uPSP{+sWwUT5nS)x;uoI_b%=N-sN^_9LkCn zixhL)=<7t^jd~ruUah?Q@jSJ(sqkUTDqRjR*n5*C0-ogYufst^CyydNf9`v7Qc_)| zAphs5eD3$2c^=C(MGLC2@;ULRxXS5#9m!LDpW4c|`s&`DyL~n8?yKK7)!d_H^;6I& zBNEe>uqqZl6{{kvB3|QWYImTVP1d!mW66mhRJ7pGzKNG^gHN4JHF4{=JK;^fLRZ;r zy-It}h3>a3(=T5HT{6lmvq|#Fx$G%5zL0+gEl*_nqlSyrm?8KZM0AZ`nT$?y%Sx2EB8R0)9wwc5slPZD*4BDk{f%qx6(X}UZ+(V%tKRsS+sYt2Lk5qVnS+&% z1i{5HaTF^6z4!dJ2QG-XrRql9i<4?RNBX5cMJloWy8#>!zAUhr#i^bn`vUXA&r|4` z)}D2LkG*V`dfS~fy_AVUslXz}E1K%Y-Y5W7@!=4+gHy#XtM{G`yJW@Kw;v)wzK$P^0?!E`WAr7J_EH$*~2mA`8yf?4Qn%l zdYpHq`-x2NQ)a^nSS|_2fCY!7D-wQ$d2^LVC7V+GUbX-difZPx`s}C+I`##VbHj&! z`i#W<&6`!MkNW(EI0QBHJ|%gnBSTG=Pt7aDVfaRo7N-F&rUMDk-_;Dh41%K$yuf>8 zGg`+vKf*jsIV@=B?Ottg^WWkBzwJi1J@ zXeK+S2c^rb8b%p%o+Ys__&xuuMs$WF_Y{f~>8fFt%EP~1B?@s%9=;e0jq&U+d%w|D z=X15#gpA|lDJpoH8d(fC=~Yi0JSN)Ga?JNjUx4vb!onYd!om!u49`5;TlPL)qMGiS zd84f7oYAqKX|ARLce15fMf@;7b{&2^x0H7J@3@>tr{z|CYTDMyid1zwUH)0Z{vuF0 z5^!D<9My$OYuTOZrw%vbG4vu59lBtRV-+~j%X;oR@g@hLdjcJG&rt+j z2AAM+mqLR7mKyUom~}(sOA8%n87eM(TJcCB`M^tkc*Es3Maz{P&9SVp<6oZQ5`2YjyB4h5gN4uZ~Pgp>qhuGi1mi`A7YA` z8H6a$^EEMX>BR7I;9+`A54{#GVRQ6898z&#h*!PUs~qs!SW;u_83|`w{N(Cj)XC#d z{`4s!Y_2xgT&q>+4CZvu4$!V`R?~(r0P}Cc!4(Sgd40(9N0$_j0F>X4n?mf zaWugtxMbzrJ9mCxDVZX;J)uuj zDROXIfA6vUspqRa)~!}M^sR@QnoM#{U{S*EQ-a;NsUUyiXg3Bbp|GuX&M=lZ&$hDa z?J`lAA6%iZ7qcuAdX9->l951{FBkJW!ncqi;3Yv@x5N%5in^;+3=TOw9D2H({BP8g zj1r!HTP(Q#_i1jd7Evl~P3xs$=Z>&}XFy76@4tHWE78%K!w-RL{TNksNM;p4R|5bs zCKNRRc2#W!ycU_R(8EKUgcKIa`w_Q(Nvk0y#UDYO4rT{)*goHksI^r!8DQe@1K?-! z2Zg5xM}dX{lrfhfU`ANPtpQtV$LM;lC)AdWMpT-5Q{|#Ei;3}EIT`!-Aw!N z5<)G(oc3Ccd?04aA}hs~FpAOT5P#KxYL05=Dxy>drUFG{{2fZ#=m zG0+(ef5`my4^SkA0Je=iTA`>!Cv2->5rfwRrDNCH`&r+7W=NMsw(|lV<dlOUwu&RK4l{>ZPM{Fl>B4N3OP-9hp zp!$LB6H#Is`ulTe0L4(dosm)J<)g!yOb}KZ`rw#3gzDN5ED51bSiU8AQ3dtTyI-+C zyf?kFhR0B%T0FzK8udB;b1!6wE=qNJJ3z+x6|N)(Y*1@5gu?Iz2uq3!$qVQDz#Gh- zAMZ5ywNl5NSH1+|BHPnH$3N|)M|>*k)w&h6JgV;%tOK+X{6H zfQ4N1D**(X+d8mM+k#^yt?UC{1>{EaTUX4V;tlH=a2J~e#JLFYuGgQK-?$_pgfk~d zA>G+ydm<G~#&24WuD=P5aLXFB$+s;3by34$yu*V0ts9d}4~p_AwUZuO)||a<&^1 z3EAUpK@$Y0oN(x?b6D_p&_bd`UZ^M7)g!tSo*T=gVmE&@5>b4$`#mA?R^;K|1?}Z^ zxpRy7Gf{iG)-?(qtz=mU{H)jgz=Y5Mc0IMDY&arc3aR{_@(%3`iwr@$3G75NSEIiS zgrLG3uP^3*s>(9Di_2E#y9hvi?sXl9X$D+lB=wqxuuBv#hp$h!tCHuWpGF!ueESf= z8HKXdQ+l*~s#~yLh5(NyjZ=-(Pj)&S&lE}2wa^#kwmbE#E~+>9tum{xTRfEi@|OTW zgan{>i41X(LLnYIAVn59kmqVZ`6ujajs(*kiGdWE@I^=p_P4w9`&8I2l0CQW>%X_X zUI?oBjfmuMmu~3?WBa!OTYMmyl&Op8>vEkgo=lC>=ZWXK+Lxmv5G&-6<_?gahFMg( z3d*k~pY?I_=UIKXCF9ICpsg+Hu{c^q4|5sM;VO7yA0U<}910aQalK$s_Eb2kVt(3m?CZj`(x^Hh0U-OGbN=azoLNRvomDTgUyq7`}RV%h% z&jin%L`skkLY?>VPg`=RuPv&%fkt0mzDH^+%#&CR7QLTtj@q%3@Qz>!k4=``Wf=5! zwUg!kB=P8UC69T+t(_~TzBsARzH8jkGk&qVx8e~L(@NNR#RlMh*o7pd$_LeELrzZ8 zeb#=Y70C{|G;TpV)Y{W4Tz?*6V{@?bBj?}=dD?ZBvnNhxC}-w&Fb1MVTqXlIV6Zjz z!ja#+G@r=JEM&J%AYsSonS56BRLO4M zO?l2>G;6-^Y8a{>xf@ZmqTnbH+F|FctyoX|Qi~b6<{IYC?QAVi^4I!DC7whxaZYX7 zkiIm(XebqdYx-M?CWKR5&5O82NJoW3w=jvj$?+G2U<2_eNw~v)bd?S>wAL||%4luH zS`K>bt@l;OGSU>06C$&NxqHqmck-p01|104MPQ0PESl_l;00yHjZdz_~`K3?+~ zEXmG-Pw`o4Hw`Lc?`i5K#@p86VKBnF{V~_s_~IFUNEsV~`V_mHU3GX2t(gS8fum-M zql5y(M2by^bfjnC7OXo{ zc|c-U9IZ=xu3RPItn@>`6Vhx7SydSki~ShT#!ctEIrTbhj4j4R>%$s>+T;jUiu(S> zQj@qD#xFUB&4>4W`EIxj!zNilr4a+wGlYaf#KH|0T4lb{y`emZs1&~1k3NG3n?Trh zpLxqzFNuvS?tg4KXu#?W^td@8>j;0LLu{PbjZ3N|rYqmxSe>P9ryENnBLeRbp~n>s zy};l;u+|F{6G0!!Nk&Vn(_UV;;`7Nss>x5wr~EG5BsipUg&=0{e75uvhl)iIJI0;A z+h@pTtVpY_g3BS3UOQ(V7_qOc#kL*7iJn+TRf&DwlZx!DAtI5u2 zX~&1au=DDX!3eG}DJmuPrT?=dbFS_(ExH7<*_?EPJ5wS(%w|sSti0@(uEZ@$>6gYV zIfMqL%X<;IFX8{mIMKb!YRsRSnsT(lYI0~AFS+xIg#)za7P==@0ey>fwyg{d| zt80nW@!_``yh|Wpb`j(5qIVe+Lkm9?c|0kc>AeDc5ys_bLn8~L9uQPjBK)kiZXCje zxS1l@AZ1VtfyE(93O>j2I-j|C`T007E}G|NEXwMlUcJ(W7^+pM+wII?2`>GrvT;ty zR!pym@UYR!Lp?r2G$G!xllgw6i%-iP*olm9GG^MG+CgriIbs$zwgRY)rUl3eh_X&}Vys%i;d3*y{omjyS%B&nM+EKb$0;_--vuu}P;}#W#AcP&D!jl4}=Xrh3Qes#T-a>La z-EB6eL55j7G>e?C|5GAUB5Zqi^{Qhch4=p;G~BVJSj{D}dKJ&DqehUUH#q$#X(?D9 zq%w?e(AbaN+A-L#qO`^EkB(y#N}{;B*S|fV7JwDyQEH=ItoGU++b6~6|;>e z{!Sjj)V-;$YtJ12bow0BI1H*MFd-v>ckMeZ*lWbtk2N9Edg11UIe8+pP-|-ZBl@7} zd2BRlDDSo((6X%R0=}hZ7u^J)&mMN|)aRSXR*6Pn1u~gc^x-<5!bPJJBTFsKn9B_= z`^nc?hU`$sJflp0=qGK5Ir(P5>*r|*i^H6|Ks*NCWvnMZLP(P*u|D9Oo!FYLJHa>a zV~~Y@b_r=e4+2A6jd<|anG{7EkS%rRjwsNZ`7Pe2V3@i|!7h$r5{b_`9A16%@QS8@ zb=(Qep+(dvgmj`);`z|K6o&hMn4$a!eraC*4hB4ajdcp+Q6;v5z2PX?;D=f2$I!#{ z?HO9vsQQyl>b&m&F1c;6uDdyIF0OS;9f68z+iBr>LhoW*9=)$E#_hF;PDt-PPj`zM z>wUCYbx~1wZtec#+AN^HdqgkBfp$vE83?mby3S33EjTR3tzN5mJ0U##n`4HFUaQ%U z$Num8zf>yfH4WdL{d~p2XwRB5>rJs9^Ojp<@0e7Cr@+8N(#V?R7Xa+vM2hVc8ckvwmz_Re?+U+q;OOHy5 zp5C$PE3t$7X9n){EuMQo2@>4^ospejA^tByznXNT9`{bivE<8?iY%zM zRm2^SwR^Qxd)8f@Z*=T_3DWuSrJCEGYM_Dg7Aa_jP{&R=+Y2k?UBhtkeh&SthkRf8KGgdw6rOjmWXwvwJ%3 z`SwT^ zjwdlhsM9%j-`ClIZctardausT`mf0b#Lk`Un~bL?&UAo`j%*MO+?cjYK>n-G*<<%4+)q8h!9*?T9Vz;+oZOrd0;J5}VG zdn-~L==iW)UK25W_JS16LK}N-pkBr2=ZfcTxT=17uLKB#n3_%1enkr?#3Uf1SSHzA zt&c>ymXb;KtO@poU)P!drpM?2o{}Q8rN!u|aYI5>{|Gm9N^2 z-WUSI6*0*R_-x#!TBi4w*@M_#PN1T^H?buhS8U);&x>ckGppvcOh6Vko5T$!~ zVi(o>`LsV)CU0ERJb&WSSY$g)M3!SZ5~$JU(w89@O|=l`V%)oua@9is{v_UK_VRJB zk4X}bK<}7($pFl6);+1SZwub;HCe^JlJF22_4J?GYKj}sK;N2j%B|TNFiFUhpa;}2pnP(pfPj-9K? zq&eKACmnN-Oe0eqY!O}FFP=^-p+9BCIP@PN5$CRFNZjfENzBQ24E+*p9pj6v7chP& zSYL8*+iw5xlWfVJS+kqHkmwBN+A(!bk=dH=N zmxLLszJG%78tAh1NPl9{CS+KI7iimmz{wmK6i0B>Jjx)H4u$N&M5lg-N<*#U8%;Y$GU=X@k~IJ`74UjlNo1Nv44zXkcGcOhWcH%Xbb8Q0n; zcqWkB4sD5yL*f@Q$LQ^h3L964ESUYP_97V_#@a)C7>X?F%>uiClG%4E%&!L@C{mg^r`8MW>nY z{l~I$T4JiOnPqn#YyY@()#?#uyCkY0tMHWL;sa0fk?YHR4dp^tJ~3%*O(F(Dup7@Q zm}+U_kSH|}HOoe{IZgQ34-kO3Hn1!6t@S9PKqrC|5f$b3{`roPHm=NRSVD;(adEJ% zijy>*bfa?Z@(Nw*@x{(sz15*>nlOZ=K&M1YWC95XC5`1A3l{=w=NbicB=P&%>B~c8 z^oiSPYDYW2#qc!;6v4r(1JbvvnL+9&5o3~*8258@({Nq2IMrZMV@!F?^*1A>Pogg%ZC^ew%rql-)(MF*pjwIB$GANkg!(Mk*|_O) z^tVwcI!sn_xn|>tZE3hA8q^yE$Q|v{~JpJyA`luj?%$^PRuq&VUe`A zY_CF2^ALN@@o)95^>%e(j3=aDupz!;qZnd%Td0{I8i}srDC?kxb(Z4wp&~z>EJ&{^ z5in`<7iv9cAAj zEl}ho^+8NOh@COeZhmRriPTY}7|7v|)zC7V4Z2_)`R#TV;aLoMVhYK4Y309k0uyFI zKVbrJKR`DzWVyxSKye!1d3U!X*)nq{h~&_({v$!Ve|l;+leswKIC^5jaH0L}(_*_^ z{wLom&K#q6k^QOi4cMD&gjKjDXC=H*C~%9%TH~!6!h$~LQZ@@SDHSNqv;+sk$SjI&COO|=4h&@bg=YI z)4>XRjMFqcHa}23@nYz}cKKrEkY7>$<@Q770&U0;tf2A{4?@-Lf&v~Sh1%0gDvl3# zBHK7~qx+xym7Io`7s88IZSTWIU_z%^_i;luRF77p#DKa2gE=4|46$ zis5#9oN5fNPzktB)Q&q?VB8EPK4o6M=|SJ)jHi3cbTyYB;oY15O-q_@9jW5S#1l_t znN)gRK6I3(xM0xtORNU@x-RmT3(k*Ux^f!Z+@iRAkQ(kgxTUbIAe6fFJ$Gb+d} zv);OH`R;`R!*Y>QX$pZEqDOHc2*y2!RcjMduzya4OiCo*$VUQIr)w1q5QrATcgc>E zn@c9~F?@N&M3b-BOFmA1`F!zT=ujV0L8dhvdh>Yj3zZT>wy%DxFYK9lVt5&_A)Z+R z2w*nV+o5(giZ#u?`k!;!NDoCKlG?$Aq?r3_H5fj9Fc?9J37MmUh(b_0#TxJ9XS{tr z7v7(27~txux_Dk9Kq>%p9j`ux$HfLiwUK+l3w4Hs#s2hoJTT`5M2BJCS@Au7H|!QGsKCN{f(_X7`O2Y~2Hv?++$^}Gy7 z_`Rz#c>RjG#)7ejeo=ewYi`b&^Z$<4D`d1{`Q6S5k3S=oKj5)Kvfsg+R)XSWm5ePF z8d;i7NbWwE{8R!1E*TiWnyil7LIp z=nO~L6PxiH8DkKvn6*eEuE;4F%>3e94VB}KkX&JRNc`CFqC(Hl&bX+TB1xf${;k-c z;Lsy~I_&54=iai#SekaXj%2U$&MT;le{!nelL zy{+yPIs9%e^JH#D8`-aK{}W+Aje>bI?M^x?b%O6P6U7IRMQtEw*EJ>KK!0WY2}-3U zNRkghQC`h%XD(E7{+@qt8DeVuw%<`%>FN1U)v$}88iF{iObS+1ez`h2kC$}z-Ci$7 z>Y}{VbE4huV${r}f~lRRLZ!Ati@pydC2A2d2hiAzAf)2|h8%SZiPON1Ryb>OpF+7@ z5h>b-xSFT5kfct!TgcV$Xyratv6YU6UyT;`nDuMH%l}!E_}-|cykGc9^XfdLIApXb z+q;EGogb5&(42hm;;lA7GWNN1YLULG>V!lO=*3Q-XpcSENdV7Sf8$4T@b(%^mJv<5 z__PbGzP%a%00w+0^zTLRD_;LP`wMbJ20^cO>V%?3lKGXdxjG!>W4cCx ze2Y;3r;_8*tyOSdvp8gOb{OgqjeJC?Kp}>gm8k`h45jhddW&Qwd(_Tvfv*=Uo;LFt zk%d6A8)Bd78ylxuL3S1rP~Cxep3&-kgmcFU_q3x<>3dJXP|esDC(qj1m(<3QoO*v5 zD95mUBxu*~?D^FD3`+WXNwz?SqWPHTLA4{O>_Y_b%8Snc=KJ?cRLE9S0%Z3u>;IN|hy+TJjf2 ztfUiJKlrxp4UO%ShpT^-n3_QJ&kk0gs-{3oWH@bvOiHm?*B1s&VIlmGi>Ti?O^3*#DGxfY+?fj4vQaalXa9A0Hf$KWFvi< zs4+$p&Jq|1?&;t=511>%RwM~uA=$y+S^ZdWJ_~-HoV3}S-cIwgVm+lsJkC^KbF+Sv zyeOz#u4v|BR(fNZ0NT@uyDlOkxNV|;rbxy5IBcUM(&KoHWi>ec z$ywjCb?FL9yz0h{=`{6t^Nb4ac`(Sf{R#9ELpsSh%sIOC!kGu89A>{&7hB)~=b?we)|mW|&(>f1IMt1?kEA^I$Bj*xS+M)SSb zRPUz@cB;zWBl)rCUDq+Ah)Q@Rv7g*i9BGrpG_y~xNh#L6T+#%<8cUuH`6ak-cPv}7o#DgC`;EdB; z$j~j_7K$G9fE-F0Q&xjUNh+(U<0_BnWhLe~JI<5_15X>`@lw;;)ps@6}M zAU*#XY6FkgQKGCWJKBU-(qQg-VA}+CCkw&R}F{y%svT+F6}a*%LL@?=^iZ1CBn|ji?fwx zXY~7j&+LI%<7#q{Cm=MRUZ`sKrKcp_GlaW*24Rl9d{e!cZ#`ns@;e4^QejM-fGYWj z*fqe=4#Cf^N5t&HNaFCNWmoqeOW(p{rt8F|2A=uqs8CzwPy^3gc0lR$lhYAe#~kVL zZT>;A{a?Y94Ff=i;}u0HJ%c5Mw5ya@PUf9a%*o09KVy zV^V70LlKp9PyA!-(6|k}N4B0(QP%|;n2O23SLvyFu=x5l{mXwMs2@oIv-$3ceXP+D zJLX*)LgXbPMEE@uINBz&`g$X;DvV5mDb3cRxbi%Ft&VVka}X@cRL)OYc@DYRSV$j0DCpnl4UsLFwY#S?ala*qjmgT& zW|nBNp2p76F6Jo~fkoFH?1iK4xw-i$hJai(Rz$$4LS%B^=WohAI5;@)I`IDZ-8L+{ zjteK7`K%j-aNik1(lGXLiynbtwOCqEY}=ukHRLN1u(DHBZDh+<5%!BruD73QNaqTlqJ*H;hw#ttMs>LF$8NrhVS^T$8m-@=(a-vT= zt+jvQ)Yo0O>U0E_n1hB>R-i1_O4rZ`*b+OzHy4gKkN^jErtK`hM!JU*1jLj#n-9< zV?(wJK4T}wVm^j*3rAbP${Y#VMN>uQT01Mfc|h|mp0qHQLjRDN2b+Nb7KY3*J0uPS zqd|oDfdr2?i}g8lo;&ZXE6uhP4l!$dUi?dYg@;r@O-2rjMCRzgKs-~a2E`Jch9EvK zd7YB()*~JnrDZ>&;eiC*AoAD{(2eeyqI|e4sK)YSsex}zO_U8sj}l2E znqqo}DTi*UH$!`O&$hqEblM-&o~0Rozm3$LDv5quhtn7yA2g;DGM(jKkeeNbNNABL zC=L)yawdBD<_~%o2R1%Le7(B!mx&1dVIe8M;p+NEx zGY{tAG%N^X+y}Q!)U>qBIL!Sxd)xaMJ&_sl5l?hj!*G@3fKCVY9PR$`57adrgFEdqGHsPUAL?q?uX9n z!Iv_GQ6<0U-# zLyNRo6x~mxIly(6Q5x;F@A8UW%P{`dS#sd?DUHjA9d0BPqMFK|uQX;*X#R%_Vydpi z<>$+UG{^6s06%~f`)1tnasjqwwC=^|7v%9@tWr)3PGe$9wi z&8O01cc54Bf;ud7)wkl5m_w_P06QwLBE~U~-1=aERc*0D=A5gF7~A1l`v-xDXqKco z4o?$2bSCwwhvH<5tklesg&q=EgbILr=)kw%lbK4?g`CY zCm7i+RP)$sC5JrMqkT>*3+IlCIbqWw7x4mfd!75i>~p@}B29&JE(d3+w$_yRJKf99 z9VIu22g0f}=eXs1*py_P*10>@S*6yb;j{GT&TZ5cWnA6CNLC^ED+EzE*f?$q9X>mW zQ?lIOab{)U3e%X)$R;D#r9X1tL_ul@bDyX{L_a{)LYe%q6X(&K9NPVnoWmadGZkkw z5?1$Yvv$|}ep+MZ8u}TEJg@GceE#rx9NSJ=j(OA3_1mMajs>Q#-$;vao53xlkBKhj;IEr0UCC_kg_(j(7aBks|mfNv4mRk7% zup*JMp|NqR`UL-?W*$ecfp=>RU7(C$5 zYKiR|JR!`%$2%OvklWA_q3*k6=P=`9P9m5A)T(s%oy_CSAXm|)n@Hqdc6hd6_M^Bq zX9=b8Vc1P6v6@Om_#>dYgqQd_YoZ0XssbD_bLWLpgcEmK-9hCp=Kpa??~e0(5c>3b zdH~c@W@spgw{SFvarEs)5DJN!(#>GyKJ3K@;=A*@R8j!=c>|Bof!&HI^>I zI-ES(kN}(*G75@odDjs@^N8bhwCG%AKs2$+Qj_h7vw_)|&~!Gm*u=S6b3**0VJhpg zw@CQ6q@G;o?pV$H@EHO!ydLNi{xh$c#1m`nbqnM30(STRJoOyRb3BYRn3J3rcFM-c zX@0xb)=zQoiOMxBw}M;nv2269F$si6eCpC8OKa-x|eXYU!TPE zR+auIz&eA8H=nA?$!DHq^pdtvu8MBk1>mX>Sm+N#jqH)A08B|;9eY(S+cT+_x&jT)bqey7)l!#I=OT1QehmTknuTcr2?%6=s+JFE(qhe#nNk$0SKb>gNor!sh5CKg%Z`*q#M;K;3!bNcr z8>;@$I&@Fuo4p(nceXs;bfN6I_cMdTSv!2n^fdMjH~y2;TYadMdK1zNdjAPu3}k%t zLy1>$e=ZK3kC?o7Jd`{^Ati|Dp_ppR&HMj;MW6Khz_rSj0O@8<&s@+!yC68bDT>ua|3;!>Z;*04xN(h zjy%1g6BL}frU(<^+!f7kr?SzVS6qi86(hikL~6*X?Y%g2$sAsdh)rJ^O{bP&TB0n44gB$B1Wem#=BnMgh}6hKl#SvYT7@)qb#)tb7d!@G z;oL7BwhwUMSt`{8s|sM$l4j1zeu&t|cdVWU;1N_;&kX@QX!SLWk>7b)*9~-bHM#;Y5csDrE>8kWoz^9t z%_~zCl1UP=aHVmUpiWW=ptKm+0cdat>4K{r5OU$#JSvoG!Ay`c43ZlElks9;%l(P@Kjx7gW@RBA+#tI~Xq5jZli_N=urvmGd3N46BT7DuF zC<26FBv}8%v?tVYS@Jr5=4a9jsMQpLUST`UV<2W@)NZ4%nVgha>QWM0-zTP zJL1)UKx&b3y>=RPHK_z1{t}Wt?%4hds)cc;y&2pDcxT%kI@X!y*8{MbL6rXE44U`h z$6nP>{sDIHK(xu5xi0C~)PTl&?19Ga`suyrJ4(qOO%A~#8$Z|8-1#Wazs}u(0$zhi z-3;>~M1N5fl2*QR1N#!O3RJ+^zi1`sPly~3m$2;@!imD%#*t!K=&~O`xu&palFM~^ zdg^S60{_5g0-ACZl(n1Vhv;2`B=#(5>*Q4|Ld`F;0CKL9$UFK21eXI}w!{jw1)MHl zk-33nbZE?dx`Grlt5s}%#qp`KS`ewP=KkioEsXTzj|^DMJ61xE&cp-cGEfn6lv4$tw0F-gQD?Tm1-Yx1UJ+Sp_a!-dv5P~GzPE$t-bBvq!+8>a2FwVYQy5H|Z@_I2k3FTli+ zb;OKV5J}N249Nc7CWU0gq*Laf+0VPH(h`?nd1LqGLND<8R`fSPoWmvR!-pK(kKQNt zC+tsjOUH?a;`Bo{MO8rj8S~Hbbqh##2V44vKRhuPE`|FNuC}{= zbzy4W0czFHKRydI*;)H&>F`QZgO3&S!5GjKx$E3Ock^2{e=^3cnwLjSNasGaPYyQ- z&kwOk`0QUYt@*C}0i|nKqjEo)i@0@|e-lfol;7pRlCR#bsw7MwYa!CmA1v*&)?Ohw zNX8;gmtB!@k#a6fkYF{!6hF4)snKAb%E$bSpp2MKw#*0Lx6jAjaTo4SFxA?tfaW#H zHK0eY9gaurtQK+GpuUkYg6$Er3|kzLVjn5x=J1>eDj0 z#D!C|0{J4p9ooovZ##5MoxD&2Qd9#7XgqRpdOkh0_yfZRuelQhEC&H?UOD(GarL#u zxfFs|B+hW|DIx#xtINsRh8_L*rc|3MvweL7tN?@0?EP(rDAy{&p{N}UA4CqG z^hhsk6BgOG-mZ;-O3VI`zf~jx`fsAAYa|WG)Nki{+N(vJFPn+6FrQ+)pVs(1W-Z0k z8*LV$KyTpR%d#oscS}M?9ThYvPp5oSlx$qtIArI|OoEwpwB$B#sVu!Ni6u!OzEhHr zHopJmjue{!<0kc!5;^J&(s**^qULYz7*MKooAyEgX1+eQX%%$ePP~}p4FA<_nl#Lt z;JNR5AoOhh0%X-a_V}#Qap|0&YAFA-A{#pG1O5Mm_c{0w2S`~W5tx;t5qsd$@3KVU z_~As>sHM4@@!*lQ%ptjPi_btI?W0&G&z8GHk_^rL-Y4llHywEAuR)Y)1wW(XnuJ03 zkC5YtzmIOTp+42NU0f|0! zEe6dVYLak>#EOPU_t(GQc121KfGx623X%XqozI_qyDmlPZ+MIL6s~moTmrT=jze5s zz2-3@zv7Bko6bKZw8hhxY83Nx|J=hD&Rcypd+$U;al}wUXNGPxzl66s3xtZs#t5$Y z^z^YLO1ASBP~7Fdr)ZrpcbDzPxtEOkBaR#`CnC7zBF(N(({mt)gbPj`*W^U%ea}yT z!SS8$dpYYb-7KiegV?g)@mO&EB@1|ym;~xT{HrsVeBJ@u-xmp*6S1hn{k2$p0|Tj4 z@X&E^=pN!i$J*Wm-W1yBBQpCnAU@v+%v#cn)g$i}`2h&~ zGkz#EaT+2=ELPTgd!TKd4@6kr>EDRp$3Klj>EZr}R%=60vY+uqi|_NgB3B?Nc(C{8 zdi*z;!+sw2LmKrRW~UU02|PFP&+wguPzB{v`;&QZ#V94ad2k~@h4-Hh-@pHqv4mGX-UGq(Tqla>exdf@V%l=$C?1@wDT;NRiEe3JGSc)P^rr~MD1 zk$+np`o-pOR{v=k>KR;wblvC40OBZvGTHz5I>)emc|DH*?|1&+z#=@6|2MGzzpdE)+P&*(SRlfCer9alaxwk5 zs>}Jv%lKH>IJiW#D6eDik2qecqkm!k^g;TA4AuX8 +You must configure an embedding provider and API key in **AI Models Configuration → Embedding** before using the Knowledge Base. Document processing and retrieval depend on embeddings, so this feature will not work without a valid embedding configuration. + + +![Embedding Configuration](../images/embedding-configurations.png) + +## How It Works + +1. You **upload** a document (PDF, DOCX, TXT, or JSON) to the Knowledge Base +2. Dograh **processes** and chunks the document for efficient retrieval +3. You **attach** the document to one or more workflow nodes +4. During a call, the agent **searches** the document for relevant information based on the caller's questions and uses it to generate accurate responses + +## Supported File Types + +| Format | Extension | +|--------|-----------| +| PDF | `.pdf` | +| Word | `.docx`, `.doc` | +| Text | `.txt` | +| JSON | `.json` | + +Maximum file size: **5 MB** + +## Uploading Documents + +1. Go to **Knowledge Base Files** in the dashboard +2. Click **Upload New** or drag and drop a file +3. Wait for processing to complete — the document will be chunked and indexed automatically + +## Attaching Documents to Nodes + +Once a document is processed, you can attach it to any **Start Call** or **Agent** node in your workflow: + +1. Open the node edit dialog +2. Scroll to the **Knowledge Base Documents** section +3. Select one or more documents for the agent to reference + +The agent will only search documents attached to the current node, so attach only the documents relevant to that conversation step. + +## Best Practices + +- **Keep documents focused** — a single topic per document produces better retrieval results than a large multi-topic file +- **Use clear, structured content** — headings, lists, and short paragraphs help the chunking process +- **Attach selectively** — only attach documents relevant to a specific node rather than attaching everything everywhere +- **Keep documents up to date** — re-upload when source information changes to avoid stale answers diff --git a/docs/voice-agent/pre-call-data-fetch.mdx b/docs/voice-agent/pre-call-data-fetch.mdx new file mode 100644 index 0000000..aee302b --- /dev/null +++ b/docs/voice-agent/pre-call-data-fetch.mdx @@ -0,0 +1,139 @@ +--- +title: "Pre-Call Data Fetch" +description: "Fetch customer data from your CRM or ERP before the call starts, so your voice agent can greet callers by name and reference their account details." +--- + +Pre-Call Data Fetch allows you to enrich the call context with external data before the voice agent starts speaking. When enabled on the **Start Call** node, Dograh sends an HTTP request to your API as soon as a call is initiated. While the response is loading, the caller hears a ring-back tone. Once the data arrives, it is merged into the call's [initial context](/core-concepts/context-and-variables#initial_context) and becomes available as template variables in your prompts and greetings. + + +## How It Works + +1. A call arrives (inbound) or is initiated (outbound). +2. Dograh sends a **POST** request to your configured endpoint with a standardized payload. +3. The caller hears a ring-back tone while waiting for the response. +4. Your API responds with a JSON object containing `dynamic_variables`. +5. The variables are merged into the call's initial context. +6. The voice agent starts with full access to the fetched data via `{{variable_name}}` syntax. + +## Configuration + +Open the **Start Call** node editor and expand **Advanced Settings**. Toggle **Pre-Call Data Fetch** and configure: + +| Field | Description | +| --- | --- | +| **Endpoint URL** | The URL Dograh will send the POST request to. | +| **Authentication** | Optional credential for authenticating the request. Supports API key, bearer token, basic auth, and custom header. | + +## Request Format + +Dograh sends a `POST` request with the following JSON payload: + +```json +{ + "event": "call_inbound", + "call_inbound": { + "agent_id": 123, + "from_number": "+12137771234", + "to_number": "+12137771235" + } +} +``` + +| Field | Description | +| --- | --- | +| `event` | Always `"call_inbound"`. | +| `call_inbound.agent_id` | The workflow (agent) ID. | +| `call_inbound.from_number` | The caller's phone number (`caller_number` from initial context). | +| `call_inbound.to_number` | The called phone number (`called_number` from initial context). | + +The `Content-Type` header is set to `application/json`. If you configured a credential, the corresponding authentication header is included. + +## Expected Response Format + +Your API should return a **JSON object** with a `2xx` status code. The variables to inject into the call context should be placed inside the `dynamic_variables` key: + +```json +{ + "call_inbound": { + "dynamic_variables": { + "customer_name": "Jane Doe", + "account_status": "active", + "loyalty_tier": "gold", + "open_tickets": 2 + } + } +} +``` + +You can also place `dynamic_variables` at the top level: + +```json +{ + "dynamic_variables": { + "customer_name": "Jane Doe", + "account_status": "active" + } +} +``` + +After the response is received, you can reference these values anywhere template variables are supported: + +- **Greeting**: `Hello {{customer_name}}, thank you for calling!` +- **Prompt**: `The customer is a {{loyalty_tier}} member with {{open_tickets}} open support tickets.` + + +If the response is not a valid JSON object, does not contain `dynamic_variables`, or the request fails or times out, the call proceeds normally without the additional context. The pre-call fetch never blocks or fails a call. + + +## Nested Variables + +If your `dynamic_variables` contain nested objects, you can access them using dot notation: + +```json +{ + "call_inbound": { + "dynamic_variables": { + "customer": { + "name": "Jane Doe", + "address": { + "city": "Los Angeles" + } + } + } + } +} +``` + +Access in prompts as `{{customer.name}}` and `{{customer.address.city}}`. + +## Timeout + +The request has a **10-second timeout**. If your API does not respond within this window, the call proceeds without the fetched data. Design your endpoint to respond as quickly as possible to minimize the ring-back tone duration. + +## Example Integration + +A simple Node.js endpoint that looks up a customer by phone number: + +```javascript +app.post("/dograh/pre-call", async (req, res) => { + const { call_inbound } = req.body; + + const customer = await db.customers.findOne({ + phone: call_inbound.from_number, + }); + + if (!customer) { + return res.json({}); + } + + res.json({ + call_inbound: { + dynamic_variables: { + customer_name: customer.name, + account_status: customer.status, + loyalty_tier: customer.tier, + }, + }, + }); +}); +``` diff --git a/docs/voice-agent/tools/introduction.mdx b/docs/voice-agent/tools/introduction.mdx new file mode 100644 index 0000000..1e10df5 --- /dev/null +++ b/docs/voice-agent/tools/introduction.mdx @@ -0,0 +1,39 @@ +--- +title: "Tools" +description: "Extend your voice agent's capabilities by giving it tools to perform actions during live conversations." +--- + +Tools let your AI agent take actions during a conversation — transfer calls, end calls, or call external APIs — based on the context of the conversation and your prompt instructions. + +When a tool is attached to a workflow node, the LLM decides **when** to invoke it and **what parameters** to pass, based on the user's spoken intent and your node-level instructions. + +## Tool Types + +Dograh provides two categories of tools: + +### Built-in Tools + +Pre-configured tools that handle common telephony operations out of the box: + +- [**Call Transfer**](/voice-agent/tools/call-transfer) — Transfer the active call to a phone number or SIP endpoint +- [**End Call**](/voice-agent/tools/end-call) — Terminate the call when the conversation is complete + +### Custom Tools + +Tools you define to integrate with any external system: + +- [**HTTP API**](/voice-agent/tools/http-api) — Call any REST API endpoint during a conversation (e.g., CRM updates, data lookups, triggering automations) + +## How Tools Work + +1. You **define** a tool with a name, description, and parameters +2. You **attach** the tool to one or more workflow nodes +3. During a call, the LLM reads your node prompt, the tool description, and the caller's intent to decide whether to invoke the tool +4. The tool executes and returns a result that the agent can use to continue the conversation + +## Best Practices + +- **Attach only relevant tools to each node** — fewer tools means more reliable invocations +- **Write clear tool descriptions** — the LLM uses these to decide when to call the tool +- **Guide the LLM in your node prompt** — explicitly describe when a tool should be used +- **Test tool behavior** — verify your agent invokes tools at the right moments using web or phone calls diff --git a/ui/src/components/flow/DocumentSelector.tsx b/ui/src/components/flow/DocumentSelector.tsx index f8de6cf..7b02d93 100644 --- a/ui/src/components/flow/DocumentSelector.tsx +++ b/ui/src/components/flow/DocumentSelector.tsx @@ -1,6 +1,6 @@ "use client"; -import { FileText } from "lucide-react"; +import { ExternalLink, FileText } from "lucide-react"; import Link from "next/link"; import { useMemo } from "react"; @@ -8,6 +8,7 @@ import type { DocumentResponseSchema } from "@/client/types.gen"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; +import { KNOWLEDGE_BASE_DOC_URL } from "@/constants/documentation"; interface DocumentSelectorProps { value: string[]; @@ -57,7 +58,10 @@ export const DocumentSelector = ({ <> {description && ( - + )} )} @@ -66,11 +70,12 @@ export const DocumentSelector = ({ No documents available. Upload documents to the knowledge base first.
- - - + +
@@ -83,7 +88,10 @@ export const DocumentSelector = ({ <> {description && ( - + )} )} @@ -123,15 +131,23 @@ export const DocumentSelector = ({ ))} +
+ + + Manage Documents + +
-
- + + {value.length > 0 && ( +

{value.length} {value.length === 1 ? "document" : "documents"} selected - - - Manage Documents - -

+

+ )} ); }; diff --git a/ui/src/components/flow/ToolSelector.tsx b/ui/src/components/flow/ToolSelector.tsx index cf3a100..89c1ed3 100644 --- a/ui/src/components/flow/ToolSelector.tsx +++ b/ui/src/components/flow/ToolSelector.tsx @@ -8,6 +8,7 @@ import type { ToolResponse } from "@/client/types.gen"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; +import { TOOLS_INTRODUCTION_DOC_URL } from "@/constants/documentation"; interface ToolSelectorProps { value: string[]; @@ -46,7 +47,8 @@ export function ToolSelector({ {description && ( )} diff --git a/ui/src/components/flow/nodes/StartCall.tsx b/ui/src/components/flow/nodes/StartCall.tsx index ca2949c..9393615 100644 --- a/ui/src/components/flow/nodes/StartCall.tsx +++ b/ui/src/components/flow/nodes/StartCall.tsx @@ -1,5 +1,5 @@ import { NodeProps, NodeToolbar, Position } from "@xyflow/react"; -import { Edit, FileText, Play, PlusIcon, Trash2Icon, Wrench } from "lucide-react"; +import { ChevronRight, Edit, FileText, Play, PlusIcon, Settings, Trash2Icon, Wrench } from "lucide-react"; import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { useWorkflow } from "@/app/workflow/[workflowId]/contexts/WorkflowContext"; @@ -11,12 +11,14 @@ import { MentionTextarea } from "@/components/flow/MentionTextarea"; import { ToolBadges } from "@/components/flow/ToolBadges"; import { ToolSelector } from "@/components/flow/ToolSelector"; import { ExtractionVariable, FlowNodeData } from "@/components/flow/types"; +import { CredentialSelector, UrlInput, validateUrl } from "@/components/http"; import { Button } from "@/components/ui/button"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; -import { CONTEXT_VARIABLES_DOC_URL, NODE_DOCUMENTATION_URLS } from "@/constants/documentation"; +import { CONTEXT_VARIABLES_DOC_URL, NODE_DOCUMENTATION_URLS, PRE_CALL_DATA_FETCH_DOC_URL } from "@/constants/documentation"; import { NodeContent } from "./common/NodeContent"; import { NodeEditDialog } from "./common/NodeEditDialog"; @@ -48,6 +50,12 @@ interface StartCallEditFormProps { setToolUuids: (value: string[]) => void; documentUuids: string[]; setDocumentUuids: (value: string[]) => void; + preCallFetchEnabled: boolean; + setPreCallFetchEnabled: (value: boolean) => void; + preCallFetchUrl: string; + setPreCallFetchUrl: (value: string) => void; + preCallFetchCredentialUuid: string; + setPreCallFetchCredentialUuid: (value: string) => void; tools: ToolResponse[]; documents: DocumentResponseSchema[]; recordings: RecordingResponseSchema[]; @@ -77,6 +85,9 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { const [variables, setVariables] = useState(data.extraction_variables ?? []); const [toolUuids, setToolUuids] = useState(data.tool_uuids ?? []); const [documentUuids, setDocumentUuids] = useState(data.document_uuids ?? []); + const [preCallFetchEnabled, setPreCallFetchEnabled] = useState(data.pre_call_fetch_enabled ?? false); + const [preCallFetchUrl, setPreCallFetchUrl] = useState(data.pre_call_fetch_url ?? ""); + const [preCallFetchCredentialUuid, setPreCallFetchCredentialUuid] = useState(data.pre_call_fetch_credential_uuid ?? ""); // Compute if form has unsaved changes (only check prompt, name, greeting) const isDirty = useMemo(() => { @@ -88,6 +99,14 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { }, [greeting, prompt, name, data]); const handleSave = async () => { + // Validate pre-call fetch URL if enabled + if (preCallFetchEnabled && preCallFetchUrl) { + const urlValidation = validateUrl(preCallFetchUrl); + if (!urlValidation.valid) { + return; + } + } + handleSaveNodeData({ ...data, greeting: greeting || undefined, @@ -102,6 +121,9 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { extraction_variables: variables, tool_uuids: toolUuids.length > 0 ? toolUuids : undefined, document_uuids: documentUuids.length > 0 ? documentUuids : undefined, + pre_call_fetch_enabled: preCallFetchEnabled, + pre_call_fetch_url: preCallFetchEnabled ? preCallFetchUrl || undefined : undefined, + pre_call_fetch_credential_uuid: preCallFetchEnabled && preCallFetchCredentialUuid ? preCallFetchCredentialUuid : undefined, }); setOpen(false); await saveWorkflow(); @@ -122,6 +144,9 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { setVariables(data.extraction_variables ?? []); setToolUuids(data.tool_uuids ?? []); setDocumentUuids(data.document_uuids ?? []); + setPreCallFetchEnabled(data.pre_call_fetch_enabled ?? false); + setPreCallFetchUrl(data.pre_call_fetch_url ?? ""); + setPreCallFetchCredentialUuid(data.pre_call_fetch_credential_uuid ?? ""); } setOpen(newOpen); }; @@ -141,6 +166,9 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { setVariables(data.extraction_variables ?? []); setToolUuids(data.tool_uuids ?? []); setDocumentUuids(data.document_uuids ?? []); + setPreCallFetchEnabled(data.pre_call_fetch_enabled ?? false); + setPreCallFetchUrl(data.pre_call_fetch_url ?? ""); + setPreCallFetchCredentialUuid(data.pre_call_fetch_credential_uuid ?? ""); } }, [data, open]); @@ -243,6 +271,12 @@ export const StartCall = memo(({ data, selected, id }: StartCallNodeProps) => { setToolUuids={setToolUuids} documentUuids={documentUuids} setDocumentUuids={setDocumentUuids} + preCallFetchEnabled={preCallFetchEnabled} + setPreCallFetchEnabled={setPreCallFetchEnabled} + preCallFetchUrl={preCallFetchUrl} + setPreCallFetchUrl={setPreCallFetchUrl} + preCallFetchCredentialUuid={preCallFetchCredentialUuid} + setPreCallFetchCredentialUuid={setPreCallFetchCredentialUuid} tools={tools ?? []} documents={documents ?? []} recordings={recordings ?? []} @@ -278,6 +312,12 @@ const StartCallEditForm = ({ setToolUuids, documentUuids, setDocumentUuids, + preCallFetchEnabled, + setPreCallFetchEnabled, + preCallFetchUrl, + setPreCallFetchUrl, + preCallFetchCredentialUuid, + setPreCallFetchCredentialUuid, tools, documents, recordings, @@ -475,6 +515,57 @@ const StartCallEditForm = ({ description="Select documents from the knowledge base that the agent can reference during this conversation step." /> + + {/* Advanced Settings */} +
+ + + + Advanced Settings + + + + {/* Pre-Call Data Fetch */} +
+ + +
+

+ Fetch data from an external API before the call starts. A standardized POST request with caller/called numbers will be sent. The JSON response fields will be merged into the call context and available as template variables in your prompts.{" "} + Learn more +

+ + {preCallFetchEnabled && ( +
+
+ + + +
+ +
+ + +
+
+ )} +
+
+
); }; diff --git a/ui/src/components/flow/types.ts b/ui/src/components/flow/types.ts index e268eca..e533277 100644 --- a/ui/src/components/flow/types.ts +++ b/ui/src/components/flow/types.ts @@ -28,6 +28,10 @@ export type FlowNodeData = { detect_voicemail?: boolean; delayed_start?: boolean; delayed_start_duration?: number; + // Pre-call data fetch (StartCall only) + pre_call_fetch_enabled?: boolean; + pre_call_fetch_url?: string; + pre_call_fetch_credential_uuid?: string; // Trigger node specific trigger_path?: string; // Webhook node specific diff --git a/ui/src/constants/documentation.ts b/ui/src/constants/documentation.ts index 7f1ea6a..fd0a1b6 100644 --- a/ui/src/constants/documentation.ts +++ b/ui/src/constants/documentation.ts @@ -12,6 +12,12 @@ export const NODE_DOCUMENTATION_URLS: Record = { export const CONTEXT_VARIABLES_DOC_URL = `${DOCS_BASE}/core-concepts/context-and-variables`; +export const TOOLS_INTRODUCTION_DOC_URL = `${DOCS_BASE}/voice-agent/tools/introduction`; + +export const KNOWLEDGE_BASE_DOC_URL = `${DOCS_BASE}/voice-agent/knowledge-base`; + +export const PRE_CALL_DATA_FETCH_DOC_URL = `${DOCS_BASE}/voice-agent/pre-call-data-fetch`; + export const TOOL_DOCUMENTATION_URLS: Record = { http_api: `${DOCS_BASE}/voice-agent/tools/http-api`, end_call: `${DOCS_BASE}/voice-agent/tools/end-call`,