Lab L2.8: Call Transfer

🎯 Assignment: Accept this lab on GitHub Classroom
You’ll get your own repository with starter code, instructions, and automatic grading.

   
Duration 60 minutes
Prerequisites Previous module completed

Objectives

  • Implement warm and cold transfers
  • Handle transfer failures
  • Configure transfer destinations

How to Complete This Lab

  1. Accept the Assignment — Click the GitHub Classroom link above
  2. Clone Your Repo — git clone <your-repo-url>
  3. Read the README — Your repo has detailed requirements and grading criteria
  4. Write Your Code — Implement the solution in solution/agent.py
  5. Test Locally — Use swaig-test to verify your agent works
  6. Push to Submit — git push triggers auto-grading

Key Concepts

The following exercises walk through the concepts you’ll need. Your GitHub Classroom repo README has the specific requirements for grading.

Scenario

Build a receptionist agent that:

  1. Routes calls to departments
  2. Handles business hours
  3. Passes caller context to transfers

Part 1: Basic Department Routing (20 min)

Task

Create a routing system for multiple departments.

Starter Code

#!/usr/bin/env python3
"""Lab 2.8: Call transfer patterns."""

from datetime import datetime
from signalwire_agents import AgentBase, SwaigFunctionResult


class ReceptionistAgent(AgentBase):
    DEPARTMENTS = {
        "sales": {
            "number": "+15551111111",
            "description": "New purchases and pricing",
            "hours": (9, 18)
        },
        "support": {
            "number": "+15552222222",
            "description": "Technical assistance",
            "hours": (8, 20)
        },
        "billing": {
            "number": "+15553333333",
            "description": "Payments and invoices",
            "hours": (9, 17)
        },
        "returns": {
            "number": "+15554444444",
            "description": "Returns and exchanges",
            "hours": (10, 16)
        }
    }

    def __init__(self):
        super().__init__(name="receptionist")

        self.prompt_add_section(
            "Role",
            "You are the main receptionist. Help callers reach the right department."
        )

        self.prompt_add_section(
            "Departments",
            bullets=[
                f"{name.title()}: {info['description']}"
                for name, info in self.DEPARTMENTS.items()
            ]
        )

        self.add_language("English", "en-US", "rime.spore")
        self._setup_functions()

    def _setup_functions(self):
        # TODO: Implement transfer functions
        pass


if __name__ == "__main__":
    agent = ReceptionistAgent()
    agent.run()

Expected Implementation

