diff --git a/api/alembic/versions/f5db3dfa1f62_add_transfer_call_category.py b/api/alembic/versions/f5db3dfa1f62_add_transfer_call_category.py new file mode 100644 index 0000000..5900dd8 --- /dev/null +++ b/api/alembic/versions/f5db3dfa1f62_add_transfer_call_category.py @@ -0,0 +1,63 @@ +"""add transfer call category + +Revision ID: f5db3dfa1f62 +Revises: 34c8537dfde5 +Create Date: 2026-02-06 09:24:44.887105 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from alembic_postgresql_enum import TableReference + +# revision identifiers, used by Alembic. +revision: str = "f5db3dfa1f62" +down_revision: Union[str, None] = "34c8537dfde5" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_index( + "idx_queued_runs_campaign_state_optimized", + "queued_runs", + ["campaign_id", "state"], + unique=False, + postgresql_where=sa.text("state = 'queued'"), + ) + op.sync_enum_values( + enum_schema="public", + enum_name="tool_category", + new_values=["http_api", "end_call", "transfer_call", "native", "integration"], + affected_columns=[ + TableReference( + table_schema="public", table_name="tools", column_name="category" + ) + ], + enum_values_to_rename=[], + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values( + enum_schema="public", + enum_name="tool_category", + new_values=["http_api", "end_call", "native", "integration"], + affected_columns=[ + TableReference( + table_schema="public", table_name="tools", column_name="category" + ) + ], + enum_values_to_rename=[], + ) + op.drop_index( + "idx_queued_runs_campaign_state_optimized", + table_name="queued_runs", + postgresql_where=sa.text("state = 'queued'"), + ) + # ### end Alembic commands ### diff --git a/api/routes/tool.py b/api/routes/tool.py index b54d83f..829c3ac 100644 --- a/api/routes/tool.py +++ b/api/routes/tool.py @@ -75,8 +75,8 @@ class EndCallToolDefinition(BaseModel): class TransferCallConfig(BaseModel): """Configuration for Transfer Call tools.""" - transfer_number: str = Field(description="Number to transfer the call to") - transfer_message: Optional[str] = Field( + transferNumber: str = Field(description="Number to transfer the call to") + transferMessage: Optional[str] = Field( default=None, description="Message to play before transferring the call" ) diff --git a/api/services/workflow/pipecat_engine_custom_tools.py b/api/services/workflow/pipecat_engine_custom_tools.py index 7481537..3bcd120 100644 --- a/api/services/workflow/pipecat_engine_custom_tools.py +++ b/api/services/workflow/pipecat_engine_custom_tools.py @@ -279,7 +279,9 @@ class CustomToolManager: # Wait for the audio to play or until stopped try: - await asyncio.wait_for(stop_event.wait(), timeout=duration_secs) + await asyncio.wait_for( + stop_event.wait(), timeout=duration_secs + 1.5 + ) break # Stop event was set except asyncio.TimeoutError: pass # Continue looping diff --git a/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx b/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx new file mode 100644 index 0000000..028c5a5 --- /dev/null +++ b/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +export interface TransferCallToolConfigProps { + name: string; + onNameChange: (name: string) => void; + description: string; + onDescriptionChange: (description: string) => void; + transferNumber: string; + onTransferNumberChange: (number: string) => void; + transferMessage: string; + onTransferMessageChange: (message: string) => void; +} + +export function TransferCallToolConfig({ + name, + onNameChange, + description, + onDescriptionChange, + transferNumber, + onTransferNumberChange, + transferMessage, + onTransferMessageChange, +}: TransferCallToolConfigProps) { + return ( + + + Transfer Call Configuration + + Configure call transfer behavior + + + + + Tool Name + + A descriptive name for this tool + + onNameChange(e.target.value)} + placeholder="e.g., Transfer Call" + /> + + + + Description + + Helps the LLM understand when to use this tool + + onDescriptionChange(e.target.value)} + placeholder="When should the AI transfer the call?" + rows={3} + /> + + + + + Transfer Number + + The phone number to transfer the call to + + onTransferNumberChange(e.target.value)} + placeholder="e.g., +14155551234" + /> + + + + Transfer Message + + Optional message to play before transferring + + onTransferMessageChange(e.target.value)} + placeholder="e.g., Please hold while I transfer your call." + rows={2} + /> + + + + + ); +} diff --git a/ui/src/app/tools/[toolUuid]/components/index.ts b/ui/src/app/tools/[toolUuid]/components/index.ts index e2514c1..f99bab5 100644 --- a/ui/src/app/tools/[toolUuid]/components/index.ts +++ b/ui/src/app/tools/[toolUuid]/components/index.ts @@ -1,2 +1,3 @@ export { EndCallToolConfig, type EndCallToolConfigProps } from "./EndCallToolConfig"; export { HttpApiToolConfig, type HttpApiToolConfigProps } from "./HttpApiToolConfig"; +export { TransferCallToolConfig, type TransferCallToolConfigProps } from "./TransferCallToolConfig"; diff --git a/ui/src/app/tools/[toolUuid]/page.tsx b/ui/src/app/tools/[toolUuid]/page.tsx index d9ad127..8c61ec9 100644 --- a/ui/src/app/tools/[toolUuid]/page.tsx +++ b/ui/src/app/tools/[toolUuid]/page.tsx @@ -28,8 +28,9 @@ import { getToolTypeLabel, renderToolIcon, type ToolCategory, + type TransferCallConfig, } from "../config"; -import { EndCallToolConfig, HttpApiToolConfig } from "./components"; +import { EndCallToolConfig, HttpApiToolConfig, TransferCallToolConfig } from "./components"; // Extended HttpApiConfig with parameters (until client types are regenerated) interface HttpApiConfigWithParams { @@ -69,6 +70,10 @@ export default function ToolDetailPage() { const [endCallMessageType, setEndCallMessageType] = useState("none"); const [endCallCustomMessage, setEndCallCustomMessage] = useState(""); + // Transfer Call form state + const [transferNumber, setTransferNumber] = useState(""); + const [transferMessage, setTransferMessage] = useState(""); + // Redirect if not authenticated useEffect(() => { if (!loading && !user) { @@ -117,6 +122,16 @@ export default function ToolDetailPage() { setEndCallMessageType("none"); setEndCallCustomMessage(""); } + } else if (tool.category === "transfer_call") { + // Populate transfer call specific fields + const config = tool.definition?.config as TransferCallConfig | undefined; + if (config) { + setTransferNumber(config.transferNumber || ""); + setTransferMessage(config.transferMessage || ""); + } else { + setTransferNumber(""); + setTransferMessage(""); + } } else { // Populate HTTP API specific fields const config = tool.definition?.config as HttpApiConfigWithParams | undefined; @@ -163,7 +178,7 @@ export default function ToolDetailPage() { if (!tool) return; // Validation based on tool type - if (tool.category !== "end_call") { + if (tool.category === "http_api") { // Validate URL for HTTP API tools const urlValidation = validateUrl(url); if (!urlValidation.valid) { @@ -201,6 +216,20 @@ export default function ToolDetailPage() { }, }, }; + } else if (tool.category === "transfer_call") { + // Build transfer call request body + requestBody = { + name, + description: description || undefined, + definition: { + schema_version: 1, + type: "transfer_call", + config: { + transferNumber, + transferMessage: transferMessage || undefined, + }, + }, + }; } else { // Build HTTP API request body const headersObject: Record = {}; @@ -331,6 +360,7 @@ const data = await response.json();`; } const isEndCallTool = tool.category === "end_call"; + const isTransferCallTool = tool.category === "transfer_call"; const categoryConfig = getCategoryConfig(tool.category as ToolCategory); return ( @@ -365,30 +395,6 @@ const data = await response.json();`; - - {!isEndCallTool && ( - setShowCodeDialog(true)} - > - - View Code - - )} - - {isSaving ? ( - <> - - Saving... - > - ) : ( - <> - - Save - > - )} - - {error && ( @@ -414,6 +420,17 @@ const data = await response.json();`; customMessage={endCallCustomMessage} onCustomMessageChange={setEndCallCustomMessage} /> + ) : isTransferCallTool ? ( + ) : ( )} + + + {!isEndCallTool && !isTransferCallTool && ( + setShowCodeDialog(true)} + > + + View Code + + )} + + {isSaving ? ( + <> + + Saving... + > + ) : ( + <> + + Save + > + )} + +