1import { type OrchestrationV2UserInputQuestion, TrimmedNonEmptyString } from "@t3tools/contracts";
2import type { AcpAdapterV2Flavor } from "@t3tools/provider-acp/server/adapter";
3import * as Schema from "effect/Schema";
4
5const isForm = Schema.is(
6 Schema.Struct({ properties: Schema.Record(Schema.String, Schema.Unknown) }),
7);
8const choice = Schema.Struct({
9 const: TrimmedNonEmptyString,
10 title: TrimmedNonEmptyString,
11 description: Schema.optional(Schema.String),
12});
13const isQuestion = Schema.is(
14 Schema.Struct({
15 title: TrimmedNonEmptyString,
16 description: Schema.optional(Schema.String),
17 type: Schema.Literal("string"),
18 oneOf: Schema.Array(choice),
19 }),
20);
21const isCompanion = Schema.is(
22 Schema.Struct({ type: Schema.Literal("string"), title: Schema.Literal("Other (type your own)") }),
23);
24
25/** OMP askDialog pairs qN choices with qN__other and puts the question in title. */
26export const extractOmpElicitationForm: NonNullable<
27 AcpAdapterV2Flavor["extractElicitationForm"]
28> = (request) => {
29 if (request.mode !== "form" || !isForm(request.requestedSchema)) return undefined;
30 const { properties } = request.requestedSchema;
31 const questions: OrchestrationV2UserInputQuestion[] = [];
32 for (const [id, field] of Object.entries(properties)) {
33 if (!/^q\d+$/.test(id)) continue;
34 if (!isQuestion(field) || !isCompanion(properties[`${id}__other`])) return undefined;
35 const choices = field.oneOf;
36 if (choices.length === 0) return undefined;
37 questions.push({
38 id,
39 header: field.description?.trim() || `Question ${questions.length + 1}`,
40 question: field.title,
41 options: choices.map((option) => ({
42 label: option.title,
43 value: option.const,
44 description: option.description?.trim() || option.title,
45 })),
46 });
47 }
48 // Do not hide unknown fields or partially reinterpret a different form.
49 if (questions.length === 0 || Object.keys(properties).length !== questions.length * 2)
50 return undefined;
51 return {
52 questions,
53 respond: (answers) => {
54 const content: Record<string, string> = {};
55 for (const question of questions) {
56 const answer = Object.hasOwn(answers, question.id) ? answers[question.id] : undefined;
57 if (typeof answer !== "string" || !answer.trim()) return { action: "cancel" };
58 if (question.options.some((option) => option.value === answer))
59 content[question.id] = answer;
60 else content[`${question.id}__other`] = answer;
61 }
62 return { action: "accept", content };
63 },
64 };
65};