| 1 | import type * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import type { BotAlias } from "./BotAlias.ts"; |
| 4 | |
| 5 | /** |
| 6 | * The intent inside a code hook event's `sessionState` — the intent Lex is |
| 7 | * currently eliciting or fulfilling. |
| 8 | */ |
| 9 | export interface CodeHookIntent { |
| 10 | /** Name of the intent. */ |
| 11 | name: string; |
| 12 | /** Slot values gathered so far (`slotName` → value or `null`). */ |
| 13 | slots?: Record<string, unknown>; |
| 14 | /** `InProgress` | `ReadyForFulfillment` | `Fulfilled` | `Failed` | ... */ |
| 15 | state?: string; |
| 16 | /** How the intent was matched (`Confirmed`/`Denied`/`None`). */ |
| 17 | confirmationState?: string; |
| 18 | } |
| 19 | |
| 20 | /** |
| 21 | * The `sessionState` envelope shared by code hook events and responses — |
| 22 | * the conversation's active intent, dialog action, and session attributes. |
| 23 | */ |
| 24 | export interface CodeHookSessionState { |
| 25 | /** The active intent and its slots. */ |
| 26 | intent?: CodeHookIntent; |
| 27 | /** What Lex should do next (`Close`, `ElicitSlot`, `Delegate`, ...). */ |
| 28 | dialogAction?: { |
| 29 | type: string; |
| 30 | slotToElicit?: string; |
| 31 | }; |
| 32 | /** Application-specific session attributes. */ |
| 33 | sessionAttributes?: Record<string, string>; |
| 34 | /** Contexts active for the current turn. */ |
| 35 | activeContexts?: unknown[]; |
| 36 | /** The locale and other original request metadata. */ |
| 37 | [key: string]: unknown; |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * The event Amazon Lex V2 delivers to a dialog or fulfillment Lambda code |
| 42 | * hook (the `messageVersion: "1.0"` envelope). |
| 43 | */ |
| 44 | export interface CodeHookEvent { |
| 45 | /** Always `"1.0"`. */ |
| 46 | messageVersion: string; |
| 47 | /** Which hook fired: `DialogCodeHook` or `FulfillmentCodeHook`. */ |
| 48 | invocationSource: "DialogCodeHook" | "FulfillmentCodeHook"; |
| 49 | /** How the user provided input (`Text`, `Speech`, `DTMF`). */ |
| 50 | inputMode: string; |
| 51 | /** The bot, alias, and locale the conversation runs against. */ |
| 52 | bot: { |
| 53 | id: string; |
| 54 | name: string; |
| 55 | aliasId: string; |
| 56 | aliasName?: string; |
| 57 | localeId: string; |
| 58 | version: string; |
| 59 | }; |
| 60 | /** Identifier of the conversation session. */ |
| 61 | sessionId: string; |
| 62 | /** The user's raw input for this turn. */ |
| 63 | inputTranscript?: string; |
| 64 | /** The interpretations Lex considered for this turn. */ |
| 65 | interpretations?: unknown[]; |
| 66 | /** Request attributes sent by the client. */ |
| 67 | requestAttributes?: Record<string, string>; |
| 68 | /** The conversation state at the time of invocation. */ |
| 69 | sessionState: CodeHookSessionState; |
| 70 | /** Additional event fields (the schema grows over time). */ |
| 71 | [key: string]: unknown; |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * The response a code hook returns to Amazon Lex V2 — the (usually mutated) |
| 76 | * `sessionState` plus optional messages to relay to the user. |
| 77 | */ |
| 78 | export interface CodeHookResponse { |
| 79 | /** The new conversation state (dialog action, intent state, slots). */ |
| 80 | sessionState: CodeHookSessionState; |
| 81 | /** Messages to relay to the user. */ |
| 82 | messages?: { |
| 83 | contentType: "PlainText" | "SSML" | "CustomPayload" | "ImageResponseCard"; |
| 84 | content?: string; |
| 85 | }[]; |
| 86 | /** Request attributes to echo back. */ |
| 87 | requestAttributes?: Record<string, string>; |
| 88 | } |
| 89 | |
| 90 | export interface CodeHookProps { |
| 91 | /** |
| 92 | * The alias locale whose code hook this function handles, e.g. `en_US`. |
| 93 | * The alias's `botAliasLocaleSettings` for this locale are pointed at the |
| 94 | * hosting Lambda. |
| 95 | */ |
| 96 | localeId: string; |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * A code hook handler receives the typed Lex event and returns the full |
| 101 | * response Lex expects (`sessionState` + optional `messages`). Use |
| 102 | * {@link fulfillIntent} to build a valid close response. |
| 103 | */ |
| 104 | export type CodeHookHandler<Req = never> = ( |
| 105 | event: CodeHookEvent, |
| 106 | ) => Effect.Effect<CodeHookResponse, never, Req>; |
| 107 | |
| 108 | export type CodeHookEventSourceService = <Req = never>( |
| 109 | alias: BotAlias, |
| 110 | props: CodeHookProps, |
| 111 | handler: CodeHookHandler<Req>, |
| 112 | ) => Effect.Effect<void, never, never>; |
| 113 | |
| 114 | /** |
| 115 | * Event source connecting an Amazon Lex V2 bot alias's Lambda code hook |
| 116 | * (dialog and fulfillment) to the hosting Lambda function. |
| 117 | * |
| 118 | * At deploy time the implementation (`LexV2.LambdaCodeHookEventSource`) |
| 119 | * injects the function ARN into the alias's `botAliasLocaleSettings` |
| 120 | * (through the alias's binding contract) and creates the |
| 121 | * `lambda:InvokeFunction` Permission for `lexv2.amazonaws.com`; at runtime |
| 122 | * it dispatches matching code hook events to the handler and returns the |
| 123 | * handler's response to Lex. |
| 124 | * |
| 125 | * Enable the hook per intent with the `dialogCodeHook` / |
| 126 | * `fulfillmentCodeHook` props on `LexV2.Intent`. |
| 127 | * |
| 128 | * Use the {@link onCodeHook} helper rather than the service directly, and |
| 129 | * provide `LexV2.LambdaCodeHookEventSource` on the hosting function. |
| 130 | * ### Handling Code Hooks |
| 131 | * **Example:** Fulfill an Intent from a Lambda Function |
| 132 | * ```typescript |
| 133 | * export default BotFunction.make( |
| 134 | * { main: import.meta.url }, |
| 135 | * Effect.gen(function* () { |
| 136 | * const alias = yield* AWS.LexV2.BotAlias("Live", { |
| 137 | * botId: version.botId, |
| 138 | * botVersion: version.botVersion, |
| 139 | * }); |
| 140 | * |
| 141 | * // deploy: wires the alias's en_US code hook + invoke Permission |
| 142 | * // runtime: dispatches code hook events to this handler |
| 143 | * yield* AWS.LexV2.onCodeHook(alias, { localeId: "en_US" }, (event) => |
| 144 | * Effect.succeed( |
| 145 | * AWS.LexV2.fulfillIntent(event, { message: "Order placed!" }), |
| 146 | * ), |
| 147 | * ); |
| 148 | * |
| 149 | * return {}; |
| 150 | * }).pipe(Effect.provide(AWS.LexV2.LambdaCodeHookEventSource)), |
| 151 | * ); |
| 152 | * ``` |
| 153 | * |
| 154 | * @binding |
| 155 | */ |
| 156 | export interface CodeHookEventSource extends Binding.Service< |
| 157 | CodeHookEventSource, |
| 158 | "AWS.LexV2.CodeHookEventSource", |
| 159 | CodeHookEventSourceService |
| 160 | > {} |
| 161 | |
| 162 | export const CodeHookEventSource = Binding.Service<CodeHookEventSource>( |
| 163 | "AWS.LexV2.CodeHookEventSource", |
| 164 | ); |
| 165 | |
| 166 | /** |
| 167 | * Handle an Amazon Lex V2 code hook (dialog + fulfillment) for a bot alias |
| 168 | * locale with the current Lambda function. |
| 169 | * |
| 170 | * Provide `LexV2.LambdaCodeHookEventSource` on the hosting function to |
| 171 | * satisfy the requirement. |
| 172 | * |
| 173 | * @param alias The bot alias whose code hook to handle. |
| 174 | * @param props The alias locale to attach to. |
| 175 | * @param handler Invoked once per code hook event; the returned value is |
| 176 | * the response Lex receives. |
| 177 | * |
| 178 | * @example Close every fulfilled intent with a message |
| 179 | * ```typescript |
| 180 | * yield* LexV2.onCodeHook(alias, { localeId: "en_US" }, (event) => |
| 181 | * Effect.succeed(LexV2.fulfillIntent(event, { message: "Done!" })), |
| 182 | * ); |
| 183 | * ``` |
| 184 | */ |
| 185 | export function onCodeHook<Req = never>( |
| 186 | alias: BotAlias, |
| 187 | props: CodeHookProps, |
| 188 | handler: CodeHookHandler<Req>, |
| 189 | ): Effect.Effect<void, never, CodeHookEventSource> { |
| 190 | return CodeHookEventSource.use((source) => source(alias, props, handler)); |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * Build a valid code hook response that closes the event's active intent as |
| 195 | * `Fulfilled` (or `Failed`), optionally relaying a plain-text message — |
| 196 | * the common shape for a fulfillment code hook. |
| 197 | * |
| 198 | * @example Fulfillment hook that confirms the order |
| 199 | * ```typescript |
| 200 | * yield* LexV2.onCodeHook(alias, { localeId: "en_US" }, (event) => |
| 201 | * Effect.succeed( |
| 202 | * LexV2.fulfillIntent(event, { message: "Your order is placed." }), |
| 203 | * ), |
| 204 | * ); |
| 205 | * ``` |
| 206 | */ |
| 207 | export const fulfillIntent = ( |
| 208 | event: CodeHookEvent, |
| 209 | options?: { |
| 210 | /** Plain-text message to relay to the user. */ |
| 211 | message?: string; |
| 212 | /** The intent's final state. @default "Fulfilled" */ |
| 213 | state?: "Fulfilled" | "Failed"; |
| 214 | }, |
| 215 | ): CodeHookResponse => ({ |
| 216 | sessionState: { |
| 217 | ...event.sessionState, |
| 218 | dialogAction: { type: "Close" }, |
| 219 | intent: { |
| 220 | ...event.sessionState.intent!, |
| 221 | state: options?.state ?? "Fulfilled", |
| 222 | }, |
| 223 | }, |
| 224 | ...(options?.message !== undefined |
| 225 | ? { |
| 226 | messages: [ |
| 227 | { contentType: "PlainText" as const, content: options.message }, |
| 228 | ], |
| 229 | } |
| 230 | : {}), |
| 231 | }); |