1import * as cci from "@distilled.cloud/gcp/contactcenterinsights_v1";
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 { createInternalLabels } from "../Labels.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 encodeOwnershipLine,
14 hasOwnershipMarker,
15 lastSegment,
16 locationOf,
17 locationParent,
18 ownedByAlchemy,
19 parentOf,
20 parseOwnership,
21} from "./ownership.ts";
22
23export type AgentInfo = {
24 /** Opaque agent id the assessment is for. Required on create. */
25 agentId?: string;
26 /** Agent display name. */
27 displayName?: string;
28 /** Agent type (e.g. `HUMAN_AGENT`). */
29 agentType?: string;
30 /** Agent team. */
31 team?: string;
32 /** Agent teams. */
33 teams?: string[];
34 /** Agent location. */
35 location?: string;
36};
37
38export type AuthorizedViewSetsAuthorizedViewsConversationsAssessmentProps = {
39 /**
40 * Parent conversation under an AuthorizedView
41 * (`{authorizedView}/conversations/{conversation}`). Immutable —
42 * changing it replaces the assessment.
43 */
44 parent: string;
45 /**
46 * Agent the assessment is for. Assessments have no labels or
47 * description field, so Alchemy ownership is stored in
48 * `agentInfo.displayName`.
49 */
50 agentInfo: AgentInfo;
51};
52
53export type AuthorizedViewSetsAuthorizedViewsConversationsAssessment = Resource<
54 "GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedViewsConversationsAssessment",
55 AuthorizedViewSetsAuthorizedViewsConversationsAssessmentProps,
56 {
57 /** Full resource name. */
58 name: string;
59 /** Assessment id (last path segment). */
60 assessmentId: string;
61 /** Parent conversation under the AuthorizedView. */
62 parent: string;
63 /** Location id. */
64 location: string;
65 /** Project id. */
66 project: string;
67 /** Assessment state. */
68 state: string | undefined;
69 /** Agent info with the Alchemy ownership prefix stripped from displayName. */
70 agentInfo: AgentInfo | undefined;
71 /** RFC3339 creation timestamp. */
72 createTime: string | undefined;
73 /** RFC3339 last-update timestamp. */
74 updateTime: string | undefined;
75 },
76 never,
77 Providers
78>;
79
80/**
81 * A Contact Center AI Insights quality assessment of a conversation,
82 * created through an AuthorizedView.
83 *
84 * Assessments have no labels field and no patch API — Alchemy stamps
85 * ownership into `agentInfo.displayName`. Parent conversation is
86 * immutable. Reconcile is observe-then-ensure.
87 *
88 * ### Creating an Assessment
89 * **Example:** Assess a conversation through a view
90 * ```typescript
91 * const assessment =
92 * yield* GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedViewsConversationsAssessment(
93 * "Qa",
94 * {
95 * parent: `${view.name}/conversations/${conversation.conversationId}`,
96 * agentInfo: { agentId: "agent-1", displayName: "Ada" },
97 * },
98 * );
99 * ```
100 *
101 * @resource
102 * @category ContactCenterInsights
103 */
104export const AuthorizedViewSetsAuthorizedViewsConversationsAssessment =
105 Resource<AuthorizedViewSetsAuthorizedViewsConversationsAssessment>(
106 "GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedViewsConversationsAssessment",
107 );
108
109export class AuthorizedViewSetsAuthorizedViewsConversationsAssessmentNotResolved extends Data.TaggedError(
110 "GCP.ContactCenterInsights.AuthorizedViewSetsAuthorizedViewsConversationsAssessmentNotResolved",
111)<{
112 name: string;
113}> {}
114
115const agentInfoOf = (
116 info:
117 | cci.GoogleCloudContactcenterinsightsV1ConversationQualityMetadataAgentInfo
118 | undefined,
119): AgentInfo | undefined => {
120 if (info === undefined) return undefined;
121 const parsed = parseOwnership(info.displayName);
122 return {
123 agentId: info.agentId,
124 displayName: parsed.text,
125 agentType: info.agentType,
126 team: info.team,
127 teams: info.teams,
128 location: info.location,
129 };
130};
131
132const toAttrs = (
133 assessment: cci.GoogleCloudContactcenterinsightsV1Assessment,
134 project: string,
135) => {
136 const name = assessment.name ?? "";
137 return {
138 name,
139 assessmentId: lastSegment(name),
140 parent: parentOf(name),
141 location: locationOf(name),
142 project,
143 state: assessment.state,
144 agentInfo: agentInfoOf(assessment.agentInfo),
145 createTime: assessment.createTime,
146 updateTime: assessment.updateTime,
147 };
148};
149
150const getByName = (name: string) =>
151 name.length === 0
152 ? Effect.succeed(undefined)
153 : cci
154 .getProjectsLocationsAuthorizedViewSetsAuthorizedViewsConversationsAssessments(
155 { name },
156 )
157 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
158
159const listAtParent = (parent: string, project: string) =>
160 cci.listProjectsLocationsAuthorizedViewSetsAuthorizedViewsConversationsAssessments
161 .pages({ parent, pageSize: 100 })
162 .pipe(
163 Stream.flatMap((page) => Stream.fromIterable(page.assessments ?? [])),
164 Stream.filter((assessment) =>
165 hasOwnershipMarker(assessment.agentInfo?.displayName),
166 ),
167 Stream.map((assessment) => toAttrs(assessment, project)),
168 Stream.runCollect,
169 Effect.map((chunk) => Array.from(chunk)),
170 Effect.catchTag("NotFound", () => Effect.succeed([])),
171 );
172
173const findOwned = (
174 parent: string,
175 displayName: string,
176 agentId: string | undefined,
177) =>
178 cci.listProjectsLocationsAuthorizedViewSetsAuthorizedViewsConversationsAssessments
179 .pages({ parent, pageSize: 100 })
180 .pipe(
181 Stream.flatMap((page) => Stream.fromIterable(page.assessments ?? [])),
182 Stream.filter(
183 (assessment) =>
184 assessment.agentInfo?.displayName === displayName &&
185 (agentId === undefined || assessment.agentInfo?.agentId === agentId),
186 ),
187 Stream.runHead,
188 Effect.map((option) =>
189 option._tag === "Some" ? option.value : undefined,
190 ),
191 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
192 );
193
194export const AuthorizedViewSetsAuthorizedViewsConversationsAssessmentProvider =
195 () =>
196 Provider.succeed(AuthorizedViewSetsAuthorizedViewsConversationsAssessment, {
197 stables: [
198 "name",
199 "assessmentId",
200 "parent",
201 "location",
202 "project",
203 "createTime",
204 ],
205
206 diff: Effect.fn(function* ({ news, olds, output }) {
207 if (!isResolved(news)) return undefined;
208 const previousParent = olds?.parent ?? output?.parent;
209 if (previousParent !== undefined && news.parent !== previousParent) {
210 return { action: "replace" as const, deleteFirst: false };
211 }
212 const previousAgent =
213 olds?.agentInfo?.agentId ?? output?.agentInfo?.agentId;
214 if (
215 previousAgent !== undefined &&
216 news.agentInfo.agentId !== undefined &&
217 news.agentInfo.agentId !== previousAgent
218 ) {
219 return { action: "replace" as const, deleteFirst: false };
220 }
221 return undefined;
222 }),
223
224 read: Effect.fn(function* ({ id, olds, output }) {
225 const env = yield* GcpEnvironment.current;
226 let existing = yield* getByName(output?.name ?? "");
227 if (existing === undefined && olds?.parent !== undefined) {
228 const ownership = yield* createInternalLabels(id);
229 existing = yield* findOwned(
230 olds.parent,
231 encodeOwnershipLine(ownership, olds.agentInfo?.displayName),
232 olds.agentInfo?.agentId,
233 );
234 }
235 if (existing === undefined) return undefined;
236 const attrs = toAttrs(existing, env.project);
237 return (yield* ownedByAlchemy(id, existing.agentInfo?.displayName))
238 ? attrs
239 : Unowned(attrs);
240 }),
241
242 list: () =>
243 Effect.gen(function* () {
244 const env = yield* GcpEnvironment.current;
245 const viewsParent = `${locationParent(env.project, env.region)}/authorizedViewSets/-`;
246 const views =
247 yield* cci.listProjectsLocationsAuthorizedViewSetsAuthorizedViews
248 .pages({ parent: viewsParent, pageSize: 100 })
249 .pipe(
250 Stream.flatMap((page) =>
251 Stream.fromIterable(page.authorizedViews ?? []),
252 ),
253 Stream.map((view) => view.name ?? ""),
254 Stream.filter((name) => name.length > 0),
255 Stream.runCollect,
256 Effect.map((chunk) => Array.from(chunk)),
257 Effect.catchTag("NotFound", () =>
258 Effect.succeed([] as string[]),
259 ),
260 );
261 const pages = yield* Effect.forEach(
262 views,
263 (view) => listAtParent(`${view}/conversations/-`, env.project),
264 { concurrency: 4 },
265 );
266 return pages.flat();
267 }),
268
269 reconcile: Effect.fn(function* ({ id, news, output }) {
270 const env = yield* GcpEnvironment.current;
271 const ownership = yield* createInternalLabels(id);
272 const displayName = encodeOwnershipLine(
273 ownership,
274 news.agentInfo.displayName,
275 );
276 const agentInfo = {
277 ...news.agentInfo,
278 displayName,
279 };
280
281 let current = yield* getByName(output?.name ?? "");
282 if (current === undefined) {
283 current = yield* findOwned(
284 news.parent,
285 displayName,
286 news.agentInfo.agentId,
287 );
288 }
289
290 if (current === undefined) {
291 const created = yield* cci
292 .createProjectsLocationsAuthorizedViewSetsAuthorizedViewsConversationsAssessments(
293 {
294 parent: news.parent,
295 body: { agentInfo },
296 },
297 )
298 .pipe(
299 Effect.catchTag("Conflict", () =>
300 findOwned(news.parent, displayName, news.agentInfo.agentId),
301 ),
302 );
303 current = created ?? undefined;
304 }
305
306 if (current === undefined) {
307 return yield* new AuthorizedViewSetsAuthorizedViewsConversationsAssessmentNotResolved(
308 {
309 name: output?.name ?? `${news.parent}/assessments/-`,
310 },
311 );
312 }
313
314 return toAttrs(current, env.project);
315 }),
316
317 delete: Effect.fn(function* ({ output }) {
318 yield* cci
319 .deleteProjectsLocationsAuthorizedViewSetsAuthorizedViewsConversationsAssessments(
320 { name: output.name, force: true },
321 )
322 .pipe(Effect.catchTag("NotFound", () => Effect.void));
323 }),
324 });