|
1 | 1 | import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts"; |
| 2 | +import * as Exit from "effect/Exit"; |
2 | 3 | import * as Schema from "effect/Schema"; |
3 | 4 |
|
4 | | -export const XAiAskUserQuestionRequest = Schema.Unknown; |
| 5 | +const XAiAskUserQuestionOption = Schema.Struct({ |
| 6 | + label: Schema.String, |
| 7 | + description: Schema.optional(Schema.String), |
| 8 | + preview: Schema.optional(Schema.String), |
| 9 | + id: Schema.optional(Schema.String), |
| 10 | +}); |
5 | 11 |
|
6 | | -type UnknownRecord = Record<string, unknown>; |
| 12 | +const XAiAskUserQuestion = Schema.Struct({ |
| 13 | + id: Schema.optional(Schema.String), |
| 14 | + question: Schema.String, |
| 15 | + options: Schema.Array(XAiAskUserQuestionOption), |
| 16 | + multiSelect: Schema.optional(Schema.Boolean), |
| 17 | +}); |
7 | 18 |
|
8 | | -function trimmed(value: string | undefined): string | undefined { |
9 | | - const text = value?.trim(); |
10 | | - return text && text.length > 0 ? text : undefined; |
11 | | -} |
| 19 | +const XAiAskUserQuestionParams = Schema.Struct({ |
| 20 | + sessionId: Schema.String, |
| 21 | + toolCallId: Schema.String, |
| 22 | + questions: Schema.Array(XAiAskUserQuestion), |
| 23 | + mode: Schema.Union([Schema.Literal("default"), Schema.Literal("plan")]), |
| 24 | +}); |
12 | 25 |
|
13 | | -function isRecord(value: unknown): value is UnknownRecord { |
14 | | - return typeof value === "object" && value !== null && !Array.isArray(value); |
15 | | -} |
| 26 | +const XAiWrappedAskUserQuestionParams = Schema.Struct({ |
| 27 | + method: Schema.Literal("x.ai/ask_user_question"), |
| 28 | + params: XAiAskUserQuestionParams, |
| 29 | +}); |
16 | 30 |
|
17 | | -function stringField(record: UnknownRecord, keys: ReadonlyArray<string>): string | undefined { |
18 | | - for (const key of keys) { |
19 | | - const value = record[key]; |
20 | | - if (typeof value === "string") { |
21 | | - const text = trimmed(value); |
22 | | - if (text) { |
23 | | - return text; |
24 | | - } |
25 | | - } |
26 | | - } |
27 | | - return undefined; |
28 | | -} |
| 31 | +export const XAiAskUserQuestionRequest = Schema.Unknown; |
29 | 32 |
|
30 | | -function booleanField(record: UnknownRecord, keys: ReadonlyArray<string>): boolean | undefined { |
31 | | - for (const key of keys) { |
32 | | - const value = record[key]; |
33 | | - if (typeof value === "boolean") { |
34 | | - return value; |
35 | | - } |
36 | | - } |
37 | | - return undefined; |
38 | | -} |
| 33 | +type XAiAskUserQuestionRequestParams = typeof XAiAskUserQuestionParams.Type; |
39 | 34 |
|
40 | | -function arrayField(record: UnknownRecord, keys: ReadonlyArray<string>): ReadonlyArray<unknown> { |
41 | | - for (const key of keys) { |
42 | | - const value = record[key]; |
43 | | - if (Array.isArray(value)) { |
44 | | - return value; |
45 | | - } |
46 | | - } |
47 | | - return []; |
48 | | -} |
| 35 | +const decodeXAiAskUserQuestionParams = Schema.decodeUnknownSync(XAiAskUserQuestionParams); |
| 36 | +const decodeXAiWrappedAskUserQuestionParamsExit = Schema.decodeUnknownExit( |
| 37 | + XAiWrappedAskUserQuestionParams, |
| 38 | +); |
49 | 39 |
|
50 | | -function nestedRecord( |
51 | | - record: UnknownRecord, |
52 | | - keys: ReadonlyArray<string>, |
53 | | -): UnknownRecord | undefined { |
54 | | - for (const key of keys) { |
55 | | - const value = record[key]; |
56 | | - if (isRecord(value)) { |
57 | | - return value; |
58 | | - } |
59 | | - } |
60 | | - return undefined; |
| 40 | +function trimmed(value: string | undefined): string | undefined { |
| 41 | + const text = value?.trim(); |
| 42 | + return text && text.length > 0 ? text : undefined; |
61 | 43 | } |
62 | 44 |
|
63 | | -function unwrapParams(params: unknown): UnknownRecord { |
64 | | - if (!isRecord(params)) { |
65 | | - return {}; |
| 45 | +function unwrapAskUserQuestionParams(params: unknown): XAiAskUserQuestionRequestParams { |
| 46 | + const wrapped = decodeXAiWrappedAskUserQuestionParamsExit(params); |
| 47 | + if (Exit.isSuccess(wrapped)) { |
| 48 | + return wrapped.value.params; |
66 | 49 | } |
67 | | - const request = nestedRecord(params, ["request"]); |
68 | | - const requestInput = request ? nestedRecord(request, ["input", "arguments", "args"]) : undefined; |
69 | | - return nestedRecord(params, ["input", "arguments", "args", "params"]) ?? requestInput ?? params; |
70 | | -} |
71 | | - |
72 | | -function extractOptionLabel(option: unknown): string | undefined { |
73 | | - return typeof option === "string" |
74 | | - ? trimmed(option) |
75 | | - : isRecord(option) |
76 | | - ? stringField(option, ["label", "value", "id", "text", "title", "name"]) |
77 | | - : undefined; |
78 | | -} |
79 | | - |
80 | | -function extractOptions(options: ReadonlyArray<unknown>) { |
81 | | - const extracted = (options ?? []).flatMap((option) => { |
82 | | - const label = extractOptionLabel(option); |
83 | | - if (!label) { |
84 | | - return []; |
85 | | - } |
86 | | - const description = |
87 | | - typeof option === "string" |
88 | | - ? label |
89 | | - : isRecord(option) |
90 | | - ? (stringField(option, ["description", "detail", "subtitle"]) ?? label) |
91 | | - : label; |
92 | | - return [{ label, description }]; |
93 | | - }); |
94 | | - return extracted.length > 0 ? extracted : [{ label: "OK", description: "Continue" }]; |
95 | | -} |
96 | | - |
97 | | -function extractQuestion( |
98 | | - question: unknown, |
99 | | - fallbackTitle: string | undefined, |
100 | | - index: number, |
101 | | -): UserInputQuestion { |
102 | | - const record = isRecord(question) ? question : {}; |
103 | | - const nestedQuestion = nestedRecord(record, ["question"]); |
104 | | - const questionSource = nestedQuestion ?? record; |
105 | | - const questionText = |
106 | | - (typeof question === "string" ? trimmed(question) : undefined) ?? |
107 | | - stringField(questionSource, ["question", "prompt", "text", "content", "message"]) ?? |
108 | | - fallbackTitle ?? |
109 | | - `Question ${index + 1}`; |
110 | | - const id = stringField(questionSource, ["id", "questionId", "key"]) ?? questionText; |
111 | | - return { |
112 | | - id, |
113 | | - header: |
114 | | - stringField(questionSource, ["header", "title", "label"]) ?? fallbackTitle ?? "Question", |
115 | | - question: questionText, |
116 | | - multiSelect: |
117 | | - booleanField(questionSource, ["multiSelect", "allowMultiple", "allow_multiple"]) === true, |
118 | | - options: extractOptions(arrayField(questionSource, ["options", "choices", "answers"])), |
119 | | - }; |
| 50 | + return decodeXAiAskUserQuestionParams(params); |
120 | 51 | } |
121 | 52 |
|
122 | 53 | export function extractXAiAskUserQuestions(params: unknown): ReadonlyArray<UserInputQuestion> { |
123 | | - const root = unwrapParams(params); |
124 | | - const title = stringField(root, ["title", "header", "toolTitle"]); |
125 | | - const questions = arrayField(root, ["questions", "items", "prompts"]); |
126 | | - if (questions.length > 0) { |
127 | | - return questions.map((question, index) => extractQuestion(question, title, index)); |
128 | | - } |
129 | | - const singleQuestion = nestedRecord(root, ["question"]) ?? root; |
130 | | - const singleQuestionOptions = arrayField(root, ["options", "choices", "answers"]); |
131 | | - const question = |
132 | | - singleQuestion === root || singleQuestionOptions.length === 0 |
133 | | - ? singleQuestion |
134 | | - : { ...singleQuestion, options: singleQuestionOptions }; |
135 | | - return [extractQuestion(question, title, 0)]; |
| 54 | + return unwrapAskUserQuestionParams(params).questions.map((question) => ({ |
| 55 | + id: question.id ?? question.question, |
| 56 | + header: "Question", |
| 57 | + question: question.question, |
| 58 | + multiSelect: question.multiSelect === true, |
| 59 | + options: |
| 60 | + question.options.length > 0 |
| 61 | + ? question.options.map((option) => ({ |
| 62 | + label: option.label, |
| 63 | + description: option.description ?? option.label, |
| 64 | + })) |
| 65 | + : [{ label: "OK", description: "Continue" }], |
| 66 | + })); |
136 | 67 | } |
137 | 68 |
|
138 | 69 | function answerValues(answer: unknown): ReadonlyArray<string> { |
|
0 commit comments