@self.tool(
    description="Transfer to a department",
    parameters={
        "type": "object",
        "properties": {
            "department": {
                "type": "string",
                "enum": list(self.DEPARTMENTS.keys()),
                "description": "Department to transfer to"
            }
        },
        "required": ["department"]
    }
)
def transfer_to_department(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    department = args.get("department", "")
    dept_info = self.DEPARTMENTS.get(department)

    if not dept_info:
        dept_list = ", ".join(self.DEPARTMENTS.keys())
        return SwaigFunctionResult(
            f"Unknown department. Available: {dept_list}"
        )

    return (
        SwaigFunctionResult(f"Connecting you to {department}.")
        .connect(dept_info["number"], final=True)
    )

Part 2: Time-Based Routing (20 min)

Task

Add business hours checking to transfers.

Expected Implementation

def _is_department_open(self, department: str) -> tuple:
    """Check if department is open. Returns (is_open, message)."""
    dept_info = self.DEPARTMENTS.get(department)
    if not dept_info:
        return False, "Unknown department"

    hour = datetime.now().hour
    start, end = dept_info["hours"]

    if start <= hour < end:
        return True, None
    else:
        return False, f"{department.title()} is open {start}:00 to {end}:00"

@self.tool(
    description="Transfer with business hours check",
    parameters={
        "type": "object",
        "properties": {
            "department": {
                "type": "string",
                "enum": list(self.DEPARTMENTS.keys())
            }
        },
        "required": ["department"]
    }
)
def smart_transfer(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    department = args.get("department", "")
    is_open, message = self._is_department_open(department)

    if not is_open:
        return SwaigFunctionResult(
            f"I'm sorry, {message}. Would you like to leave a message "
            "or try a different department?"
        )

    dept_info = self.DEPARTMENTS[department]
    return (
        SwaigFunctionResult(f"Connecting you to {department} now.")
        .connect(dept_info["number"], final=True)
    )

@self.tool(
    description="Check department availability",
    parameters={
        "type": "object",
        "properties": {
            "department": {"type": "string"}
        },
        "required": ["department"]
    }
)
def check_availability(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    department = args.get("department", "")
    is_open, message = self._is_department_open(department)

    if is_open:
        dept_info = self.DEPARTMENTS.get(department)
        start, end = dept_info["hours"]
        return SwaigFunctionResult(
            f"{department.title()} is open until {end}:00. "
            "Would you like me to transfer you?"
        )
    return SwaigFunctionResult(message)

Part 3: Transfer with Context (20 min)

Task

Implement a transfer that passes caller context.

Expected Implementation

@self.tool(
    description="Transfer with caller context",
    parameters={
        "type": "object",
        "properties": {
            "department": {
                "type": "string",
                "enum": list(self.DEPARTMENTS.keys())
            },
            "reason": {
                "type": "string",
                "description": "Reason for transfer"
            },
            "caller_name": {
                "type": "string",
                "description": "Caller's name if provided"
            }
        },
        "required": ["department", "reason"]
    }
)
def transfer_with_context(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    department = args.get("department", "")
    reason = args.get("reason", "")
    caller_name = args.get("caller_name")

    is_open, message = self._is_department_open(department)

    if not is_open:
        return SwaigFunctionResult(f"Sorry, {message}")

    dept_info = self.DEPARTMENTS[department]

    # Store context for receiving agent
    context = {
        "transfer_reason": reason,
        "caller_name": caller_name or "Unknown",
        "transfer_time": datetime.now().isoformat(),
        "from_receptionist": True
    }

    return (
        SwaigFunctionResult(
            f"I'm transferring you to {department}. "
            f"I'll let them know about your {reason}."
        )
        .update_global_data( context)
        .connect(dept_info["number"], final=True)
    )

Testing

# Test agent
swaig-test lab2_8_transfer.py --dump-swml

# List departments
swaig-test lab2_8_transfer.py --exec list_departments

# Check availability
swaig-test lab2_8_transfer.py --exec check_availability \
  --department "sales"

# Transfer with context
swaig-test lab2_8_transfer.py --exec transfer_with_context \
  --department "support" \
  --reason "billing question" \
  --caller_name "John"

Validation Checklist

  • Department listing works
  • Availability check returns correct status
  • Transfer blocked outside business hours
  • Context stored in metadata before transfer
  • Post-process transfer executes after speaking

Challenge Extension

Add an after-hours voicemail option:

@self.tool(description="Leave voicemail for department")
def leave_voicemail(args: dict, raw_data: dict = None) -> SwaigFunctionResult:
    department = args.get("department", "")
    message = args.get("message", "")

    # Store voicemail in metadata for later retrieval
    return (
        SwaigFunctionResult(
            f"Your message for {department} has been recorded. "
            "They'll receive it when they open."
        )
        .update_global_data( {
            "voicemail_department": department,
            "voicemail_message": message,
            "voicemail_time": datetime.now().isoformat()
        })
    )

Submission

Upload your completed lab2_8_transfer.py file.


Complete Agent Code

Click to reveal complete solution
#!/usr/bin/env python3
"""Receptionist agent with call transfer patterns.

Lab 2.8 Deliverable: Demonstrates department routing, time-based
transfers, and context passing to transferred calls.
"""

from datetime import datetime
from signalwire_agents import AgentBase, SwaigFunctionResult


class ReceptionistAgent(AgentBase):
    """Receptionist agent with intelligent call routing."""

    DEPARTMENTS = {
        "sales": {
            "number": "+15551111111",
            "description": "New purchases and pricing",
            "hours": (9, 18)
        },
        "support": {
            "number": "+15552222222",
            "description": "Technical assistance",
            "hours": (8, 20)
        },
        "billing": {
            "number": "+15553333333",
            "description": "Payments and invoices",
            "hours": (9, 17)
        },
        "returns": {
            "number": "+15554444444",
            "description": "Returns and exchanges",
            "hours": (10, 16)
        }
    }

    def __init__(self):
        super().__init__(name="receptionist")

        self.prompt_add_section(
            "Role",
            "You are the main receptionist. Help callers reach the right department."
        )

        self.prompt_add_section(
            "Departments",
            bullets=[
                f"{name.title()}: {info['description']}"
                for name, info in self.DEPARTMENTS.items()
            ]
        )

        self.prompt_add_section(
            "Guidelines",
            bullets=[
                "Always check if department is open before transferring",
                "Collect caller name and reason for context",
                "Offer alternatives if requested department is closed"
            ]
        )

        self.add_language("English", "en-US", "rime.spore")
        self._setup_functions()

    def _is_department_open(self, department: str) -> tuple:
        """Check if department is open. Returns (is_open, message)."""
        dept_info = self.DEPARTMENTS.get(department)
        if not dept_info:
            return False, "Unknown department"

        hour = datetime.now().hour
        start, end = dept_info["hours"]

        if start <= hour < end:
            return True, None
        else:
            return False, f"{department.title()} is open {start}:00 to {end}:00"

    def _setup_functions(self):
        """Define routing and transfer functions."""

        @self.tool(description="List all available departments")
        def list_departments() -> SwaigFunctionResult:
            dept_list = [
                f"{name.title()}: {info['description']}"
                for name, info in self.DEPARTMENTS.items()
            ]
            return SwaigFunctionResult("Our departments: " + "; ".join(dept_list))

        @self.tool(
            description="Check if a department is currently available",
            parameters={
                "type": "object",
                "properties": {
                    "department": {
                        "type": "string",
                        "description": "Department name"
                    }
                },
                "required": ["department"]
            }
        )
        def check_availability(department: str) -> SwaigFunctionResult:
            department = department.lower()
            is_open, message = self._is_department_open(department)

            if is_open:
                dept_info = self.DEPARTMENTS.get(department)
                start, end = dept_info["hours"]
                return SwaigFunctionResult(
                    f"{department.title()} is open until {end}:00. "
                    "Would you like me to transfer you?"
                )
            return SwaigFunctionResult(message)

        @self.tool(
            description="Transfer to a department",
            parameters={
                "type": "object",
                "properties": {
                    "department": {
                        "type": "string",
                        "enum": list(self.DEPARTMENTS.keys()),
                        "description": "Department to transfer to"
                    }
                },
                "required": ["department"]
            }
        )
        def transfer_to_department(department: str) -> SwaigFunctionResult:
            department = department.lower()
            dept_info = self.DEPARTMENTS.get(department)

            if not dept_info:
                dept_list = ", ".join(self.DEPARTMENTS.keys())
                return SwaigFunctionResult(
                    f"Unknown department. Available: {dept_list}"
                )

            is_open, message = self._is_department_open(department)

            if not is_open:
                return SwaigFunctionResult(
                    f"I'm sorry, {message}. Would you like to leave a message "
                    "or try a different department?"
                )

            return (
                SwaigFunctionResult(f"Connecting you to {department} now.")
                .connect(dept_info["number"], final=True)
            )

        @self.tool(
            description="Transfer with caller context",
            parameters={
                "type": "object",
                "properties": {
                    "department": {
                        "type": "string",
                        "enum": list(self.DEPARTMENTS.keys()),
                        "description": "Department to transfer to"
                    },
                    "reason": {
                        "type": "string",
                        "description": "Reason for calling"
                    },
                    "caller_name": {
                        "type": "string",
                        "description": "Caller's name"
                    }
                },
                "required": ["department", "reason"]
            }
        )
        def transfer_with_context(
            department: str,
            reason: str,
            caller_name: str = None,
            raw_data: dict = None
        ) -> SwaigFunctionResult:
            department = department.lower()
            is_open, message = self._is_department_open(department)

            if not is_open:
                return SwaigFunctionResult(
                    f"Sorry, {message}. Would you like to leave a voicemail?"
                )

            dept_info = self.DEPARTMENTS[department]

            # Store context for receiving agent
            context = {
                "transfer_reason": reason,
                "caller_name": caller_name or "Unknown",
                "transfer_time": datetime.now().isoformat(),
                "from_receptionist": True
            }

            return (
                SwaigFunctionResult(
                    f"I'm transferring you to {department}. "
                    f"I'll let them know about your {reason}.",
                    post_process=True
                )
                .update_global_data(context)
                .connect(dept_info["number"], final=True)
            )

        @self.tool(
            description="Leave a voicemail for a closed department",
            parameters={
                "type": "object",
                "properties": {
                    "department": {
                        "type": "string",
                        "description": "Department name"
                    },
                    "message": {
                        "type": "string",
                        "description": "Message to leave"
                    },
                    "callback_number": {
                        "type": "string",
                        "description": "Number to call back"
                    }
                },
                "required": ["department", "message"]
            }
        )
        def leave_voicemail(
            department: str,
            message: str,
            callback_number: str = None
        ) -> SwaigFunctionResult:
            return (
                SwaigFunctionResult(
                    f"Your message for {department} has been recorded. "
                    "They'll receive it when they open."
                )
                .update_global_data({
                    "voicemail_department": department,
                    "voicemail_message": message,
                    "voicemail_callback": callback_number,
                    "voicemail_time": datetime.now().isoformat()
                })
            )


if __name__ == "__main__":
    agent = ReceptionistAgent()
    agent.run()

Back to top

SignalWire AI Agents Certification Program