项目文件夹

文件
2026-07-13 13:39:38 +08:00

185 行
7.9 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING
from ... import llm, stt, tts, vad
from ...llm.chat_context import Instructions
from ...llm.tool_context import ToolError, ToolFlag, function_tool
from ...types import NOT_GIVEN, NotGivenOr
from ...utils import is_given
from ...voice.agent import AgentTask
from ...voice.events import RunContext
if TYPE_CHECKING:
from ...voice.audio_recognition import TurnDetectionMode
PHONE_REGEX = r"^\+?[1-9]\d{6,14}$"
_BASE_INSTRUCTIONS = """
You are only a single step in a broader system, responsible solely for capturing a phone number.
{modality_specific}
Call `update_phone_number` at the first opportunity whenever you form a new hypothesis about the phone number. (before asking any questions or providing any answers.)
Don't invent phone numbers, stick strictly to what the user said.
{confirmation_instructions}
If the number is unclear or invalid, or it takes too much back-and-forth, prompt for it in parts: first the area code, then the remaining digits.
Never repeat the phone number back to the user as a single block of digits. Read it back in groups.
Ignore unrelated input and avoid going off-topic. Do not generate markdown, greetings, or unnecessary commentary.
Avoid verbosity by not sharing example phone numbers or formats unless prompted to do so. Do not deviate from the goal of collecting the user's phone number.
Always explicitly invoke a tool when applicable. Do not simulate tool usage, no real action is taken unless the tool is explicitly called.\
{extra_instructions}
"""
_AUDIO_SPECIFIC = """
Handle input as noisy voice transcription. Expect that users will say phone numbers aloud with formats like:
- '555 123 4567'
- 'five five five, one two three, four five six seven'
- '+1 555 123 4567'
- 'area code 555, 123 4567'
- '555-123-4567'
Normalize common spoken patterns silently:
- Convert spoken digits to their numeric form: 'five' → 5, 'zero' → 0, 'oh' → 0.
- Remove filler words, pauses, and hesitations.
- Strip dashes, spaces, parentheses, and dots from the number.
- Recognize 'plus' at the start as the international prefix `+`.
- Recognize 'area code' as a prefix for the area code digits.
Don't mention corrections. Treat inputs as possibly imperfect but fix them silently.
"""
_TEXT_SPECIFIC = """
Handle input as typed text. Expect users to type their phone number directly.
Strip dashes, spaces, parentheses, and dots from the number.
If the number looks almost correct but has minor formatting issues, clean it up silently.
"""
@dataclass
class GetPhoneNumberResult:
phone_number: str
class GetPhoneNumberTask(AgentTask[GetPhoneNumberResult]):
def __init__(
self,
extra_instructions: str = "",
chat_ctx: NotGivenOr[llm.ChatContext] = NOT_GIVEN,
turn_detection: NotGivenOr[TurnDetectionMode | None] = NOT_GIVEN,
tools: NotGivenOr[list[llm.Tool | llm.Toolset]] = NOT_GIVEN,
stt: NotGivenOr[stt.STT | None] = NOT_GIVEN,
vad: NotGivenOr[vad.VAD | None] = NOT_GIVEN,
llm: NotGivenOr[llm.LLM | llm.RealtimeModel | None] = NOT_GIVEN,
tts: NotGivenOr[tts.TTS | None] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
require_confirmation: NotGivenOr[bool] = NOT_GIVEN,
require_explicit_ask: bool = False,
) -> None:
confirmation_instructions = (
"Call `confirm_phone_number` after the user confirmed the phone number is correct."
)
extra = extra_instructions if extra_instructions else ""
self._current_phone_number = ""
self._require_confirmation = require_confirmation
self._require_explicit_ask = require_explicit_ask
super().__init__(
instructions=Instructions(
audio=_BASE_INSTRUCTIONS.format(
modality_specific=_AUDIO_SPECIFIC,
confirmation_instructions=(
confirmation_instructions if require_confirmation is not False else ""
),
extra_instructions=extra,
),
text=_BASE_INSTRUCTIONS.format(
modality_specific=_TEXT_SPECIFIC,
confirmation_instructions=(
confirmation_instructions if require_confirmation is True else ""
),
extra_instructions=extra,
),
),
chat_ctx=chat_ctx,
turn_detection=turn_detection,
tools=[*(tools or []), self._build_update_phone_number_tool()],
stt=stt,
vad=vad,
llm=llm,
tts=tts,
allow_interruptions=allow_interruptions,
)
async def on_enter(self) -> None:
self.session.generate_reply(instructions="Ask the user to provide their phone number.")
def _build_update_phone_number_tool(self) -> llm.FunctionTool:
# Built dynamically so we can apply IGNORE_ON_ENTER per-instance
# based on require_explicit_ask.
flags = ToolFlag.IGNORE_ON_ENTER if self._require_explicit_ask else ToolFlag.NONE
@function_tool(flags=flags)
async def update_phone_number(phone_number: str, ctx: RunContext) -> str | None:
"""Update the phone number provided by the user.
Args:
phone_number: The phone number provided by the user, digits only with optional leading +
"""
return await self._update_phone_number_impl(phone_number, ctx)
return update_phone_number
async def _update_phone_number_impl(self, phone_number: str, ctx: RunContext) -> str | None:
cleaned = re.sub(r"[\s\-().]+", "", phone_number.strip())
if not re.match(PHONE_REGEX, cleaned):
raise ToolError(f"Invalid phone number provided: {phone_number}")
self._current_phone_number = cleaned
if not self._confirmation_required(ctx):
if not self.done():
self.complete(GetPhoneNumberResult(phone_number=self._current_phone_number))
return None # no need to continue the conversation
confirm_tool = self._build_confirm_tool(phone_number=cleaned)
current_tools = [t for t in self.tools if t.id != "confirm_phone_number"]
current_tools.append(confirm_tool)
await self.update_tools(current_tools)
return (
f"The phone number has been updated to {cleaned}\n"
f"Read the number back to the user in groups.\n"
f"Prompt the user for confirmation, do not call `confirm_phone_number` directly"
)
def _build_confirm_tool(self, *, phone_number: str) -> llm.FunctionTool:
@function_tool()
async def confirm_phone_number() -> None:
"""Call after the user confirms the phone number is correct."""
if phone_number != self._current_phone_number:
self.session.generate_reply(
instructions="The phone number has changed since confirmation was requested, ask the user to confirm the updated number."
)
return
if not self.done():
self.complete(GetPhoneNumberResult(phone_number=phone_number))
return confirm_phone_number
@function_tool(flags=ToolFlag.IGNORE_ON_ENTER)
async def decline_phone_number_capture(self, reason: str) -> None:
"""Handles the case when the user explicitly declines to provide a phone number.
Args:
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}"))
def _confirmation_required(self, ctx: RunContext) -> bool:
if is_given(self._require_confirmation):
return self._require_confirmation
return ctx.speech_handle.input_details.modality == "audio"