1import * as discoveryengine from "@distilled.cloud/gcp/discoveryengine_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import { expandDataStore, parseResourceName } from "./internal.ts";
10
11export type DataStoresConversationProps = {
12 /**
13 * Parent Data Store resource name
14 * `projects/{project}/locations/{location}/dataStores/{dataStore}`.
15 * Immutable — changing it replaces the conversation.
16 */
17 dataStore: string;
18 /**
19 * Unique identifier for tracking users.
20 */
21 userPseudoId?: string;
22 /**
23 * Conversation state (`IN_PROGRESS`, `COMPLETED`).
24 */
25 state?: string;
26};
27
28export type DataStoresConversation = Resource<
29 "GCP.DiscoveryEngine.DataStoresConversation",
30 DataStoresConversationProps,
31 {
32 /** Full resource name `.../dataStores/{dataStore}/conversations/{id}`. */
33 name: string;
34 /** Conversation id (last path segment). Server-assigned on create. */
35 conversationId: string;
36 /** Parent data store resource name. */
37 dataStore: string;
38 /** Project id. */
39 project: string;
40 /** Location id. */
41 location: string;
42 /** Pseudo id. */
43 userPseudoId: string | undefined;
44 /** Conversation state. */
45 state: string | undefined;
46 /** RFC3339 start timestamp. */
47 startTime: string | undefined;
48 /** RFC3339 end timestamp. */
49 endTime: string | undefined;
50 },
51 never,
52 Providers
53>;
54
55/**
56 * A Vertex AI Search Conversation attached to a Data Store.
57 *
58 * Its id is assigned by the API, so only a recorded resource can be read
59 * back. The conversation id is server-assigned. Parent is immutable; user id
60 * and state update in place.
61 *
62 * ### Creating a Conversation
63 * **Example:** Start a conversation
64 * ```typescript
65 * const conversation = yield* GCP.DiscoveryEngine.DataStoresConversation(
66 * "Support",
67 * { dataStore: dataStore.name },
68 * );
69 * ```
70 *
71 * ### Updating a Conversation
72 * **Example:** Mark completed
73 * ```typescript
74 * // Same logical id as before; only the changed props differ.
75 * const conversation = yield* GCP.DiscoveryEngine.DataStoresConversation(
76 * "Support",
77 * {
78 * dataStore: dataStore.name,
79 * state: "COMPLETED",
80 * },
81 * );
82 * ```
83 *
84 * @resource
85 * @category DiscoveryEngine
86 */
87export const DataStoresConversation = Resource<DataStoresConversation>(
88 "GCP.DiscoveryEngine.DataStoresConversation",
89);
90
91export class DataStoresConversationNotResolved extends Data.TaggedError(
92 "GCP.DiscoveryEngine.DataStoresConversationNotResolved",
93)<{
94 name: string;
95}> {}
96
97const toAttrs = (
98 conversation: discoveryengine.GoogleCloudDiscoveryengineV1Conversation,
99 project: string,
100) => {
101 const name = conversation.name ?? "";
102 const parsed = parseResourceName(name, "conversations");
103 return {
104 name,
105 conversationId: parsed.id,
106 dataStore: parsed.dataStore,
107 project: parsed.project || project,
108 location: parsed.location,
109 userPseudoId: conversation.userPseudoId,
110 state: conversation.state,
111 startTime: conversation.startTime,
112 endTime: conversation.endTime,
113 };
114};
115
116const getByName = (name: string) =>
117 name.length === 0
118 ? Effect.succeed(undefined)
119 : discoveryengine
120 .getProjectsLocationsDataStoresConversations({ name })
121 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
122
123export const DataStoresConversationProvider = () =>
124 Provider.succeed(DataStoresConversation, {
125 stables: [
126 "name",
127 "conversationId",
128 "dataStore",
129 "project",
130 "location",
131 "startTime",
132 ],
133
134 diff: Effect.fn(function* ({ news, olds, output }) {
135 if (!isResolved(news)) return undefined;
136 const previousParent = olds?.dataStore ?? output?.dataStore;
137 if (previousParent !== undefined && news.dataStore !== previousParent) {
138 return { action: "replace" as const, deleteFirst: false };
139 }
140 return undefined;
141 }),
142
143 read: Effect.fn(function* ({ output }) {
144 const env = yield* GcpEnvironment.current;
145 // Server-assigned id: only a recorded name can be observed.
146 if (output?.name === undefined) return undefined;
147 const existing = yield* getByName(output.name);
148 if (existing === undefined) return undefined;
149 return toAttrs(existing, env.project);
150 }),
151
152 reconcile: Effect.fn(function* ({ news, output }) {
153 const env = yield* GcpEnvironment.current;
154 const parent = expandDataStore(
155 news.dataStore,
156 env.project,
157 output?.location ?? "global",
158 );
159 const userPseudoId = news.userPseudoId;
160
161 let current = yield* getByName(output?.name ?? "");
162
163 if (current === undefined) {
164 const created = yield* discoveryengine
165 .createProjectsLocationsDataStoresConversations({
166 parent,
167 body: {
168 userPseudoId,
169 state: news.state,
170 },
171 })
172 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
173 current = created ?? undefined;
174 }
175
176 if (current === undefined) {
177 return yield* new DataStoresConversationNotResolved({
178 name: output?.name ?? `${parent}/conversations`,
179 });
180 }
181
182 const userChanged =
183 userPseudoId !== undefined && current.userPseudoId !== userPseudoId;
184 const stateChanged =
185 news.state !== undefined && (current.state ?? "") !== news.state;
186
187 if (userChanged || stateChanged) {
188 current =
189 yield* discoveryengine.patchProjectsLocationsDataStoresConversations({
190 name: current.name ?? "",
191 updateMask: [
192 userChanged ? "user_pseudo_id" : undefined,
193 stateChanged ? "state" : undefined,
194 ]
195 .filter((field): field is string => field !== undefined)
196 .join(","),
197 body: {
198 name: current.name,
199 userPseudoId,
200 state: news.state,
201 },
202 });
203 }
204
205 return toAttrs(current, env.project);
206 }),
207
208 delete: Effect.fn(function* ({ output }) {
209 const existing = yield* getByName(output.name);
210 if (existing === undefined) return;
211 yield* discoveryengine
212 .deleteProjectsLocationsDataStoresConversations({
213 name: output.name,
214 })
215 .pipe(Effect.catchTag("NotFound", () => Effect.void));
216 }),
217 });