diff --git a/agents/src/workflows/address.ts b/agents/src/workflows/address.ts index 5440771c8..02510d501 100644 --- a/agents/src/workflows/address.ts +++ b/agents/src/workflows/address.ts @@ -25,12 +25,12 @@ export interface GetAddressTaskOptions { */ instructions?: InstructionParts | Instructions | string; chatCtx?: ChatContext; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; /** * Whether to ask the user to confirm the captured address. Defaults to confirming on audio @@ -42,13 +42,11 @@ export interface GetAddressTaskOptions { * can't silently fill one from the chat context during `onEnter`. */ requireExplicitAsk?: boolean; - /** @deprecated use `instructions.extra` instead */ - extraInstructions?: string; } interface UpdateAddressArgs { - street_address: string; - unit_number: string; + streetAddress: string; + unitNumber: string; locality: string; country: string; } @@ -62,7 +60,7 @@ export function createGetAddressTask({ instructions, chatCtx, turnDetection, - tools, + tools = [], stt, vad, llm, @@ -70,7 +68,6 @@ export function createGetAddressTask({ allowInterruptions, requireConfirmation, requireExplicitAsk = false, - extraInstructions = '', }: GetAddressTaskOptions = {}): AgentTask { let currentAddress = ''; @@ -106,12 +103,12 @@ export function createGetAddressTask({ name: 'update_address', description: 'Update the address provided by the user.', parameters: z.object({ - street_address: z + streetAddress: z .string() .describe( 'Dependent on country, may include fields like house number, street name, block, or district', ), - unit_number: z + unitNumber: z .string() .describe( "The unit number, for example Floor 1 or Apartment 12. If there is no unit number, return ''", @@ -124,8 +121,10 @@ export function createGetAddressTask({ // With requireExplicitAsk, the model can't silent-fill from chatCtx during // onEnter — it must produce an asking utterance first. flags: requireExplicitAsk ? ToolFlag.IGNORE_ON_ENTER : ToolFlag.NONE, - execute: async (args: UpdateAddressArgs, { ctx }) => { - const { street_address: streetAddress, unit_number: unitNumber, locality, country } = args; + execute: async ( + { streetAddress, unitNumber, locality, country }: UpdateAddressArgs, + { ctx }, + ) => { const addressFields = unitNumber.trim() ? [streetAddress, unitNumber, locality, country] : [streetAddress, locality, country]; @@ -136,7 +135,7 @@ export function createGetAddressTask({ if (!task.done) { task.complete({ address: currentAddress }); } - return undefined; + return; } const confirmTool = buildConfirmTool(address); @@ -171,28 +170,25 @@ export function createGetAddressTask({ id: 'get_address_task', instructions: resolveWorkflowInstructions({ instructions, - extraInstructions, template: INSTRUCTIONS_TEMPLATE, defaultPersona: PERSONA, - kwargs: { - _modality_specific: new Instructions('', { - audio: AUDIO_SPECIFIC, - text: TEXT_SPECIFIC, - }), - _confirmation: new Instructions('', { - // confirmation is enabled by default for audio, disabled by default for text - audio: requireConfirmation !== false ? CONFIRMATION_INSTRUCTION : '', - text: requireConfirmation === true ? CONFIRMATION_INSTRUCTION : '', - }), - }, + modalitySpecific: new Instructions('', { + audio: AUDIO_SPECIFIC, + text: TEXT_SPECIFIC, + }), + confirmation: new Instructions('', { + // confirmation is enabled by default for audio, disabled by default for text + audio: requireConfirmation !== false ? CONFIRMATION_INSTRUCTION : '', + text: requireConfirmation === true ? CONFIRMATION_INSTRUCTION : '', + }), }), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [...(tools ?? []), updateAddressTool, declineTool], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateAddressTool, declineTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { task.session.generateReply({ instructions: 'Ask the user to provide their address.' }); @@ -228,11 +224,11 @@ const PERSONA = const AUDIO_SPECIFIC = `You will be handling addresses from any country. Expect that users will say address in different formats with fields filled like: -- 'street_address': '450 SOUTH MAIN ST', 'unit_number': 'FLOOR 2', 'locality': 'SALT LAKE CITY UT 84101', 'country': 'UNITED STATES', -- 'street_address': '123 MAPLE STREET', 'unit_number': 'APARTMENT 10', 'locality': 'OTTAWA ON K1A 0B1', 'country': 'CANADA', -- 'street_address': 'GUOMAO JIE 3 HAO, CHAOYANG QU', 'unit_number': 'GUOMAO DA SHA 18 LOU 101 SHI', 'locality': 'BEIJING SHI 100000', 'country': 'CHINA', -- 'street_address': '5 RUE DE L’ANCIENNE COMÉDIE', 'unit_number': 'APP C4', 'locality': '75006 PARIS', 'country': 'FRANCE', -- 'street_address': 'PLOT 10, NEHRU ROAD', 'unit_number': 'OFFICE 403, 4TH FLOOR', 'locality': 'VILE PARLE (E), MUMBAI MAHARASHTRA 400099', 'country': 'INDIA', +- 'streetAddress': '450 SOUTH MAIN ST', 'unitNumber': 'FLOOR 2', 'locality': 'SALT LAKE CITY UT 84101', 'country': 'UNITED STATES', +- 'streetAddress': '123 MAPLE STREET', 'unitNumber': 'APARTMENT 10', 'locality': 'OTTAWA ON K1A 0B1', 'country': 'CANADA', +- 'streetAddress': 'GUOMAO JIE 3 HAO, CHAOYANG QU', 'unitNumber': 'GUOMAO DA SHA 18 LOU 101 SHI', 'locality': 'BEIJING SHI 100000', 'country': 'CHINA', +- 'streetAddress': '5 RUE DE L’ANCIENNE COMÉDIE', 'unitNumber': 'APP C4', 'locality': '75006 PARIS', 'country': 'FRANCE', +- 'streetAddress': 'PLOT 10, NEHRU ROAD', 'unitNumber': 'OFFICE 403, 4TH FLOOR', 'locality': 'VILE PARLE (E), MUMBAI MAHARASHTRA 400099', 'country': 'INDIA', Normalize common spoken patterns silently: - Convert words like 'dash' and 'apostrophe' into symbols: \`-\`, \`'\`. - Convert spelled out numbers like 'six' and 'seven' into numerals: \`6\`, \`7\`. @@ -255,11 +251,11 @@ const CONFIRMATION_INSTRUCTION = `Call \`confirm_address\` after the user confir const INSTRUCTIONS_TEMPLATE = `{persona} -{_modality_specific} +{modalitySpecific} Call \`update_address\` at the first opportunity whenever you form a new hypothesis about the address. (before asking any questions or providing any answers.) Don't invent new addresses, stick strictly to what the user said. -{_confirmation} +{confirmation} If the address is unclear or invalid, or it takes too much back-and-forth, prompt for it in parts in this order: street address, unit number if applicable, locality, and country. Ignore unrelated input and avoid going off-topic. Do not generate markdown, greetings, or unnecessary commentary. diff --git a/agents/src/workflows/credit_card.ts b/agents/src/workflows/credit_card.ts index 1c54125d5..5e9b5c510 100644 --- a/agents/src/workflows/credit_card.ts +++ b/agents/src/workflows/credit_card.ts @@ -122,12 +122,12 @@ interface SubTaskOptions { requireConfirmation?: boolean; requireExplicitAsk?: boolean; extraInstructions?: string; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; } @@ -143,13 +143,13 @@ function buildSubTaskInstructions( return new Instructions('', { audio: safeRender(baseInstructions, { - modality_specific: audioSpecific, - confirmation_instructions: requireConfirmation !== false ? confirmationInstructions : '', + modalitySpecific: audioSpecific, + confirmationInstructions: requireConfirmation !== false ? confirmationInstructions : '', }) + extraSuffix, text: safeRender(baseInstructions, { - modality_specific: textSpecific, - confirmation_instructions: requireConfirmation === true ? confirmationInstructions : '', + modalitySpecific: textSpecific, + confirmationInstructions: requireConfirmation === true ? confirmationInstructions : '', }) + extraSuffix, }); } @@ -160,7 +160,7 @@ function createGetCardNumberTask({ requireExplicitAsk = false, extraInstructions = '', turnDetection, - tools, + tools = [], stt, vad, llm, @@ -181,11 +181,9 @@ function createGetCardNumberTask({ name: 'confirm_card_number', description: 'Call after the user repeats their card number for confirmation.', parameters: z.object({ - repeated_card_number: z - .string() - .describe('The card number repeated by the user as a string'), + repeatedCardNumber: z.string().describe('The card number repeated by the user as a string'), }), - execute: async ({ repeated_card_number: repeatedCardNumber }) => { + execute: async ({ repeatedCardNumber }) => { repeatedCardNumber = repeatedCardNumber.replace(/\D/g, ''); if (repeatedCardNumber !== cardNumber) { task.session.generateReply({ @@ -213,21 +211,21 @@ function createGetCardNumberTask({ description: "Call to record the user's card number. Only call once the entire number has been given, do not call in increments.", parameters: z.object({ - card_number: z + cardNumber: z .string() .describe('The credit card number as a string with no dashes or spaces'), }), // With requireExplicitAsk, the model can't silent-fill from chatCtx during // onEnter — it must produce an asking utterance first. flags: requireExplicitAsk ? ToolFlag.IGNORE_ON_ENTER : ToolFlag.NONE, - execute: async ({ card_number: rawCardNumber }: { card_number: string }, { ctx }) => { + execute: async ({ cardNumber: rawCardNumber }: { cardNumber: string }, { ctx }) => { const cardNumber = rawCardNumber.replace(/\D/g, ''); if (cardNumber.length < 13 || cardNumber.length > 19) { task.session.generateReply({ instructions: 'The length of the card number is invalid, ask the user to repeat their card number.', }); - return undefined; + return; } currentCardNumber = cardNumber; @@ -244,7 +242,7 @@ function createGetCardNumberTask({ task.complete({ issuer, cardNumber: currentCardNumber }); } } - return undefined; + return; } const confirmTool = buildConfirmTool(cardNumber); @@ -269,17 +267,12 @@ function createGetCardNumberTask({ extraInstructions, ), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [ - ...(tools ?? []), - updateCardNumberTool, - declineCardCaptureTool, - restartCardCollectionTool, - ], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateCardNumberTool, declineCardCaptureTool, restartCardCollectionTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { await task.session.generateReply({ @@ -302,7 +295,7 @@ function createGetSecurityCodeTask({ requireExplicitAsk = false, extraInstructions = '', turnDetection, - tools, + tools = [], stt, vad, llm, @@ -323,9 +316,9 @@ function createGetSecurityCodeTask({ name: 'confirm_security_code', description: 'Call after the user repeats their security code for confirmation.', parameters: z.object({ - repeated_security_code: z.string().describe('The security code repeated by the user'), + repeatedSecurityCode: z.string().describe('The security code repeated by the user'), }), - execute: async ({ repeated_security_code: repeatedSecurityCode }) => { + execute: async ({ repeatedSecurityCode }) => { if (repeatedSecurityCode.trim() !== securityCode) { task.session.generateReply({ instructions: 'The repeated security code does not match, ask the user to try again.', @@ -343,21 +336,21 @@ function createGetSecurityCodeTask({ name: 'update_security_code', description: "Call to update the card's security code.", parameters: z.object({ - security_code: z + securityCode: z .string() .describe("The card's security code (3-4 digits, may have leading zeros)."), }), // With requireExplicitAsk, the model can't silent-fill from chatCtx during // onEnter — it must produce an asking utterance first. flags: requireExplicitAsk ? ToolFlag.IGNORE_ON_ENTER : ToolFlag.NONE, - execute: async ({ security_code: securityCode }: { security_code: string }, { ctx }) => { + execute: async ({ securityCode }: { securityCode: string }, { ctx }) => { const stripped = securityCode.trim(); if (!/^\d+$/.test(stripped) || stripped.length < 3 || stripped.length > 4) { task.session.generateReply({ instructions: "The security code's length is invalid, ask the user to repeat or to provide a new card and start over.", }); - return undefined; + return; } currentSecurityCode = stripped; @@ -366,7 +359,7 @@ function createGetSecurityCodeTask({ if (!task.done) { task.complete({ securityCode: currentSecurityCode }); } - return undefined; + return; } const confirmTool = buildConfirmTool(stripped); @@ -392,17 +385,12 @@ function createGetSecurityCodeTask({ extraInstructions, ), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [ - ...(tools ?? []), - updateSecurityCodeTool, - declineCardCaptureTool, - restartCardCollectionTool, - ], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateSecurityCodeTool, declineCardCaptureTool, restartCardCollectionTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { await task.session.generateReply({ @@ -423,7 +411,7 @@ function createGetExpirationDateTask({ requireExplicitAsk = false, extraInstructions = '', turnDetection, - tools, + tools = [], stt, vad, llm, @@ -455,19 +443,19 @@ function createGetExpirationDateTask({ name: 'confirm_expiration_date', description: 'Call after the user repeats their expiration date for confirmation.', parameters: z.object({ - repeated_expiration_month: z + repeatedExpirationMonth: z .number() .int() .describe('The expiration month repeated by the user'), - repeated_expiration_year: z + repeatedExpirationYear: z .number() .int() .describe('The expiration year repeated by the user'), }), - execute: async ({ repeated_expiration_month, repeated_expiration_year }) => { + execute: async ({ repeatedExpirationMonth, repeatedExpirationYear }) => { if ( - repeated_expiration_month !== expirationMonth || - repeated_expiration_year !== expirationYear + repeatedExpirationMonth !== expirationMonth || + repeatedExpirationYear !== expirationYear ) { task.session.generateReply({ instructions: 'The repeated expiration date does not match, ask the user to try again.', @@ -487,11 +475,11 @@ function createGetExpirationDateTask({ description: "Call to update the card's expiration date. Collect both the numerical month and year.", parameters: z.object({ - expiration_month: z + expirationMonth: z .number() .int() .describe("The numerical expiration month of the card, example: '04' for April"), - expiration_year: z + expirationYear: z .number() .int() .describe( @@ -502,10 +490,7 @@ function createGetExpirationDateTask({ // onEnter — it must produce an asking utterance first. flags: requireExplicitAsk ? ToolFlag.IGNORE_ON_ENTER : ToolFlag.NONE, execute: async ( - { - expiration_month: expirationMonth, - expiration_year: expirationYear, - }: { expiration_month: number; expiration_year: number }, + { expirationMonth, expirationYear }: { expirationMonth: number; expirationYear: number }, { ctx }, ) => { if (expirationMonth < 1 || expirationMonth > 12) { @@ -513,21 +498,21 @@ function createGetExpirationDateTask({ instructions: 'The expiration month is invalid, ask the user to repeat the expiration month.', }); - return undefined; + return; } if (expirationYear < 0 || expirationYear > 99) { task.session.generateReply({ instructions: 'The expiration year is invalid, ask the user to repeat the expiration year.', }); - return undefined; + return; } if (isExpired(expirationMonth, expirationYear)) { task.session.generateReply({ instructions: 'The expiration date is in the past, the card is expired. Ask the user to provide another card.', }); - return undefined; + return; } currentExpirationDate = `${String(expirationMonth).padStart(2, '0')}/${String(expirationYear).padStart(2, '0')}`; @@ -536,7 +521,7 @@ function createGetExpirationDateTask({ if (!task.done) { task.complete({ date: currentExpirationDate }); } - return undefined; + return; } const confirmTool = buildConfirmTool(expirationMonth, expirationYear); @@ -562,17 +547,12 @@ function createGetExpirationDateTask({ extraInstructions, ), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [ - ...(tools ?? []), - updateExpirationDateTool, - declineCardCaptureTool, - restartCardCollectionTool, - ], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateExpirationDateTool, declineCardCaptureTool, restartCardCollectionTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { await task.session.generateReply({ @@ -589,12 +569,12 @@ function createGetExpirationDateTask({ export interface GetCreditCardTaskOptions { chatCtx?: ChatContext; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; /** * Whether to ask the user to confirm each captured card detail. Defaults to confirming on @@ -614,7 +594,7 @@ export interface GetCreditCardTaskOptions { export function createGetCreditCardTask({ chatCtx, turnDetection, - tools, + tools = [], stt, vad, llm, @@ -627,12 +607,12 @@ export function createGetCreditCardTask({ id: 'get_credit_card_task', instructions: '*none*', chatCtx, - turnDetection: turnDetection ?? undefined, - tools: tools ?? [], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools, + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { // Pass chatCtx into both the TaskGroup AND every sub-task. The @@ -797,12 +777,12 @@ export class GetCreditCardTask extends AgentTask { const CARD_NUMBER_BASE_INSTRUCTIONS = ` You are a single step in a broader process of collecting credit card information. You are solely responsible for collecting the credit card number. -{modality_specific} +{modalitySpecific} If the user refuses to provide a credit card number, call decline_card_capture(). If the user wishes to start over the credit card collection process, call restart_card_collection(). Avoid listing out questions with bullet points or numbers, use a natural conversational tone. Never repeat any sensitive information, such as the user's credit card number, back to the user. -{confirmation_instructions} +{confirmationInstructions} `; const CARD_NUMBER_AUDIO_SPECIFIC = ` @@ -818,12 +798,12 @@ Handle input as typed text. Users may type the number with or without spaces or const SECURITY_CODE_BASE_INSTRUCTIONS = ` You are a single step in a broader process of collecting credit card information. You are solely responsible for collecting the user's card's security code. -{modality_specific} +{modalitySpecific} If the user refuses to provide a code, call decline_card_capture(). If the user wishes to start over the card collection process, call restart_card_collection(). Avoid listing out questions with bullet points or numbers, use a natural conversational tone. Never repeat any sensitive information, such as the user's security code, back to the user. -{confirmation_instructions} +{confirmationInstructions} `; const SECURITY_CODE_AUDIO_SPECIFIC = ` @@ -839,12 +819,12 @@ Handle input as typed text. Users will type the security code directly. const EXPIRATION_DATE_BASE_INSTRUCTIONS = ` You are a single step in a broader process of collecting credit card information. You are solely responsible for collecting the user's card's expiration date. -{modality_specific} +{modalitySpecific} If the user refuses to provide a date, call decline_card_capture(). If the user wishes to start over the card collection process, call restart_card_collection(). Avoid listing out questions with bullet points or numbers, use a natural conversational tone. Never repeat any sensitive information, such as the user's expiration date, back to the user. -{confirmation_instructions} +{confirmationInstructions} `; const EXPIRATION_DATE_AUDIO_SPECIFIC = ` diff --git a/agents/src/workflows/dob.ts b/agents/src/workflows/dob.ts index 8b4a4a2f4..bb7c5a95c 100644 --- a/agents/src/workflows/dob.ts +++ b/agents/src/workflows/dob.ts @@ -37,12 +37,12 @@ export interface GetDOBTaskOptions { /** Also capture the (optional) time of birth. Defaults to false. */ includeTime?: boolean; chatCtx?: ChatContext; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; /** * Whether to ask the user to confirm the captured date of birth. Defaults to confirming on @@ -106,7 +106,7 @@ export function createGetDOBTask({ includeTime = false, chatCtx, turnDetection, - tools, + tools = [], stt, vad, llm, @@ -236,7 +236,7 @@ export function createGetDOBTask({ if (!task.done) { task.complete(buildResult(currentDob)); } - return undefined; + return; } await injectConfirmTool(); @@ -274,7 +274,7 @@ export function createGetDOBTask({ if (!task.done) { task.complete(buildResult(currentDob)); } - return undefined; + return; } if (confirmationRequired(ctx)) { @@ -319,25 +319,25 @@ export function createGetDOBTask({ id: 'get_dob_task', instructions: new Instructions('', { audio: safeRender(BASE_INSTRUCTIONS, { - modality_specific: AUDIO_SPECIFIC, - time_instructions: timeInstructions, - confirmation_instructions: requireConfirmation !== false ? confirmationInstructions : '', - extra_instructions: extraInstructions, + modalitySpecific: AUDIO_SPECIFIC, + timeInstructions, + confirmationInstructions: requireConfirmation !== false ? confirmationInstructions : '', + extraInstructions, }), text: safeRender(BASE_INSTRUCTIONS, { - modality_specific: TEXT_SPECIFIC, - time_instructions: timeInstructions, - confirmation_instructions: requireConfirmation === true ? confirmationInstructions : '', - extra_instructions: extraInstructions, + modalitySpecific: TEXT_SPECIFIC, + timeInstructions, + confirmationInstructions: requireConfirmation === true ? confirmationInstructions : '', + extraInstructions, }), }), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [...(tools ?? []), updateDobTool, declineTool, ...(includeTime ? [updateTimeTool] : [])], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateDobTool, declineTool, ...(includeTime ? [updateTimeTool] : [])], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { const prompt = includeTime @@ -372,16 +372,16 @@ export class GetDOBTask extends AgentTask { const BASE_INSTRUCTIONS = ` You are only a single step in a broader system, responsible solely for capturing a date of birth. -{modality_specific} -{time_instructions}Call \`update_dob\` at the first opportunity whenever you form a new hypothesis about the date of birth. (before asking any questions or providing any answers.) +{modalitySpecific} +{timeInstructions}Call \`update_dob\` at the first opportunity whenever you form a new hypothesis about the date of birth. (before asking any questions or providing any answers.) Don't invent dates, stick strictly to what the user said. -{confirmation_instructions} +{confirmationInstructions} When reading back dates, use a natural spoken format like 'January fifteenth, nineteen ninety'. If the date is unclear or invalid, or it takes too much back-and-forth, prompt for it in parts: first the month, then the day, then the year. Ignore unrelated input and avoid going off-topic. Do not generate markdown, greetings, or unnecessary commentary. Avoid verbosity by not sharing example dates or formats unless prompted to do so. Do not deviate from the goal of collecting the user's birthday. 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} +{extraInstructions} `; const AUDIO_SPECIFIC = ` diff --git a/agents/src/workflows/email_address.ts b/agents/src/workflows/email_address.ts index 2f094cc0f..60488222b 100644 --- a/agents/src/workflows/email_address.ts +++ b/agents/src/workflows/email_address.ts @@ -25,12 +25,12 @@ export interface GetEmailTaskOptions { */ instructions?: InstructionParts | Instructions | string; chatCtx?: ChatContext; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; /** * Whether to ask the user to confirm the captured email address. Defaults to confirming on @@ -42,8 +42,6 @@ export interface GetEmailTaskOptions { * it can't silently fill one from the chat context during `onEnter`. */ requireExplicitAsk?: boolean; - /** @deprecated use `instructions.extra` instead */ - extraInstructions?: string; } /** @@ -55,7 +53,7 @@ export function createGetEmailTask({ instructions, chatCtx, turnDetection, - tools, + tools = [], stt, vad, llm, @@ -63,7 +61,6 @@ export function createGetEmailTask({ allowInterruptions, requireConfirmation, requireExplicitAsk = false, - extraInstructions = '', }: GetEmailTaskOptions = {}): AgentTask { let currentEmail = ''; @@ -116,7 +113,7 @@ export function createGetEmailTask({ if (!task.done) { task.complete({ emailAddress: currentEmail }); } - return undefined; // no need to continue the conversation + return; // no need to continue the conversation } const confirmTool = buildConfirmTool(email); @@ -151,28 +148,25 @@ export function createGetEmailTask({ id: 'get_email_task', instructions: resolveWorkflowInstructions({ instructions, - extraInstructions, template: INSTRUCTIONS_TEMPLATE, defaultPersona: PERSONA, - kwargs: { - _modality_specific: new Instructions('', { - audio: AUDIO_SPECIFIC, - text: TEXT_SPECIFIC, - }), - _confirmation: new Instructions('', { - // confirmation is enabled by default for audio, disabled by default for text - audio: requireConfirmation !== false ? CONFIRMATION_INSTRUCTION : '', - text: requireConfirmation === true ? CONFIRMATION_INSTRUCTION : '', - }), - }, + modalitySpecific: new Instructions('', { + audio: AUDIO_SPECIFIC, + text: TEXT_SPECIFIC, + }), + confirmation: new Instructions('', { + // confirmation is enabled by default for audio, disabled by default for text + audio: requireConfirmation !== false ? CONFIRMATION_INSTRUCTION : '', + text: requireConfirmation === true ? CONFIRMATION_INSTRUCTION : '', + }), }), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [...(tools ?? []), updateEmailTool, declineTool], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateEmailTool, declineTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { task.session.generateReply({ instructions: 'Ask the user to provide an email address.' }); @@ -230,11 +224,11 @@ const CONFIRMATION_INSTRUCTION = `Call \`confirm_email_address\` after the user const INSTRUCTIONS_TEMPLATE = `{persona} -{_modality_specific} +{modalitySpecific} Call \`update_email_address\` at the first opportunity whenever you form a new hypothesis about the email. (before asking any questions or providing any answers.) Don't invent new email addresses, stick strictly to what the user said. -{_confirmation} +{confirmation} If the email is unclear or invalid, or it takes too much back-and-forth, prompt for it in parts: first the part before the '@', then the domain—only if needed. Ignore unrelated input and avoid going off-topic. Do not generate markdown, greetings, or unnecessary commentary. diff --git a/agents/src/workflows/name.ts b/agents/src/workflows/name.ts index 79ed95df0..0edfe56a9 100644 --- a/agents/src/workflows/name.ts +++ b/agents/src/workflows/name.ts @@ -27,8 +27,8 @@ export interface GetNameTaskOptions { /** Collect the user's middle name. Defaults to false. */ middleName?: boolean; /** - * Order in which to collect the name parts, using `{first_name}`, `{middle_name}` and - * `{last_name}` placeholders. Defaults to the enabled parts in first/middle/last order. + * Order in which to collect the name parts, using `{firstName}`, `{middleName}` and + * `{lastName}` placeholders. Defaults to the enabled parts in first/middle/last order. */ nameFormat?: string; /** Ask the user to verify the spelling of the captured name. Defaults to false. */ @@ -36,12 +36,12 @@ export interface GetNameTaskOptions { /** Extra instructions appended to the built-in prompt for domain-specific context. */ extraInstructions?: string; chatCtx?: ChatContext; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; /** * Whether to ask the user to confirm the captured name. Defaults to confirming on audio @@ -56,9 +56,9 @@ export interface GetNameTaskOptions { } interface UpdateNameArgs { - first_name?: string | null; - middle_name?: string | null; - last_name?: string | null; + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; } function cleanNameArg(value: string | null | undefined): string | undefined { @@ -87,12 +87,12 @@ function renderNameFormat( parts: { firstName: string; middleName: string; lastName: string }, ): string { const replacements: Record = { - first_name: parts.firstName, - middle_name: parts.middleName, - last_name: parts.lastName, + firstName: parts.firstName, + middleName: parts.middleName, + lastName: parts.lastName, }; return nameFormat - .replace(/\{(first_name|middle_name|last_name)\}/g, (_match, key: string) => { + .replace(/\{(firstName|middleName|lastName)\}/g, (_match, key: string) => { return replacements[key] ?? ''; }) .trim(); @@ -112,7 +112,7 @@ export function createGetNameTask({ extraInstructions = '', chatCtx, turnDetection, - tools, + tools = [], stt, vad, llm, @@ -130,9 +130,9 @@ export function createGetNameTask({ resolvedNameFormat = nameFormat; } else { const parts: string[] = []; - if (firstName) parts.push('{first_name}'); - if (middleName) parts.push('{middle_name}'); - if (lastName) parts.push('{last_name}'); + if (firstName) parts.push('{firstName}'); + if (middleName) parts.push('{middleName}'); + if (lastName) parts.push('{lastName}'); resolvedNameFormat = parts.join(' '); } @@ -188,21 +188,21 @@ export function createGetNameTask({ name: 'update_name', description: 'Update the name provided by the user.', parameters: z.object({ - first_name: z.string().nullable().optional().describe("The user's first name."), - middle_name: z + firstName: z.string().nullable().optional().describe("The user's first name."), + middleName: z .string() .nullable() .optional() .describe("The user's middle name, if collected."), - last_name: z.string().nullable().optional().describe("The user's last name, if collected."), + lastName: z.string().nullable().optional().describe("The user's last name, if collected."), }), // With requireExplicitAsk, the model can't silent-fill from chatCtx during // onEnter — it must produce an asking utterance first. flags: requireExplicitAsk ? ToolFlag.IGNORE_ON_ENTER : ToolFlag.NONE, execute: async (args: UpdateNameArgs, { ctx }) => { - const cleanedFirst = cleanNameArg(args.first_name); - const cleanedMiddle = cleanNameArg(args.middle_name); - const cleanedLast = cleanNameArg(args.last_name); + const cleanedFirst = cleanNameArg(args.firstName); + const cleanedMiddle = cleanNameArg(args.middleName); + const cleanedLast = cleanNameArg(args.lastName); const errors: string[] = []; if (firstName && !cleanedFirst?.trim()) { @@ -249,7 +249,7 @@ export function createGetNameTask({ if (!task.done) { task.complete(buildResult()); } - return undefined; + return; } const confirmTool = buildConfirmTool({ @@ -296,27 +296,27 @@ export function createGetNameTask({ id: 'get_name_task', instructions: new Instructions('', { audio: safeRender(BASE_INSTRUCTIONS, { - name_format: resolvedNameFormat, - modality_specific: AUDIO_SPECIFIC, - spelling_instructions: spellingInstructions, - confirmation_instructions: requireConfirmation !== false ? confirmationInstructions : '', - extra_instructions: extraInstructions, + nameFormat: resolvedNameFormat, + modalitySpecific: AUDIO_SPECIFIC, + spellingInstructions, + confirmationInstructions: requireConfirmation !== false ? confirmationInstructions : '', + extraInstructions, }), text: safeRender(BASE_INSTRUCTIONS, { - name_format: resolvedNameFormat, - modality_specific: TEXT_SPECIFIC, - spelling_instructions: spellingInstructions, - confirmation_instructions: requireConfirmation === true ? confirmationInstructions : '', - extra_instructions: extraInstructions, + nameFormat: resolvedNameFormat, + modalitySpecific: TEXT_SPECIFIC, + spellingInstructions, + confirmationInstructions: requireConfirmation === true ? confirmationInstructions : '', + extraInstructions, }), }), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [...(tools ?? []), updateNameTool, declineTool], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updateNameTool, declineTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { task.session.generateReply({ @@ -360,16 +360,16 @@ export class GetNameTask extends AgentTask { const BASE_INSTRUCTIONS = ` You are only a single step in a broader system, responsible solely for capturing the user's name. -You need to naturally collect the name parts in this order: {name_format}. -{modality_specific} -{spelling_instructions}Call \`update_name\` at the first opportunity whenever you form a new hypothesis about the name. (before asking any questions or providing any answers.) +You need to naturally collect the name parts in this order: {nameFormat}. +{modalitySpecific} +{spellingInstructions}Call \`update_name\` at the first opportunity whenever you form a new hypothesis about the name. (before asking any questions or providing any answers.) Don't invent names, stick strictly to what the user said. -{confirmation_instructions} +{confirmationInstructions} If the name is unclear or it takes too much back-and-forth, prompt for each name part separately. Ignore unrelated input and avoid going off-topic. Do not generate markdown, greetings, or unnecessary commentary. Avoid verbosity by not sharing example names or spellings unless prompted to do so. Do not deviate from the goal of collecting the user's name. 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} +{extraInstructions} `; const AUDIO_SPECIFIC = ` diff --git a/agents/src/workflows/phone_number.ts b/agents/src/workflows/phone_number.ts index 3ae7ccb17..306f3d87d 100644 --- a/agents/src/workflows/phone_number.ts +++ b/agents/src/workflows/phone_number.ts @@ -23,12 +23,12 @@ export interface GetPhoneNumberTaskOptions { /** Extra instructions appended to the built-in prompt for domain-specific context. */ extraInstructions?: string; chatCtx?: ChatContext; - turnDetection?: TurnDetectionMode | null; + turnDetection?: TurnDetectionMode; tools?: readonly ToolContextEntry[]; - stt?: STT | STTModelString | null; - vad?: VAD | null; - llm?: LLM | RealtimeModel | LLMModels | null; - tts?: TTS | TTSModelString | null; + stt?: STT | STTModelString; + vad?: VAD; + llm?: LLM | RealtimeModel | LLMModels; + tts?: TTS | TTSModelString; allowInterruptions?: boolean; /** * Whether to ask the user to confirm the captured phone number. Defaults to confirming on @@ -51,7 +51,7 @@ export function createGetPhoneNumberTask({ extraInstructions = '', chatCtx, turnDetection, - tools, + tools = [], stt, vad, llm, @@ -95,14 +95,14 @@ export function createGetPhoneNumberTask({ name: 'update_phone_number', description: 'Update the phone number provided by the user.', parameters: z.object({ - phone_number: z + phoneNumber: z .string() .describe('The phone number provided by the user, digits only with optional leading +'), }), // With requireExplicitAsk, the model can't silent-fill from chatCtx during // onEnter — it must produce an asking utterance first. flags: requireExplicitAsk ? ToolFlag.IGNORE_ON_ENTER : ToolFlag.NONE, - execute: async ({ phone_number: phoneNumber }: { phone_number: string }, { ctx }) => { + execute: async ({ phoneNumber }: { phoneNumber: string }, { ctx }) => { const cleaned = phoneNumber.trim().replace(/[\s\-().]+/g, ''); if (!PHONE_REGEX.test(cleaned)) { @@ -115,7 +115,7 @@ export function createGetPhoneNumberTask({ if (!task.done) { task.complete({ phoneNumber: currentPhoneNumber }); } - return undefined; // no need to continue the conversation + return; // no need to continue the conversation } const confirmTool = buildConfirmTool(cleaned); @@ -150,23 +150,23 @@ export function createGetPhoneNumberTask({ id: 'get_phone_number_task', instructions: new Instructions('', { audio: safeRender(BASE_INSTRUCTIONS, { - modality_specific: AUDIO_SPECIFIC, - confirmation_instructions: requireConfirmation !== false ? confirmationInstructions : '', - extra_instructions: extraInstructions, + modalitySpecific: AUDIO_SPECIFIC, + confirmationInstructions: requireConfirmation !== false ? confirmationInstructions : '', + extraInstructions, }), text: safeRender(BASE_INSTRUCTIONS, { - modality_specific: TEXT_SPECIFIC, - confirmation_instructions: requireConfirmation === true ? confirmationInstructions : '', - extra_instructions: extraInstructions, + modalitySpecific: TEXT_SPECIFIC, + confirmationInstructions: requireConfirmation === true ? confirmationInstructions : '', + extraInstructions, }), }), chatCtx, - turnDetection: turnDetection ?? undefined, - tools: [...(tools ?? []), updatePhoneNumberTool, declineTool], - stt: stt ?? undefined, - vad: vad ?? undefined, - llm: llm ?? undefined, - tts: tts ?? undefined, + turnDetection, + tools: [...tools, updatePhoneNumberTool, declineTool], + stt, + vad, + llm, + tts, allowInterruptions, onEnter: async () => { task.session.generateReply({ instructions: 'Ask the user to provide their phone number.' }); @@ -198,16 +198,16 @@ export class GetPhoneNumberTask extends AgentTask { const BASE_INSTRUCTIONS = ` You are only a single step in a broader system, responsible solely for capturing a phone number. -{modality_specific} +{modalitySpecific} 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} +{confirmationInstructions} 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} +{extraInstructions} `; const AUDIO_SPECIFIC = ` diff --git a/agents/src/workflows/utils.ts b/agents/src/workflows/utils.ts index b0189939f..2d435d00a 100644 --- a/agents/src/workflows/utils.ts +++ b/agents/src/workflows/utils.ts @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: Apache-2.0 import { Instructions, isInstructions } from '../llm/chat_context.js'; -import { log } from '../log.js'; /** * Customizable instruction sections for built-in workflow tasks. @@ -27,17 +26,14 @@ export interface InstructionParts { */ export function resolveWorkflowInstructions(options: { instructions?: InstructionParts | Instructions | string; - /** @deprecated legacy `extraInstructions` option, ignored when `instructions` is provided. */ - extraInstructions?: string; template: string; defaultPersona: string; - kwargs?: Record; + /** Fills the template's `{modalitySpecific}` slot with per-modality (audio/text) guidance. */ + modalitySpecific: Instructions; + /** Fills the template's `{confirmation}` slot with the per-modality confirmation instruction. */ + confirmation: Instructions; }): string | Instructions { - const { instructions, extraInstructions = '', template, defaultPersona, kwargs = {} } = options; - - if (instructions !== undefined && extraInstructions) { - log().warn('`extraInstructions` will be ignored when `instructions` is provided'); - } + const { instructions, template, defaultPersona, modalitySpecific, confirmation } = options; // A full instruction string or Instructions replaces the built-in prompt entirely. if (typeof instructions === 'string' || isInstructions(instructions)) { @@ -45,12 +41,13 @@ export function resolveWorkflowInstructions(options: { } // No instructions or an `InstructionParts` override: fill the built-in template. - const parts: InstructionParts = instructions ?? { extra: extraInstructions }; + // Unset preserves the built-in default; an explicit empty string removes the section. + const parts: InstructionParts = instructions ?? {}; return Instructions.resolveTemplate(template, { - // Unset preserves the built-in default; an explicit empty string removes the section. - persona: parts.persona !== undefined ? parts.persona : defaultPersona, - extra: parts.extra !== undefined ? parts.extra : '', - ...kwargs, + persona: parts.persona ?? defaultPersona, + extra: parts.extra ?? '', + modalitySpecific, + confirmation, }); }