Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions examples/hotel_receptionist/book_restaurant.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
Never speak the same question twice in a row. If a field was just captured ("name recorded", "time recorded"), it is DONE - asking for it again stalls the call; the only valid next move is the directive in the last tool return.
"""

# Naming the concrete situation is what routes the model to the tool; widening this to
# "cannot be captured, for any reason" stops covering the caller who needs to go look the
# number up. A reservation requires a phone, so this flow gives up rather than waiting.
_RESTAURANT_PHONE_INSTRUCTIONS = """\
Caller cannot provide the phone number or does not have it handy: call
`decline_phone_number_capture` immediately.
"""


class BookRestaurantTask(AgentTask[RestaurantReservation]):
"""Restaurant booking as one focused task, mirroring BookRoomTask: `set_party`
Expand Down Expand Up @@ -137,9 +145,16 @@ async def open_name_dialog(self) -> str:
@function_tool()
async def open_phone_dialog(self) -> str:
"""Open the phone dialog. It collects the guest's phone number (read back and confirmed) from the caller."""
r = await beta.workflows.GetPhoneNumberTask(
chat_ctx=speech_only(self.chat_ctx), extra_instructions=COMMON_INSTRUCTIONS
)
try:
r = await beta.workflows.GetPhoneNumberTask(
chat_ctx=speech_only(self.chat_ctx),
extra_instructions=(f"{COMMON_INSTRUCTIONS}\n\n{_RESTAURANT_PHONE_INSTRUCTIONS}"),
)
except beta.workflows.PhoneNumberCaptureDeclinedError:
error = ToolError("reservation not created: a phone number is required")
if not self.done():
self.complete(error)
return f"{error} | never tell the caller the table is reserved"
self._phone = r.phone_number
return f"phone recorded: {self._phone} | {self._status()}"

Expand Down
7 changes: 6 additions & 1 deletion livekit-agents/livekit/agents/beta/workflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
from .dtmf_inputs import GetDtmfResult, GetDtmfTask
from .email_address import GetEmailResult, GetEmailTask
from .name import GetNameResult, GetNameTask
from .phone_number import GetPhoneNumberResult, GetPhoneNumberTask
from .phone_number import (
GetPhoneNumberResult,
GetPhoneNumberTask,
PhoneNumberCaptureDeclinedError,
)
from .task_group import TaskCompletedEvent, TaskGroup, TaskGroupResult
from .utils import WorkflowInstructions
from .warm_transfer import WarmTransferResult, WarmTransferTask
Expand All @@ -25,6 +29,7 @@
"GetNameResult",
"GetPhoneNumberTask",
"GetPhoneNumberResult",
"PhoneNumberCaptureDeclinedError",
"TaskCompletedEvent",
"TaskGroup",
"TaskGroupResult",
Expand Down
12 changes: 11 additions & 1 deletion livekit-agents/livekit/agents/beta/workflows/phone_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ class GetPhoneNumberResult:
phone_number: str


class PhoneNumberCaptureDeclinedError(ToolError):
def __init__(self, reason: str) -> None:
super().__init__(f"couldn't get the phone number: {reason}")
self._reason = reason

@property
def reason(self) -> str:
return self._reason
Comment on lines +62 to +69

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New public error type ships without documentation

The newly exported error class for a declined phone number is added to the public workflows API without any docstring (PhoneNumberCaptureDeclinedError at livekit-agents/livekit/agents/beta/workflows/phone_number.py:62-69), so the generated API docs describe it with nothing.
Impact: Users of the public API see an undocumented error type in the reference docs and can't tell when it is raised.

Repository rule requiring docs for new public classes/methods

CONTRIBUTING.md states: "If writing new methods/enums/classes, document them. This project uses pdoc3 for automatic API documentation generation, and every new addition has to be properly documented." The class is exported in livekit-agents/livekit/agents/beta/workflows/__init__.py:7-11 and listed in __all__, so it is part of the public surface; neither the class nor its reason property carries a docstring. (RestaurantReservationNotCreatedError in the example does carry one.)

Suggested change
class PhoneNumberCaptureDeclinedError(ToolError):
def __init__(self, reason: str) -> None:
super().__init__(f"couldn't get the phone number: {reason}")
self._reason = reason
@property
def reason(self) -> str:
return self._reason
class PhoneNumberCaptureDeclinedError(ToolError):
"""Raised when the user explicitly declines to provide a phone number.
`GetPhoneNumberTask` completes with this error (instead of a generic
`ToolError`) so callers can branch on an explicit refusal.
"""
def __init__(self, reason: str) -> None:
super().__init__(f"couldn't get the phone number: {reason}")
self._reason = reason
@property
def reason(self) -> str:
"""The short explanation of why the user declined."""
return self._reason
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.



class GetPhoneNumberTask(AgentTask[GetPhoneNumberResult]):
def __init__(
self,
Expand Down Expand Up @@ -179,7 +189,7 @@ async def decline_phone_number_capture(self, reason: str) -> None:
reason: A short explanation of why the user declined to provide the phone number
"""
if not self.done():
self.complete(ToolError(f"couldn't get the phone number: {reason}"))
self.complete(PhoneNumberCaptureDeclinedError(reason))

def _confirmation_required(self, ctx: RunContext) -> bool:
if is_given(self._require_confirmation):
Expand Down
49 changes: 49 additions & 0 deletions tests/test_phone_number_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from __future__ import annotations

import sys
from pathlib import Path
from typing import Any

import pytest

from livekit.agents import beta
from livekit.agents.llm.tool_context import ToolError

pytestmark = pytest.mark.unit

_HOTEL_EXAMPLE = Path(__file__).parents[1] / "examples" / "hotel_receptionist"
sys.path.insert(0, str(_HOTEL_EXAMPLE))

from book_restaurant import BookRestaurantTask # noqa: E402
from fake_data.seed import build_seed_bytes # noqa: E402
from hotel_db import TODAY, HotelDB # noqa: E402


class _DeclinedPhoneTask:
def __init__(self, **kwargs: Any) -> None:
pass

def __await__(self): # type: ignore[no-untyped-def]
async def _decline() -> None:
raise beta.workflows.PhoneNumberCaptureDeclinedError("caller declined")

return _decline().__await__()


async def test_restaurant_phone_refusal_ends_without_reservation(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(beta.workflows, "GetPhoneNumberTask", _DeclinedPhoneTask)
db = HotelDB.from_bytes(build_seed_bytes(TODAY))
before = db.connection.execute("SELECT count(*) FROM restaurant_reservations").fetchone()
task = BookRestaurantTask(db)

output = await task.open_phone_dialog()

after = db.connection.execute("SELECT count(*) FROM restaurant_reservations").fetchone()
assert before == after
assert output is not None
assert task.done()
task_error = task._AgentTask__fut.exception() # type: ignore[attr-defined]
assert isinstance(task_error, ToolError)
await db.aclose()
Loading