1import * as lexm from "@distilled.cloud/aws/lex-models-v2";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7import { retryWhileConflict, waitForLocaleSettled } from "./internal.ts";
8
9export interface BotLocaleProps {
10 /**
11 * ID of the bot the locale belongs to. Changing it replaces the locale.
12 */
13 botId: string;
14 /**
15 * The language/locale the bot converses in, e.g. `en_US`. Changing it
16 * replaces the locale.
17 */
18 localeId: string;
19 /**
20 * Confidence threshold (0 to 1) below which Amazon Lex inserts
21 * `AMAZON.FallbackIntent` into the possible-intents list.
22 * @default 0.4
23 */
24 nluIntentConfidenceThreshold?: number;
25 /**
26 * Description of the locale.
27 */
28 description?: string;
29 /**
30 * Amazon Polly voice used for spoken interaction with the user.
31 */
32 voiceSettings?: {
33 /** Amazon Polly voice ID, e.g. `Ivy`. */
34 voiceId: string;
35 /**
36 * Polly engine to use.
37 * @default "standard"
38 */
39 engine?: "standard" | "neural" | "long-form" | "generative";
40 };
41}
42
43export interface BotLocale extends Resource<
44 "AWS.LexV2.BotLocale",
45 BotLocaleProps,
46 {
47 /** ID of the bot the locale belongs to. */
48 botId: string;
49 /** Bot version the locale lives on — always `DRAFT`. */
50 botVersion: string;
51 /** The locale ID, e.g. `en_US`. */
52 localeId: string;
53 /** Human-readable locale name, e.g. `English (US)`. */
54 localeName: string | undefined;
55 /** Current status of the locale (e.g. `NotBuilt`, `Built`). */
56 botLocaleStatus: string;
57 },
58 never,
59 Providers
60> {}
61
62/**
63 * A language/locale on the DRAFT version of an Amazon Lex V2 bot. Intents and
64 * slot types live under a locale; a locale must exist before either can be
65 * created.
66 *
67 * ### Creating a Locale
68 * **Example:** US English Locale
69 * ```typescript
70 * import * as AWS from "alchemy/AWS";
71 *
72 * const locale = yield* AWS.LexV2.BotLocale("En", {
73 * botId: bot.botId,
74 * localeId: "en_US",
75 * });
76 * ```
77 *
78 * **Example:** Locale with Voice and Threshold
79 * ```typescript
80 * const locale = yield* AWS.LexV2.BotLocale("En", {
81 * botId: bot.botId,
82 * localeId: "en_US",
83 * nluIntentConfidenceThreshold: 0.7,
84 * voiceSettings: { voiceId: "Ivy", engine: "neural" },
85 * });
86 * ```
87 *
88 * @resource
89 */
90export const BotLocale = Resource<BotLocale>("AWS.LexV2.BotLocale");
91
92const describeLocale = Effect.fn(function* (botId: string, localeId: string) {
93 return yield* lexm
94 .describeBotLocale({ botId, botVersion: "DRAFT", localeId })
95 .pipe(
96 Effect.catchTag("ResourceNotFoundException", () =>
97 Effect.succeed(undefined),
98 ),
99 );
100});
101
102const attributesOf = (
103 locale: lexm.DescribeBotLocaleResponse,
104): BotLocale["Attributes"] => ({
105 botId: locale.botId!,
106 botVersion: "DRAFT",
107 localeId: locale.localeId!,
108 localeName: locale.localeName,
109 botLocaleStatus: locale.botLocaleStatus!,
110});
111
112export const BotLocaleProvider = () =>
113 Provider.effect(
114 BotLocale,
115 Effect.gen(function* () {
116 return {
117 stables: ["botId", "botVersion", "localeId"],
118
119 // Sub-resource keyed entirely by its bot — nuke reaches it through
120 // the parent bot's deletion.
121 list: () => Effect.succeed([]),
122
123 read: Effect.fn(function* ({ olds, output }) {
124 const botId = output?.botId ?? olds?.botId;
125 const localeId = output?.localeId ?? olds?.localeId;
126 if (botId === undefined || localeId === undefined) return undefined;
127 const observed = yield* describeLocale(botId, localeId);
128 return observed === undefined ? undefined : attributesOf(observed);
129 }),
130
131 diff: Effect.fn(function* ({ news, olds }) {
132 if (!isResolved(news)) return undefined;
133 if (olds?.botId !== news.botId || olds?.localeId !== news.localeId) {
134 return { action: "replace" } as const;
135 }
136 }),
137
138 reconcile: Effect.fn(function* ({ news, session }) {
139 const desiredThreshold = news.nluIntentConfidenceThreshold ?? 0.4;
140
141 // 1. OBSERVE
142 let observed = yield* describeLocale(news.botId, news.localeId);
143
144 // 2. ENSURE — a Conflict is a concurrent create of the same locale.
145 if (observed === undefined) {
146 yield* lexm
147 .createBotLocale({
148 botId: news.botId,
149 botVersion: "DRAFT",
150 localeId: news.localeId,
151 nluIntentConfidenceThreshold: desiredThreshold,
152 description: news.description,
153 voiceSettings: news.voiceSettings,
154 })
155 .pipe(Effect.catchTag("ConflictException", () => Effect.void));
156 observed = yield* waitForLocaleSettled(news.botId, news.localeId);
157 }
158
159 // 3. SYNC — apply the delta when a declared prop drifted.
160 if (
161 observed.nluIntentConfidenceThreshold !== desiredThreshold ||
162 (observed.description ?? undefined) !==
163 (news.description ?? undefined) ||
164 (observed.voiceSettings?.voiceId ?? undefined) !==
165 (news.voiceSettings?.voiceId ?? undefined)
166 ) {
167 yield* retryWhileConflict(
168 lexm.updateBotLocale({
169 botId: news.botId,
170 botVersion: "DRAFT",
171 localeId: news.localeId,
172 nluIntentConfidenceThreshold: desiredThreshold,
173 description: news.description,
174 voiceSettings: news.voiceSettings,
175 }),
176 );
177 observed = yield* waitForLocaleSettled(news.botId, news.localeId);
178 }
179
180 yield* session.note(`${news.botId}/${news.localeId}`);
181 return attributesOf(observed);
182 }),
183
184 delete: Effect.fn(function* ({ output }) {
185 // Lex reports a missing locale (or an already-deleted parent bot)
186 // as PreconditionFailed.
187 yield* retryWhileConflict(
188 lexm.deleteBotLocale({
189 botId: output.botId,
190 botVersion: "DRAFT",
191 localeId: output.localeId,
192 }),
193 ).pipe(
194 Effect.catchTag("PreconditionFailedException", () => Effect.void),
195 );
196 }),
197 };
198 }),
199 );