1import * as Category from "@distilled.cloud/core/category";
2import * as dialogflow from "@distilled.cloud/gcp/dialogflow_v3";
3import * as GcpRetry from "@distilled.cloud/gcp/Retry";
4import * as Effect from "effect/Effect";
5import * as Schedule from "effect/Schedule";
6import * as Stream from "effect/Stream";
7
8/**
9 * The Dialogflow suite runs ~20 files at once against one project, which
10 * overruns the per-minute "All other requests" quota. Distilled's default
11 * backoff gives up before the minute rolls over, so the suite rides it out.
12 */
13export const quotaTolerant = GcpRetry.policy({
14 while: (error) =>
15 Category.isThrottling(error) || Category.isTransientError(error),
16 schedule: Schedule.max([Schedule.spaced("10 seconds"), Schedule.recurs(12)]),
17});
18
19export const DEFAULT_LOCATION = "global";
20
21export const locationParent = (project: string, location = DEFAULT_LOCATION) =>
22 `projects/${project}/locations/${location}`;
23
24export const getAgent = (name: string) =>
25 dialogflow
26 .getProjectsLocationsAgents({ name })
27 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
28
29const listAgents = (parent: string) =>
30 dialogflow.listProjectsLocationsAgents.pages({ parent, pageSize: 100 }).pipe(
31 Stream.flatMap((page) => Stream.fromIterable(page.agents ?? [])),
32 Stream.runCollect,
33 Effect.map((chunk) => Array.from(chunk)),
34 Effect.catchTag("NotFound", () => Effect.succeed([])),
35 );
36
37export const ensureAgent = (
38 project: string,
39 displayName: string,
40 location = DEFAULT_LOCATION,
41) =>
42 Effect.gen(function* () {
43 const parent = locationParent(project, location);
44 const agents = yield* listAgents(parent);
45 const existing = agents.find((agent) => agent.displayName === displayName);
46 if (existing?.name) {
47 const current = yield* getAgent(existing.name);
48 if (current !== undefined) return current;
49 }
50 return yield* dialogflow.createProjectsLocationsAgents({
51 parent,
52 body: {
53 displayName,
54 defaultLanguageCode: "en",
55 timeZone: "America/Los_Angeles",
56 description: displayName,
57 },
58 });
59 });
60
61export const deleteAgent = (name: string) =>
62 Effect.gen(function* () {
63 if (name.length === 0) return;
64 const existing = yield* getAgent(name);
65 if (existing === undefined) return;
66 yield* dialogflow
67 .deleteProjectsLocationsAgents({ name })
68 .pipe(Effect.catchTag("NotFound", () => Effect.void));
69 }).pipe(Effect.ignore);
70
71export const getEntityType = (name: string) =>
72 dialogflow
73 .getProjectsLocationsAgentsEntityTypes({ name })
74 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
75
76export const ensureEntityType = (agent: string, displayName: string) =>
77 Effect.gen(function* () {
78 const listed = yield* dialogflow.listProjectsLocationsAgentsEntityTypes
79 .pages({ parent: agent, pageSize: 100 })
80 .pipe(
81 Stream.flatMap((page) => Stream.fromIterable(page.entityTypes ?? [])),
82 Stream.runCollect,
83 Effect.map((chunk) => Array.from(chunk)),
84 Effect.catchTag("NotFound", () => Effect.succeed([])),
85 );
86 const existing = listed.find(
87 (entityType) => entityType.displayName === displayName,
88 );
89 if (existing?.name) {
90 const current = yield* getEntityType(existing.name);
91 if (current !== undefined) return current;
92 }
93 return yield* dialogflow.createProjectsLocationsAgentsEntityTypes({
94 parent: agent,
95 body: {
96 displayName,
97 kind: "KIND_MAP",
98 entities: [{ value: "blue", synonyms: ["blue", "navy"] }],
99 },
100 });
101 });
102
103export const deleteEntityType = (name: string) =>
104 dialogflow
105 .deleteProjectsLocationsAgentsEntityTypes({ name, force: true })
106 .pipe(Effect.catchTag("NotFound", () => Effect.void));