1import * as discoveryengine from "@distilled.cloud/gcp/discoveryengine_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 parentBefore,
12 parseResourceName,
13 sameJson,
14 toResourceId,
15} from "./internal.ts";
16
17export type CollectionsEnginesAssistantProps = {
18 /**
19 * Parent Engine resource name
20 * `projects/{project}/locations/{location}/collections/{collection}/engines/{engine}`.
21 * Immutable — changing it replaces the assistant.
22 */
23 engine: string;
24 /**
25 * Assistant id. If omitted, a unique RFC-1034 id is generated.
26 * Immutable — changing it replaces the assistant.
27 */
28 assistantId?: string;
29 /**
30 * User-facing display name (max 128 characters).
31 */
32 displayName?: string;
33 /**
34 * Configuration description.
35 */
36 description?: string;
37 /**
38 * Web grounding type.
39 */
40 webGroundingType?: discoveryengine.GoogleCloudDiscoveryengineV1AssistantWebGroundingTypeEnum;
41 /**
42 * When true, the default web-grounding toggle is off in the UI.
43 * @default false
44 */
45 defaultWebGroundingToggleOff?: boolean;
46 /**
47 * Generation configuration (language, model, system instruction).
48 */
49 generationConfig?: discoveryengine.GoogleCloudDiscoveryengineV1AssistantGenerationConfig;
50 /**
51 * Customer policy (banned phrases, Model Armor).
52 */
53 customerPolicy?: discoveryengine.GoogleCloudDiscoveryengineV1AssistantCustomerPolicy;
54};
55
56export type CollectionsEnginesAssistant = Resource<
57 "GCP.DiscoveryEngine.CollectionsEnginesAssistant",
58 CollectionsEnginesAssistantProps,
59 {
60 /** Full resource name. */
61 name: string;
62 /** Assistant id (last path segment). */
63 assistantId: string;
64 /** Parent engine resource name. */
65 engine: string;
66 /** Project id. */
67 project: string;
68 /** Location id. */
69 location: string;
70 /** Collection id. */
71 collectionId: string;
72 /** User display name. */
73 displayName: string | undefined;
74 /** Description. */
75 description: string | undefined;
76 /** Web grounding type. */
77 webGroundingType: string | undefined;
78 /** Whether the default web-grounding toggle is off. */
79 defaultWebGroundingToggleOff: boolean;
80 /** RFC3339 creation timestamp. */
81 createTime: string | undefined;
82 /** RFC3339 last-update timestamp. */
83 updateTime: string | undefined;
84 },
85 never,
86 Providers
87>;
88
89/**
90 * A Discovery Engine Assistant attached to a collection Engine.
91 *
92 * Without labels, ownership rests on the deterministic id: `read` reports a
93 * resource it finds without prior state as unowned (adopt it with `--adopt`).
94 * Parent engine and assistant id are immutable; display name, description,
95 * grounding, and generation config update in place.
96 *
97 * ### Creating an Assistant
98 * **Example:** Assistant on a search engine
99 * ```typescript
100 * const engine = yield* GCP.DiscoveryEngine.CollectionsEngine("Search", {
101 * dataStoreIds: [store.dataStoreId],
102 * });
103 * const assistant = yield* GCP.DiscoveryEngine.CollectionsEnginesAssistant(
104 * "Help",
105 * {
106 * engine: engine.name,
107 * displayName: "docs helper",
108 * },
109 * );
110 * ```
111 *
112 * ### Updating an Assistant
113 * **Example:** Rename
114 * ```typescript
115 * // Same logical id as before; only the changed props differ.
116 * const assistant = yield* GCP.DiscoveryEngine.CollectionsEnginesAssistant(
117 * "Help",
118 * {
119 * engine: engine.name,
120 * displayName: "docs helper prod",
121 * },
122 * );
123 * ```
124 *
125 * @resource
126 * @category DiscoveryEngine
127 */
128export const CollectionsEnginesAssistant =
129 Resource<CollectionsEnginesAssistant>(
130 "GCP.DiscoveryEngine.CollectionsEnginesAssistant",
131 );
132
133export class CollectionsEnginesAssistantNotResolved extends Data.TaggedError(
134 "GCP.DiscoveryEngine.CollectionsEnginesAssistantNotResolved",
135)<{
136 name: string;
137}> {}
138
139const resourceName = (engine: string, assistantId: string) =>
140 `${engine}/assistants/${assistantId}`;
141
142const getByName = (name: string) =>
143 discoveryengine
144 .getProjectsLocationsCollectionsEnginesAssistants({ name })
145 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
146
147const toAttrs = (
148 assistant: discoveryengine.GoogleCloudDiscoveryengineV1Assistant,
149 project: string,
150) => {
151 const name = assistant.name ?? "";
152 const parsed = parseResourceName(name, "assistants");
153 return {
154 name,
155 assistantId: parsed.id,
156 engine: parentBefore(name, "assistants"),
157 project: parsed.project || project,
158 location: parsed.location,
159 collectionId: parsed.collectionId,
160 displayName: assistant.displayName,
161 description: assistant.description,
162 webGroundingType: assistant.webGroundingType,
163 defaultWebGroundingToggleOff:
164 assistant.defaultWebGroundingToggleOff === true,
165 createTime: assistant.createTime,
166 updateTime: assistant.updateTime,
167 };
168};
169
170export const CollectionsEnginesAssistantProvider = () =>
171 Provider.succeed(CollectionsEnginesAssistant, {
172 stables: [
173 "name",
174 "assistantId",
175 "engine",
176 "project",
177 "location",
178 "collectionId",
179 "createTime",
180 ],
181
182 diff: Effect.fn(function* ({ news, olds, output }) {
183 if (!isResolved(news)) return undefined;
184 const previousEngine = olds?.engine ?? output?.engine;
185 const previousId = olds?.assistantId ?? output?.assistantId;
186 const nextId = news.assistantId ?? previousId;
187 if (
188 (previousEngine !== undefined && news.engine !== previousEngine) ||
189 (previousId !== undefined &&
190 nextId !== undefined &&
191 nextId !== previousId)
192 ) {
193 return { action: "replace" as const, deleteFirst: false };
194 }
195 return undefined;
196 }),
197
198 read: Effect.fn(function* ({ id, olds, output }) {
199 const env = yield* GcpEnvironment.current;
200 const parent = olds?.engine ?? output?.engine;
201 const childId = yield* toResourceId(
202 id,
203 olds?.assistantId,
204 output?.assistantId,
205 );
206 const name =
207 output?.name ??
208 (parent !== undefined ? resourceName(parent, childId) : undefined);
209 if (name === undefined) return undefined;
210 const existing = yield* getByName(name);
211 if (existing === undefined) return undefined;
212 const attrs = toAttrs(existing, env.project);
213 // No labels field: without prior state it may not be ours.
214 return output !== undefined ? attrs : Unowned(attrs);
215 }),
216
217 reconcile: Effect.fn(function* ({ id, news, output }) {
218 const env = yield* GcpEnvironment.current;
219 const assistantId = yield* toResourceId(
220 id,
221 news.assistantId,
222 output?.assistantId,
223 );
224 const description = news.description;
225 const displayName = news.displayName ?? assistantId;
226 const fallbackName =
227 output?.name ?? resourceName(news.engine, assistantId);
228 const groundingOff = news.defaultWebGroundingToggleOff === true;
229
230 let current = yield* getByName(fallbackName);
231
232 if (current === undefined) {
233 const created = yield* discoveryengine
234 .createProjectsLocationsCollectionsEnginesAssistants({
235 parent: news.engine,
236 assistantId,
237 body: {
238 displayName,
239 description,
240 webGroundingType: news.webGroundingType,
241 defaultWebGroundingToggleOff: groundingOff ? true : undefined,
242 generationConfig: news.generationConfig,
243 customerPolicy: news.customerPolicy,
244 },
245 })
246 .pipe(Effect.catchTag("Conflict", () => getByName(fallbackName)));
247 current = created ?? undefined;
248 }
249
250 if (current === undefined) {
251 return yield* new CollectionsEnginesAssistantNotResolved({
252 name: fallbackName,
253 });
254 }
255
256 const name = current.name ?? fallbackName;
257 const displayNameChanged =
258 (current.displayName ?? "") !== (displayName ?? "");
259 const descriptionChanged =
260 (current.description ?? "") !== (description ?? "");
261 const groundingTypeChanged =
262 (current.webGroundingType ?? "") !== (news.webGroundingType ?? "");
263 const groundingOffChanged =
264 (current.defaultWebGroundingToggleOff === true) !== groundingOff;
265 const generationChanged = !sameJson(
266 current.generationConfig,
267 news.generationConfig,
268 );
269 const policyChanged = !sameJson(
270 current.customerPolicy,
271 news.customerPolicy,
272 );
273
274 if (
275 displayNameChanged ||
276 descriptionChanged ||
277 groundingTypeChanged ||
278 groundingOffChanged ||
279 generationChanged ||
280 policyChanged
281 ) {
282 current =
283 yield* discoveryengine.patchProjectsLocationsCollectionsEnginesAssistants(
284 {
285 name,
286 updateMask: [
287 displayNameChanged ? "display_name" : undefined,
288 descriptionChanged ? "description" : undefined,
289 groundingTypeChanged ? "web_grounding_type" : undefined,
290 groundingOffChanged
291 ? "default_web_grounding_toggle_off"
292 : undefined,
293 generationChanged ? "generation_config" : undefined,
294 policyChanged ? "customer_policy" : undefined,
295 ]
296 .filter((field): field is string => field !== undefined)
297 .join(","),
298 body: {
299 name,
300 displayName,
301 description,
302 webGroundingType: news.webGroundingType,
303 defaultWebGroundingToggleOff: groundingOff,
304 generationConfig: news.generationConfig,
305 customerPolicy: news.customerPolicy,
306 },
307 },
308 );
309 }
310
311 return toAttrs(current, env.project);
312 }),
313
314 delete: Effect.fn(function* ({ output }) {
315 const existing = yield* getByName(output.name);
316 if (existing === undefined) return;
317 yield* discoveryengine
318 .deleteProjectsLocationsCollectionsEnginesAssistants({
319 name: output.name,
320 })
321 .pipe(Effect.catchTag("NotFound", () => Effect.void));
322 }),
323 });