1import * as dialogflow from "@distilled.cloud/gcp/dialogflow_v3";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { isResolved } from "../../Diff.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 collectionParent,
13 encodeOwnership,
14 encodeOwnershipLine,
15 fingerprint,
16 hasOwnershipMarker,
17 internalLabels,
18 lastSegment,
19 locationOf,
20 namedAgents,
21 ownedByAlchemy,
22 parentOf,
23 parseOwnership,
24 projectOf,
25 sameText,
26 toResourceId,
27 retryQuota,
28} from "./internal.ts";
29
30export type ExampleConversationState =
31 | "OUTPUT_STATE_UNSPECIFIED"
32 | "OUTPUT_STATE_OK"
33 | "OUTPUT_STATE_CANCELLED"
34 | "OUTPUT_STATE_FAILED"
35 | "OUTPUT_STATE_ESCALATED"
36 | "OUTPUT_STATE_PENDING";
37
38export type ExampleAction = {
39 /** User utterance. */
40 userUtterance?: { text?: string };
41 /** Agent utterance. */
42 agentUtterance?: { text?: string };
43 /** Tool use. */
44 toolUse?: {
45 tool?: string;
46 action?: string;
47 displayName?: string;
48 };
49 /** Playbook invocation. */
50 playbookInvocation?: {
51 playbook?: string;
52 displayName?: string;
53 };
54 /** Flow invocation. */
55 flowInvocation?: {
56 flow?: string;
57 displayName?: string;
58 };
59};
60
61export type AgentsPlaybooksExampleProps = {
62 /**
63 * Parent playbook resource name
64 * `projects/{project}/locations/{location}/agents/{agent}/playbooks/{playbook}`.
65 * Immutable — changing it replaces the example.
66 */
67 playbook: string;
68 /**
69 * Example id (the `{example}` segment). Server-assigned when omitted.
70 * Immutable — changing it replaces the example.
71 */
72 exampleId?: string;
73 /**
74 * Human-readable name. Examples have no labels field, so Alchemy
75 * stamps ownership into `displayName` and `description`.
76 */
77 displayName?: string;
78 /**
79 * End state of the example conversation.
80 * @default "OUTPUT_STATE_OK"
81 */
82 conversationState?: ExampleConversationState | (string & {});
83 /**
84 * Ordered conversation actions. Defaults to a single user utterance
85 * followed by an agent utterance.
86 */
87 actions?: ExampleAction[];
88 /** Human-readable description. */
89 description?: string;
90 /** Language code (`en`, `en-US`, …). */
91 languageCode?: string;
92 /** Preceding conversation summary used as playbook input. */
93 playbookInput?: { precedingConversationSummary?: string };
94 /** Playbook output summary. */
95 playbookOutput?: { executionSummary?: string };
96};
97
98export type AgentsPlaybooksExample = Resource<
99 "GCP.Dialogflow.AgentsPlaybooksExample",
100 AgentsPlaybooksExampleProps,
101 {
102 /** Full resource name `.../playbooks/{playbook}/examples/{example}`. */
103 name: string;
104 /** Example id (last path segment). */
105 exampleId: string;
106 /** Parent playbook resource name. */
107 playbook: string;
108 /** Location id. */
109 location: string;
110 /** Project id. */
111 project: string;
112 /** User display name with the Alchemy ownership prefix stripped. */
113 displayName: string | undefined;
114 /** Conversation end state. */
115 conversationState: string | undefined;
116 /** Conversation actions. */
117 actions: ExampleAction[] | undefined;
118 /** User description with the Alchemy ownership prefix stripped. */
119 description: string | undefined;
120 /** Language code. */
121 languageCode: string | undefined;
122 /** Token count. */
123 tokenCount: string | undefined;
124 /** RFC3339 create timestamp. */
125 createTime: string | undefined;
126 /** RFC3339 update timestamp. */
127 updateTime: string | undefined;
128 },
129 never,
130 Providers
131>;
132
133/**
134 * A Dialogflow CX playbook example (few-shot conversation).
135 *
136 * Examples have no labels field — Alchemy stamps ownership into
137 * `displayName` and `description` so `list` / nuke can find them. Parent
138 * playbook and id are immutable. Display name, conversation state,
139 * actions, and description update in place.
140 *
141 * ### Creating an Example
142 * **Example:** Greeting conversation
143 * ```typescript
144 * const example = yield* GCP.Dialogflow.AgentsPlaybooksExample("Hello", {
145 * playbook: playbook.name,
146 * displayName: "hello",
147 * conversationState: "OUTPUT_STATE_OK",
148 * actions: [
149 * { userUtterance: { text: "hello" } },
150 * { agentUtterance: { text: "Hi, how can I help?" } },
151 * ],
152 * });
153 * ```
154 *
155 * @resource
156 * @category Dialogflow
157 */
158export const AgentsPlaybooksExample = Resource<AgentsPlaybooksExample>(
159 "GCP.Dialogflow.AgentsPlaybooksExample",
160);
161
162export class AgentsPlaybooksExampleNotResolved extends Data.TaggedError(
163 "GCP.Dialogflow.AgentsPlaybooksExampleNotResolved",
164)<{
165 name: string;
166}> {}
167
168const DEFAULT_STATE: ExampleConversationState = "OUTPUT_STATE_OK";
169const DEFAULT_ACTIONS: ExampleAction[] = [
170 { userUtterance: { text: "hello" } },
171 { agentUtterance: { text: "Hi, how can I help?" } },
172];
173
174const resourceName = (playbook: string, exampleId: string) =>
175 `${playbook}/examples/${exampleId}`;
176
177const actionsOf = (
178 actions: dialogflow.GoogleCloudDialogflowCxV3ActionList | undefined,
179): ExampleAction[] | undefined => {
180 if (actions === undefined) return undefined;
181 return actions.map((action) => ({
182 userUtterance: action.userUtterance
183 ? { text: action.userUtterance.text }
184 : undefined,
185 agentUtterance: action.agentUtterance
186 ? { text: action.agentUtterance.text }
187 : undefined,
188 toolUse: action.toolUse
189 ? {
190 tool: action.toolUse.tool,
191 action: action.toolUse.action,
192 displayName: action.toolUse.displayName,
193 }
194 : undefined,
195 playbookInvocation: action.playbookInvocation
196 ? {
197 playbook: action.playbookInvocation.playbook,
198 displayName: action.playbookInvocation.displayName,
199 }
200 : undefined,
201 flowInvocation: action.flowInvocation
202 ? {
203 flow: action.flowInvocation.flow,
204 displayName: action.flowInvocation.displayName,
205 }
206 : undefined,
207 }));
208};
209
210const toAttrs = (
211 example: dialogflow.GoogleCloudDialogflowCxV3Example,
212 project: string,
213 playbookHint?: string,
214) => {
215 const name = example.name ?? "";
216 return {
217 name,
218 exampleId: lastSegment(name),
219 playbook: name.includes("/examples/")
220 ? collectionParent(name, "playbooks")
221 : (playbookHint ?? parentOf(name)),
222 location: locationOf(name),
223 project: projectOf(name) || project,
224 displayName: parseOwnership(example.displayName).text,
225 conversationState: example.conversationState,
226 actions: actionsOf(example.actions),
227 description: parseOwnership(example.description).text,
228 languageCode: example.languageCode,
229 tokenCount: example.tokenCount,
230 createTime: example.createTime,
231 updateTime: example.updateTime,
232 };
233};
234
235const getByName = (name: string) =>
236 name.length === 0
237 ? Effect.succeed(undefined)
238 : dialogflow
239 .getProjectsLocationsAgentsPlaybooksExamples({ name })
240 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
241
242const listAt = (parent: string, project: string) =>
243 dialogflow.listProjectsLocationsAgentsPlaybooksExamples
244 .pages({ parent, pageSize: 100 })
245 .pipe(
246 Stream.flatMap((page) => Stream.fromIterable(page.examples ?? [])),
247 Stream.filter(
248 (example) =>
249 hasOwnershipMarker(example.displayName) ||
250 hasOwnershipMarker(example.description),
251 ),
252 Stream.map((example) => toAttrs(example, project, parent)),
253 Stream.runCollect,
254 Effect.map((chunk) => Array.from(chunk)),
255 Effect.catchTag("NotFound", () => Effect.succeed([])),
256 );
257
258const listPlaybooks = (agent: string) =>
259 dialogflow.listProjectsLocationsAgentsPlaybooks
260 .pages({ parent: agent, pageSize: 100 })
261 .pipe(
262 Stream.flatMap((page) => Stream.fromIterable(page.playbooks ?? [])),
263 Stream.runCollect,
264 Effect.map((chunk) => Array.from(chunk)),
265 Effect.catchTag("NotFound", () => Effect.succeed([])),
266 );
267
268const findByDisplayName = (parent: string, displayName: string) =>
269 dialogflow.listProjectsLocationsAgentsPlaybooksExamples
270 .pages({ parent, pageSize: 100 })
271 .pipe(
272 Stream.flatMap((page) => Stream.fromIterable(page.examples ?? [])),
273 Stream.filter((example) => example.displayName === displayName),
274 Stream.runHead,
275 Effect.map((option) =>
276 option._tag === "Some" ? option.value : undefined,
277 ),
278 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
279 );
280
281export const AgentsPlaybooksExampleProvider = () =>
282 Provider.succeed(AgentsPlaybooksExample, {
283 stables: [
284 "name",
285 "exampleId",
286 "playbook",
287 "location",
288 "project",
289 "createTime",
290 ],
291
292 diff: Effect.fn(function* ({ news, olds, output }) {
293 if (!isResolved(news)) return undefined;
294 const previousParent = olds?.playbook ?? output?.playbook;
295 if (previousParent !== undefined && news.playbook !== previousParent) {
296 return { action: "replace" as const, deleteFirst: false };
297 }
298 const previousId = olds?.exampleId ?? output?.exampleId;
299 if (
300 previousId !== undefined &&
301 news.exampleId !== undefined &&
302 news.exampleId !== previousId
303 ) {
304 return { action: "replace" as const, deleteFirst: false };
305 }
306 return undefined;
307 }),
308
309 read: Effect.fn(function* ({ id, olds, output }) {
310 const env = yield* GcpEnvironment.current;
311 const playbook = olds?.playbook ?? output?.playbook;
312 const exampleId = yield* toResourceId(
313 id,
314 olds?.exampleId,
315 output?.exampleId,
316 );
317 const name =
318 output?.name ??
319 (playbook !== undefined ? resourceName(playbook, exampleId) : "");
320 let existing = yield* getByName(name);
321 if (existing === undefined && playbook !== undefined) {
322 const ownership = yield* internalLabels(id);
323 existing = yield* findByDisplayName(
324 playbook,
325 encodeOwnershipLine(ownership, olds?.displayName),
326 );
327 }
328 if (existing === undefined) return undefined;
329 const attrs = toAttrs(existing, env.project, playbook);
330 const named = yield* ownedByAlchemy(id, existing.displayName);
331 const described = yield* ownedByAlchemy(id, existing.description);
332 return named || described ? attrs : Unowned(attrs);
333 }),
334
335 list: () =>
336 Effect.gen(function* () {
337 const env = yield* GcpEnvironment.current;
338 const agents = yield* namedAgents(env.project);
339 const pages = yield* Effect.forEach(
340 agents,
341 (agent) =>
342 Effect.gen(function* () {
343 const playbooks = yield* listPlaybooks(agent.name);
344 const examples = yield* Effect.forEach(
345 playbooks,
346 (playbook) =>
347 playbook.name
348 ? listAt(playbook.name, env.project)
349 : Effect.succeed([]),
350 { concurrency: 4 },
351 );
352 return examples.flat();
353 }),
354 { concurrency: 2 },
355 );
356 return pages.flat();
357 }),
358
359 reconcile: Effect.fn(function* ({ id, news, output }) {
360 const env = yield* GcpEnvironment.current;
361 const playbook = news.playbook;
362 const exampleId = yield* toResourceId(
363 id,
364 news.exampleId,
365 output?.exampleId,
366 );
367 const name = output?.name ?? resourceName(playbook, exampleId);
368 const ownership = yield* internalLabels(id);
369 const displayName = encodeOwnershipLine(ownership, news.displayName);
370 const description = encodeOwnership(ownership, news.description);
371 const conversationState = news.conversationState ?? DEFAULT_STATE;
372 const actions = news.actions ?? DEFAULT_ACTIONS;
373 const body: dialogflow.GoogleCloudDialogflowCxV3Example = {
374 displayName,
375 description,
376 conversationState,
377 actions,
378 languageCode: news.languageCode,
379 playbookInput: news.playbookInput,
380 playbookOutput: news.playbookOutput,
381 };
382
383 let current = yield* getByName(output?.name ?? name);
384 if (current === undefined) {
385 current = yield* findByDisplayName(playbook, displayName);
386 }
387
388 if (current === undefined) {
389 const created = yield* dialogflow
390 .createProjectsLocationsAgentsPlaybooksExamples({
391 parent: playbook,
392 body,
393 })
394 .pipe(
395 Effect.catchTag("Conflict", () =>
396 findByDisplayName(playbook, displayName),
397 ),
398 );
399 current = created ?? undefined;
400 }
401
402 if (current === undefined) {
403 return yield* new AgentsPlaybooksExampleNotResolved({ name });
404 }
405
406 const currentName = current.name ?? name;
407 const displayChanged = (current.displayName ?? "") !== displayName;
408 const descriptionChanged = (current.description ?? "") !== description;
409 const stateChanged =
410 (current.conversationState ?? DEFAULT_STATE) !== conversationState;
411 const actionsChanged =
412 fingerprint(actionsOf(current.actions)) !== fingerprint(actions);
413 const languageChanged = !sameText(
414 current.languageCode,
415 news.languageCode,
416 );
417 const inputChanged =
418 fingerprint(current.playbookInput) !== fingerprint(news.playbookInput);
419 const outputChanged =
420 fingerprint(current.playbookOutput) !==
421 fingerprint(news.playbookOutput);
422
423 if (
424 displayChanged ||
425 descriptionChanged ||
426 stateChanged ||
427 actionsChanged ||
428 languageChanged ||
429 inputChanged ||
430 outputChanged
431 ) {
432 current =
433 yield* dialogflow.patchProjectsLocationsAgentsPlaybooksExamples({
434 // Repeated fields named in an update mask (`actions`) are
435 // silently ignored; the body is complete, so replace it whole.
436 name: currentName,
437 body: { ...body, name: currentName },
438 });
439 }
440
441 return toAttrs(current, env.project, playbook);
442 }, retryQuota),
443
444 delete: Effect.fn(function* ({ output }) {
445 yield* dialogflow
446 .deleteProjectsLocationsAgentsPlaybooksExamples({
447 name: output.name,
448 })
449 .pipe(Effect.catchTag("NotFound", () => Effect.void));
450 }, retryQuota),
451 });