import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; const XAiAskUserQuestionOption = Schema.Struct({ label: Schema.String, description: Schema.optional(Schema.String), preview: Schema.optional(Schema.String), id: Schema.optional(Schema.String), }); const XAiAskUserQuestion = Schema.Struct({ id: Schema.optional(Schema.String), question: Schema.String, options: Schema.Array(XAiAskUserQuestionOption), multiSelect: Schema.optional(Schema.NullOr(Schema.Boolean)), }); const XAiAskUserQuestionParams = Schema.Struct({ sessionId: Schema.String, toolCallId: Schema.String, questions: Schema.Array(XAiAskUserQuestion), mode: Schema.Literals(["default", "plan"]), }); const XAiWrappedAskUserQuestionParams = Schema.Struct({ method: Schema.Literals(["x.ai/ask_user_question", "_x.ai/ask_user_question"]), params: XAiAskUserQuestionParams, }); export const XAiAskUserQuestionRequest = Schema.Union([ XAiAskUserQuestionParams, XAiWrappedAskUserQuestionParams, ]); type XAiAskUserQuestionRequestParams = typeof XAiAskUserQuestionParams.Type; type XAiAskUserQuestionRequest = typeof XAiAskUserQuestionRequest.Type; function trimmed(value: string | undefined): string | undefined { const text = value?.trim(); return text && text.length > 0 ? text : undefined; } function unwrapAskUserQuestionParams( params: XAiAskUserQuestionRequest, ): XAiAskUserQuestionRequestParams { return "params" in params ? params.params : params; } export function extractXAiAskUserQuestions( params: XAiAskUserQuestionRequest, ): ReadonlyArray { return unwrapAskUserQuestionParams(params).questions.map((question) => ({ id: question.id ?? question.question, header: "Question", question: question.question, multiSelect: question.multiSelect === true, options: question.options.length > 0 ? question.options.map((option) => ({ label: option.label, description: option.description ?? option.label, })) : [{ label: "OK", description: "Continue" }], })); } interface XAiAskUserQuestionAnnotation { readonly preview?: string; readonly notes?: string; } interface XAiAskUserQuestionAcceptedResponse { readonly outcome: "accepted"; readonly answers: Record>; readonly annotations?: Record; } interface XAiAskUserQuestionCancelledResponse { readonly outcome: "cancelled"; } export type XAiAskUserQuestionResponse = | XAiAskUserQuestionAcceptedResponse | XAiAskUserQuestionCancelledResponse; interface NormalizedXAiAnswer { readonly questionText: string; readonly selectedLabels: ReadonlyArray; readonly annotation?: XAiAskUserQuestionAnnotation; } function answerValues(answer: unknown): ReadonlyArray { if (Array.isArray(answer)) { return answer.flatMap((entry) => { const text = typeof entry === "string" ? trimmed(entry) : undefined; return text ? [text] : []; }); } const text = typeof answer === "string" ? trimmed(answer) : undefined; return text ? [text] : []; } function normalizeAnswerForXAi( question: XAiAskUserQuestionRequestParams["questions"][number], answer: unknown, ): NormalizedXAiAnswer | undefined { const values = answerValues(answer); if (values.length === 0) { return undefined; } const optionByLabel = new Map(question.options.map((option) => [option.label, option])); const resolvedValues = values.map((value) => ({ value, option: optionByLabel.get(value), })); const selectedLabels = resolvedValues.flatMap(({ option }) => (option ? [option.label] : [])); const notes = resolvedValues.flatMap(({ option, value }) => (option ? [] : [value])); const preview = question.multiSelect === true ? undefined : resolvedValues.map(({ option }) => trimmed(option?.preview)).find((value) => value); const annotation = preview || notes.length > 0 ? { ...(preview ? { preview } : {}), ...(notes.length > 0 ? { notes: notes.join("\n") } : {}), } : undefined; return { questionText: question.question, selectedLabels: selectedLabels.length > 0 ? selectedLabels : ["Other"], ...(annotation ? { annotation } : {}), }; } function findQuestionAnswer( answers: ProviderUserInputAnswers, question: XAiAskUserQuestionRequestParams["questions"][number], ): unknown { const key = question.id ?? question.question; return answers[key] ?? answers[question.question]; } export function makeXAiAskUserQuestionResponse( params: XAiAskUserQuestionRequest, answers: ProviderUserInputAnswers, ): XAiAskUserQuestionAcceptedResponse { const questions = unwrapAskUserQuestionParams(params).questions; const normalized = questions.flatMap((question) => { const entry = normalizeAnswerForXAi(question, findQuestionAnswer(answers, question)); return entry ? [entry] : []; }); const annotations = Object.fromEntries( normalized.flatMap((entry) => entry.annotation ? [[entry.questionText, entry.annotation] as const] : [], ), ); return { outcome: "accepted", answers: Object.fromEntries( normalized.map((entry) => [entry.questionText, entry.selectedLabels]), ), ...(Object.keys(annotations).length > 0 ? { annotations } : {}), }; } export function makeXAiAskUserQuestionCancelledResponse(): XAiAskUserQuestionCancelledResponse { return { outcome: "cancelled" }; }