add transfer call tool editor

This commit is contained in:
Abhishek Kumar 2026-02-06 09:45:34 +05:30
parent f77a2afca6
commit 97a44f00ff
6 changed files with 229 additions and 29 deletions

View file

@ -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 ###

View file

@ -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"
)

View file

@ -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

View file

@ -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 (
<Card>
<CardHeader>
<CardTitle>Transfer Call Configuration</CardTitle>
<CardDescription>
Configure call transfer behavior
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-2">
<Label>Tool Name</Label>
<Label className="text-xs text-muted-foreground">
A descriptive name for this tool
</Label>
<Input
value={name}
onChange={(e) => onNameChange(e.target.value)}
placeholder="e.g., Transfer Call"
/>
</div>
<div className="grid gap-2">
<Label>Description</Label>
<Label className="text-xs text-muted-foreground">
Helps the LLM understand when to use this tool
</Label>
<Textarea
value={description}
onChange={(e) => onDescriptionChange(e.target.value)}
placeholder="When should the AI transfer the call?"
rows={3}
/>
</div>
<div className="grid gap-4 pt-4 border-t">
<div className="grid gap-2">
<Label>Transfer Number</Label>
<Label className="text-xs text-muted-foreground">
The phone number to transfer the call to
</Label>
<Input
value={transferNumber}
onChange={(e) => onTransferNumberChange(e.target.value)}
placeholder="e.g., +14155551234"
/>
</div>
<div className="grid gap-2">
<Label>Transfer Message</Label>
<Label className="text-xs text-muted-foreground">
Optional message to play before transferring
</Label>
<Textarea
value={transferMessage}
onChange={(e) => onTransferMessageChange(e.target.value)}
placeholder="e.g., Please hold while I transfer your call."
rows={2}
/>
</div>
</div>
</CardContent>
</Card>
);
}

View file

@ -1,2 +1,3 @@
export { EndCallToolConfig, type EndCallToolConfigProps } from "./EndCallToolConfig";
export { HttpApiToolConfig, type HttpApiToolConfigProps } from "./HttpApiToolConfig";
export { TransferCallToolConfig, type TransferCallToolConfigProps } from "./TransferCallToolConfig";

View file

@ -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<EndCallMessageType>("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<string, string> = {};
@ -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();`;
</div>
</div>
</div>
<div className="flex items-center gap-2">
{!isEndCallTool && (
<Button
variant="outline"
onClick={() => setShowCodeDialog(true)}
>
<Code className="w-4 h-4 mr-2" />
View Code
</Button>
)}
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
Save
</>
)}
</Button>
</div>
</div>
{error && (
@ -414,6 +420,17 @@ const data = await response.json();`;
customMessage={endCallCustomMessage}
onCustomMessageChange={setEndCallCustomMessage}
/>
) : isTransferCallTool ? (
<TransferCallToolConfig
name={name}
onNameChange={setName}
description={description}
onDescriptionChange={setDescription}
transferNumber={transferNumber}
onTransferNumberChange={setTransferNumber}
transferMessage={transferMessage}
onTransferMessageChange={setTransferMessage}
/>
) : (
<HttpApiToolConfig
name={name}
@ -434,6 +451,31 @@ const data = await response.json();`;
onTimeoutMsChange={setTimeoutMs}
/>
)}
<div className="flex items-center justify-end gap-2 mt-6">
{!isEndCallTool && !isTransferCallTool && (
<Button
variant="outline"
onClick={() => setShowCodeDialog(true)}
>
<Code className="w-4 h-4 mr-2" />
View Code
</Button>
)}
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
Save
</>
)}
</Button>
</div>
</div>
</div>