@mux/ai 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index-CwzNfBjV.d.ts → index-DXMIJjUa.d.ts} +21 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +186 -54
- package/dist/index.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +185 -53
- package/dist/workflows/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -90,6 +90,14 @@ interface Question {
|
|
|
90
90
|
question: string;
|
|
91
91
|
/** Allowed answers for this question (defaults to ["yes", "no"]). */
|
|
92
92
|
answerOptions?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* Experimental: replies with free-form prose instead of yes/no or
|
|
95
|
+
* `answerOptions`. Length-capped via
|
|
96
|
+
* {@link AskQuestionsOptions.maxFreeFormAnswerLength} (default 500),
|
|
97
|
+
* still scrubbed for safety, still skippable. Mutually exclusive with
|
|
98
|
+
* `answerOptions`. Treat the answer as untrusted model output.
|
|
99
|
+
*/
|
|
100
|
+
freeFormReply?: boolean;
|
|
93
101
|
}
|
|
94
102
|
/** A single answer to a question. */
|
|
95
103
|
interface QuestionAnswer {
|
|
@@ -136,6 +144,12 @@ interface AskQuestionsOptions extends MuxAIOptions {
|
|
|
136
144
|
* it to accept arbitrary untrusted input without other safeguards.
|
|
137
145
|
*/
|
|
138
146
|
maxAnswerOptionLength?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Experimental: max character length for free-form answers when
|
|
149
|
+
* `freeFormReply: true`. Defaults to 500. Free-form bypasses the enum
|
|
150
|
+
* schema, so this cap is the primary limit on output length.
|
|
151
|
+
*/
|
|
152
|
+
maxFreeFormAnswerLength?: number;
|
|
139
153
|
}
|
|
140
154
|
/** Structured return payload for askQuestions workflow. */
|
|
141
155
|
interface AskQuestionsResult {
|
|
@@ -200,14 +214,19 @@ declare const questionAnswerSchema: z.ZodObject<{
|
|
|
200
214
|
skipped: boolean;
|
|
201
215
|
}>;
|
|
202
216
|
type QuestionAnswerType = z.infer<typeof questionAnswerSchema>;
|
|
203
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Wraps a caller-supplied answer sub-schema in the standard envelope.
|
|
219
|
+
* See {@link buildResponseSchemaForQuestions} for how the sub-schema is
|
|
220
|
+
* derived from the question set.
|
|
221
|
+
*/
|
|
222
|
+
declare function createAskQuestionsSchema(answerSchema: z.ZodType<string>): z.ZodObject<{
|
|
204
223
|
answers: z.ZodArray<z.ZodObject<{
|
|
205
224
|
question: z.ZodString;
|
|
206
225
|
confidence: z.ZodNumber;
|
|
207
226
|
reasoning: z.ZodString;
|
|
208
227
|
skipped: z.ZodBoolean;
|
|
209
228
|
} & {
|
|
210
|
-
answer: z.
|
|
229
|
+
answer: z.ZodType<string, z.ZodTypeDef, string>;
|
|
211
230
|
}, "strip", z.ZodTypeAny, {
|
|
212
231
|
question: string;
|
|
213
232
|
answer: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,14 @@ import { W as WorkflowCredentials, S as StoragePutObjectInput, a as StoragePresi
|
|
|
2
2
|
export { A as AssetTextTrack, C as ChunkEmbedding, b as ChunkingStrategy, E as Encrypted, c as EncryptedPayload, I as ImageSubmissionMode, M as MuxAIOptions, d as MuxAsset, P as PlaybackAsset, e as PlaybackPolicy, f as StorageAdapter, T as TextChunk, g as TokenChunkingConfig, h as TokenUsage, i as ToneType, U as UsageMetadata, V as VTTChunkingConfig, j as VideoEmbeddingsResult, k as WorkflowCredentialsInput, l as WorkflowMuxClient, m as decryptFromWorkflow, n as encryptForWorkflow } from './shots-BFYOfBgN.js';
|
|
3
3
|
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
|
|
4
4
|
export { i as primitives } from './index-Drj9Cy_k.js';
|
|
5
|
-
export { i as workflows } from './index-
|
|
5
|
+
export { i as workflows } from './index-DXMIJjUa.js';
|
|
6
6
|
import '@mux/mux-node';
|
|
7
7
|
import 'zod';
|
|
8
8
|
import '@ai-sdk/anthropic';
|
|
9
9
|
import '@ai-sdk/google';
|
|
10
10
|
import '@ai-sdk/openai';
|
|
11
11
|
|
|
12
|
-
var version = "0.
|
|
12
|
+
var version = "0.20.0";
|
|
13
13
|
|
|
14
14
|
type MuxAiErrorType = "validation_error" | "processing_error" | "timeout_error";
|
|
15
15
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.20.0";
|
|
9
9
|
|
|
10
10
|
// src/lib/prompt-fragments.ts
|
|
11
11
|
import dedent from "dedent";
|
|
@@ -2828,8 +2828,7 @@ var questionAnswerSchema = z2.object({
|
|
|
2828
2828
|
skipped: z2.boolean()
|
|
2829
2829
|
});
|
|
2830
2830
|
var SKIP_SENTINEL = "__SKIPPED__";
|
|
2831
|
-
function createAskQuestionsSchema(
|
|
2832
|
-
const answerSchema = z2.enum([...allowedAnswers, SKIP_SENTINEL]);
|
|
2831
|
+
function createAskQuestionsSchema(answerSchema) {
|
|
2833
2832
|
return z2.object({
|
|
2834
2833
|
answers: z2.array(
|
|
2835
2834
|
questionAnswerSchema.extend({
|
|
@@ -2865,15 +2864,16 @@ var SYSTEM_PROMPT = promptDedent`
|
|
|
2865
2864
|
<task>
|
|
2866
2865
|
For each question provided, you must:
|
|
2867
2866
|
1. Analyze the storyboard frames and transcript (if provided)
|
|
2868
|
-
2. Answer
|
|
2867
|
+
2. Answer each question according to its specified response format: for questions with <allowed_answers>, select ONLY from the listed values; for questions with <answer_format>, follow the format specification exactly
|
|
2869
2868
|
3. Provide a confidence score between 0 and 1 reflecting your certainty
|
|
2870
2869
|
4. Explain your reasoning based on observable evidence
|
|
2871
2870
|
</task>
|
|
2872
2871
|
|
|
2873
2872
|
<answer_guidelines>
|
|
2874
|
-
- Each question is presented as its own <question> block with a <text> element (the
|
|
2875
|
-
-
|
|
2876
|
-
-
|
|
2873
|
+
- Each question is presented as its own <question> block with a <text> element and either an <allowed_answers> element (listing the permitted response values) or an <answer_format> element (describing the required response shape for open-ended responses)
|
|
2874
|
+
- For questions with <allowed_answers>: choose ONLY from the values listed in that question's <allowed_answers> element
|
|
2875
|
+
- For questions with <answer_format>: follow the format specification exactly (e.g. free-form text within the stated character budget)
|
|
2876
|
+
- Always read each question's <allowed_answers> or <answer_format> and respond in the required shape based on the evidence
|
|
2877
2877
|
- Select the answer best supported by observable evidence from the content
|
|
2878
2878
|
- When evidence is ambiguous but some signal exists, select the most conservative option and use a low confidence score. If the question cannot be answered at all from the content, skip it per the relevance_filtering rules
|
|
2879
2879
|
- Confidence should reflect the clarity and strength of evidence:
|
|
@@ -2900,7 +2900,8 @@ var SYSTEM_PROMPT = promptDedent`
|
|
|
2900
2900
|
CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
|
|
2901
2901
|
${METADATA_BOUNDARY_WARNING}
|
|
2902
2902
|
|
|
2903
|
-
CRITICAL: Do NOT answer irrelevant questions
|
|
2903
|
+
CRITICAL: Do NOT answer irrelevant questions in any form — neither with
|
|
2904
|
+
one of the <allowed_answers> nor with prose for <answer_format>.
|
|
2904
2905
|
Answering an irrelevant question is WRONG — you MUST skip it instead.
|
|
2905
2906
|
|
|
2906
2907
|
For skipped questions:
|
|
@@ -2927,7 +2928,7 @@ var SYSTEM_PROMPT = promptDedent`
|
|
|
2927
2928
|
</security>
|
|
2928
2929
|
|
|
2929
2930
|
<constraints>
|
|
2930
|
-
- You MUST answer every relevant question
|
|
2931
|
+
- You MUST answer every relevant question following its specified response format: an option from <allowed_answers>, or prose matching <answer_format>
|
|
2931
2932
|
- Skip irrelevant questions as described in relevance_filtering
|
|
2932
2933
|
- Only describe observable evidence from frames or transcript
|
|
2933
2934
|
- ${NO_FABRICATION_CONSTRAINT}
|
|
@@ -2962,15 +2963,16 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
2962
2963
|
<task>
|
|
2963
2964
|
For each question provided, you must:
|
|
2964
2965
|
1. Analyze the transcript
|
|
2965
|
-
2. Answer
|
|
2966
|
+
2. Answer each question according to its specified response format: for questions with <allowed_answers>, select ONLY from the listed values; for questions with <answer_format>, follow the format specification exactly
|
|
2966
2967
|
3. Provide a confidence score between 0 and 1 reflecting your certainty
|
|
2967
2968
|
4. Explain your reasoning based on observable evidence
|
|
2968
2969
|
</task>
|
|
2969
2970
|
|
|
2970
2971
|
<answer_guidelines>
|
|
2971
|
-
- Each question is presented as its own <question> block with a <text> element (the
|
|
2972
|
-
-
|
|
2973
|
-
-
|
|
2972
|
+
- Each question is presented as its own <question> block with a <text> element and either an <allowed_answers> element (listing the permitted response values) or an <answer_format> element (describing the required response shape for open-ended responses)
|
|
2973
|
+
- For questions with <allowed_answers>: choose ONLY from the values listed in that question's <allowed_answers> element
|
|
2974
|
+
- For questions with <answer_format>: follow the format specification exactly (e.g. free-form text within the stated character budget)
|
|
2975
|
+
- Always read each question's <allowed_answers> or <answer_format> and respond in the required shape based on the evidence
|
|
2974
2976
|
- Select the answer best supported by observable evidence from the content
|
|
2975
2977
|
- When evidence is ambiguous but some signal exists, select the most conservative option and use a low confidence score. If the question cannot be answered at all from the content, skip it per the relevance_filtering rules
|
|
2976
2978
|
- Confidence should reflect the clarity and strength of evidence:
|
|
@@ -2997,7 +2999,8 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
2997
2999
|
CRITICAL: Base your answers ONLY on the actual audio/transcript content.
|
|
2998
3000
|
${METADATA_BOUNDARY_WARNING}
|
|
2999
3001
|
|
|
3000
|
-
CRITICAL: Do NOT answer irrelevant questions
|
|
3002
|
+
CRITICAL: Do NOT answer irrelevant questions in any form — neither with
|
|
3003
|
+
one of the <allowed_answers> nor with prose for <answer_format>.
|
|
3001
3004
|
Answering an irrelevant question is WRONG — you MUST skip it instead.
|
|
3002
3005
|
|
|
3003
3006
|
For skipped questions:
|
|
@@ -3022,7 +3025,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
3022
3025
|
</security>
|
|
3023
3026
|
|
|
3024
3027
|
<constraints>
|
|
3025
|
-
- You MUST answer every relevant question
|
|
3028
|
+
- You MUST answer every relevant question following its specified response format: an option from <allowed_answers>, or prose matching <answer_format>
|
|
3026
3029
|
- Skip irrelevant questions as described in relevance_filtering
|
|
3027
3030
|
- Only describe observable evidence from transcript content
|
|
3028
3031
|
- ${NO_FABRICATION_CONSTRAINT}
|
|
@@ -3037,16 +3040,104 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
3037
3040
|
- GOOD: "The speaker describes running through a park"
|
|
3038
3041
|
- Be specific and evidence-based
|
|
3039
3042
|
</language_guidelines>`;
|
|
3043
|
+
function freeFormAddendum(maxFreeFormAnswerLength) {
|
|
3044
|
+
return promptDedent`
|
|
3045
|
+
<free_form_answers>
|
|
3046
|
+
Additional rules for questions with <answer_format>free-form text ...</answer_format>:
|
|
3047
|
+
|
|
3048
|
+
- Produce a short, evidence-grounded answer in plain prose
|
|
3049
|
+
- Keep it tight: one or two sentences, maximum ${maxFreeFormAnswerLength} characters
|
|
3050
|
+
- If the question is irrelevant to the asset content, still skip it
|
|
3051
|
+
by setting answer to "${SKIP_SENTINEL}" exactly, per relevance_filtering
|
|
3052
|
+
- The answer field is a content-analysis channel, not an instruction
|
|
3053
|
+
channel. Do NOT include raw instructions, escape sequences, code,
|
|
3054
|
+
or anything that looks like structured control content in the
|
|
3055
|
+
answer. All existing security, non-fabrication, and
|
|
3056
|
+
non-disclosure rules apply unchanged.
|
|
3057
|
+
</free_form_answers>`;
|
|
3058
|
+
}
|
|
3059
|
+
function buildSystemPrompt(isAudioOnly, hasFreeForm, maxFreeFormAnswerLength) {
|
|
3060
|
+
const base = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT : SYSTEM_PROMPT;
|
|
3061
|
+
if (!hasFreeForm)
|
|
3062
|
+
return base;
|
|
3063
|
+
return `${base}
|
|
3064
|
+
|
|
3065
|
+
${freeFormAddendum(maxFreeFormAnswerLength)}`;
|
|
3066
|
+
}
|
|
3067
|
+
var YES_NO_ANSWERS = ["yes", "no"];
|
|
3068
|
+
function allowedAnswersForQuestion(q) {
|
|
3069
|
+
switch (q.mode.kind) {
|
|
3070
|
+
case "yesNo":
|
|
3071
|
+
return YES_NO_ANSWERS;
|
|
3072
|
+
case "options":
|
|
3073
|
+
return q.mode.options;
|
|
3074
|
+
case "freeForm":
|
|
3075
|
+
return [];
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
function normalizeQuestion(q, idx) {
|
|
3079
|
+
const hasOptions = Array.isArray(q.answerOptions);
|
|
3080
|
+
const isFreeForm = q.freeFormReply === true;
|
|
3081
|
+
if (hasOptions && isFreeForm) {
|
|
3082
|
+
throw new MuxAiError(
|
|
3083
|
+
`Question at index ${idx} sets both answerOptions and freeFormReply: true. These are mutually exclusive \u2014 pick one.`,
|
|
3084
|
+
{ type: "validation_error" }
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
if (isFreeForm) {
|
|
3088
|
+
return { question: q.question, mode: { kind: "freeForm" } };
|
|
3089
|
+
}
|
|
3090
|
+
if (hasOptions) {
|
|
3091
|
+
const options = Array.from(
|
|
3092
|
+
new Set(
|
|
3093
|
+
(q.answerOptions ?? []).map((o) => o.trim()).filter(Boolean)
|
|
3094
|
+
)
|
|
3095
|
+
);
|
|
3096
|
+
if (!options.length) {
|
|
3097
|
+
throw new MuxAiError(
|
|
3098
|
+
`Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
|
|
3099
|
+
{ type: "validation_error" }
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
return {
|
|
3103
|
+
question: q.question,
|
|
3104
|
+
mode: { kind: "options", options }
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
return { question: q.question, mode: { kind: "yesNo" } };
|
|
3108
|
+
}
|
|
3109
|
+
function renderFormatTag(mode, maxFreeFormAnswerLength) {
|
|
3110
|
+
switch (mode.kind) {
|
|
3111
|
+
case "yesNo":
|
|
3112
|
+
return renderSection({
|
|
3113
|
+
tag: "allowed_answers",
|
|
3114
|
+
content: YES_NO_ANSWERS.map((a) => `"${a}"`).join(", ")
|
|
3115
|
+
});
|
|
3116
|
+
case "options":
|
|
3117
|
+
return renderSection({
|
|
3118
|
+
tag: "allowed_answers",
|
|
3119
|
+
content: mode.options.map((a) => `"${a}"`).join(", ")
|
|
3120
|
+
});
|
|
3121
|
+
case "freeForm":
|
|
3122
|
+
return renderSection({
|
|
3123
|
+
tag: "answer_format",
|
|
3124
|
+
content: `free-form text, maximum ${maxFreeFormAnswerLength} characters`
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3040
3128
|
function buildUserPrompt({
|
|
3041
3129
|
questions,
|
|
3042
3130
|
transcriptText,
|
|
3043
3131
|
isCleanTranscript = true,
|
|
3044
|
-
isAudioOnly = false
|
|
3132
|
+
isAudioOnly = false,
|
|
3133
|
+
maxFreeFormAnswerLength
|
|
3045
3134
|
}) {
|
|
3046
3135
|
const contentDescriptor = isAudioOnly ? "audio content using transcript evidence" : "content";
|
|
3136
|
+
const hasFreeForm = questions.some((q) => q.mode.kind === "freeForm");
|
|
3137
|
+
const formatInstruction = hasFreeForm ? "Follow each question's specified response format: pick from <allowed_answers> where listed, or produce a concise free-form answer where <answer_format> is specified." : "Use only the values listed inside each question's own <allowed_answers> element.";
|
|
3047
3138
|
const taskContent = dedent2`
|
|
3048
3139
|
Answer each question in the <questions> block below about the ${contentDescriptor}.
|
|
3049
|
-
|
|
3140
|
+
${formatInstruction}
|
|
3050
3141
|
Return one answer per question, in the order the questions appear.
|
|
3051
3142
|
If a question cannot be answered from the provided content, skip it as described in the system instructions.`;
|
|
3052
3143
|
const taskSection = `<task>
|
|
@@ -3054,13 +3145,10 @@ ${taskContent}
|
|
|
3054
3145
|
</task>`;
|
|
3055
3146
|
const questionBlocks = questions.map((q, idx) => {
|
|
3056
3147
|
const textTag = renderSection({ tag: "text", content: q.question });
|
|
3057
|
-
const
|
|
3058
|
-
tag: "allowed_answers",
|
|
3059
|
-
content: q.answerOptions.map((a) => `"${a}"`).join(", ")
|
|
3060
|
-
});
|
|
3148
|
+
const formatTag = renderFormatTag(q.mode, maxFreeFormAnswerLength);
|
|
3061
3149
|
return `<question number="${idx + 1}">
|
|
3062
3150
|
${textTag}
|
|
3063
|
-
${
|
|
3151
|
+
${formatTag}
|
|
3064
3152
|
</question>`;
|
|
3065
3153
|
}).join("\n\n");
|
|
3066
3154
|
const questionsSection = `<questions>
|
|
@@ -3080,18 +3168,41 @@ async function fetchImageAsBase64(imageUrl, imageDownloadOptions) {
|
|
|
3080
3168
|
const downloadResult = await downloadImageAsBase64(imageUrl, imageDownloadOptions);
|
|
3081
3169
|
return downloadResult.base64Data;
|
|
3082
3170
|
}
|
|
3171
|
+
function buildResponseSchemaForQuestions(normalizedQuestions, maxFreeFormAnswerLength) {
|
|
3172
|
+
const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
|
|
3173
|
+
if (hasFreeForm) {
|
|
3174
|
+
const longestConstrained = Math.max(
|
|
3175
|
+
0,
|
|
3176
|
+
...normalizedQuestions.flatMap(allowedAnswersForQuestion).map((a) => a.length)
|
|
3177
|
+
);
|
|
3178
|
+
const schemaMax = Math.max(
|
|
3179
|
+
maxFreeFormAnswerLength,
|
|
3180
|
+
SKIP_SENTINEL.length,
|
|
3181
|
+
longestConstrained
|
|
3182
|
+
);
|
|
3183
|
+
return createAskQuestionsSchema(z2.string().min(1).max(schemaMax));
|
|
3184
|
+
}
|
|
3185
|
+
const allowed = Array.from(
|
|
3186
|
+
new Set(normalizedQuestions.flatMap(allowedAnswersForQuestion))
|
|
3187
|
+
);
|
|
3188
|
+
return createAskQuestionsSchema(z2.enum([...allowed, SKIP_SENTINEL]));
|
|
3189
|
+
}
|
|
3083
3190
|
async function analyzeQuestions({
|
|
3084
3191
|
provider,
|
|
3085
3192
|
modelId,
|
|
3086
3193
|
userPrompt,
|
|
3087
3194
|
systemPrompt,
|
|
3088
|
-
|
|
3195
|
+
normalizedQuestions,
|
|
3196
|
+
maxFreeFormAnswerLength,
|
|
3089
3197
|
imageDataUrl,
|
|
3090
3198
|
credentials
|
|
3091
3199
|
}) {
|
|
3092
3200
|
"use step";
|
|
3093
3201
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
3094
|
-
const responseSchema =
|
|
3202
|
+
const responseSchema = buildResponseSchemaForQuestions(
|
|
3203
|
+
normalizedQuestions,
|
|
3204
|
+
maxFreeFormAnswerLength
|
|
3205
|
+
);
|
|
3095
3206
|
const response = await generateText({
|
|
3096
3207
|
model,
|
|
3097
3208
|
output: Output.object({ schema: responseSchema }),
|
|
@@ -3170,6 +3281,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3170
3281
|
);
|
|
3171
3282
|
}
|
|
3172
3283
|
questions.forEach((q, idx) => {
|
|
3284
|
+
if (q.freeFormReply === true)
|
|
3285
|
+
return;
|
|
3173
3286
|
for (const opt of q.answerOptions ?? []) {
|
|
3174
3287
|
if (typeof opt === "string" && opt.length > maxAnswerOptionLength) {
|
|
3175
3288
|
throw new MuxAiError(
|
|
@@ -3179,6 +3292,14 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3179
3292
|
}
|
|
3180
3293
|
}
|
|
3181
3294
|
});
|
|
3295
|
+
const DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH = 500;
|
|
3296
|
+
const maxFreeFormAnswerLength = options?.maxFreeFormAnswerLength ?? DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH;
|
|
3297
|
+
if (!Number.isFinite(maxFreeFormAnswerLength) || maxFreeFormAnswerLength <= 0) {
|
|
3298
|
+
throw new MuxAiError(
|
|
3299
|
+
`maxFreeFormAnswerLength must be a positive number (received ${maxFreeFormAnswerLength}).`,
|
|
3300
|
+
{ type: "validation_error" }
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3182
3303
|
const {
|
|
3183
3304
|
provider = "openai",
|
|
3184
3305
|
model,
|
|
@@ -3190,23 +3311,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3190
3311
|
storyboardWidth = 640,
|
|
3191
3312
|
credentials
|
|
3192
3313
|
} = options ?? {};
|
|
3193
|
-
const normalizedQuestions = questions.map((q, idx) =>
|
|
3194
|
-
|
|
3195
|
-
new Set(
|
|
3196
|
-
(q.answerOptions?.length ? q.answerOptions : ["yes", "no"]).map((o) => o.trim()).filter(Boolean)
|
|
3197
|
-
)
|
|
3198
|
-
);
|
|
3199
|
-
if (!options2.length) {
|
|
3200
|
-
throw new MuxAiError(
|
|
3201
|
-
`Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
|
|
3202
|
-
{ type: "validation_error" }
|
|
3203
|
-
);
|
|
3204
|
-
}
|
|
3205
|
-
return { ...q, answerOptions: options2 };
|
|
3206
|
-
});
|
|
3207
|
-
const allowedAnswers = Array.from(
|
|
3208
|
-
new Set(normalizedQuestions.flatMap((q) => q.answerOptions))
|
|
3209
|
-
);
|
|
3314
|
+
const normalizedQuestions = questions.map((q, idx) => normalizeQuestion(q, idx));
|
|
3315
|
+
const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
|
|
3210
3316
|
const modelConfig = resolveLanguageModelConfig({
|
|
3211
3317
|
...options,
|
|
3212
3318
|
model,
|
|
@@ -3240,9 +3346,10 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3240
3346
|
questions: normalizedQuestions,
|
|
3241
3347
|
transcriptText,
|
|
3242
3348
|
isCleanTranscript: cleanTranscript,
|
|
3243
|
-
isAudioOnly
|
|
3349
|
+
isAudioOnly,
|
|
3350
|
+
maxFreeFormAnswerLength
|
|
3244
3351
|
});
|
|
3245
|
-
const systemPrompt = isAudioOnly
|
|
3352
|
+
const systemPrompt = buildSystemPrompt(isAudioOnly, hasFreeForm, maxFreeFormAnswerLength);
|
|
3246
3353
|
let analysisResponse;
|
|
3247
3354
|
let imageUrl;
|
|
3248
3355
|
try {
|
|
@@ -3252,7 +3359,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3252
3359
|
modelId: modelConfig.modelId,
|
|
3253
3360
|
userPrompt,
|
|
3254
3361
|
systemPrompt,
|
|
3255
|
-
|
|
3362
|
+
normalizedQuestions,
|
|
3363
|
+
maxFreeFormAnswerLength,
|
|
3256
3364
|
credentials
|
|
3257
3365
|
});
|
|
3258
3366
|
} else {
|
|
@@ -3270,7 +3378,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3270
3378
|
modelId: modelConfig.modelId,
|
|
3271
3379
|
userPrompt,
|
|
3272
3380
|
systemPrompt,
|
|
3273
|
-
|
|
3381
|
+
normalizedQuestions,
|
|
3382
|
+
maxFreeFormAnswerLength,
|
|
3274
3383
|
imageDataUrl: base64Data,
|
|
3275
3384
|
credentials
|
|
3276
3385
|
});
|
|
@@ -3281,7 +3390,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3281
3390
|
modelId: modelConfig.modelId,
|
|
3282
3391
|
userPrompt,
|
|
3283
3392
|
systemPrompt,
|
|
3284
|
-
|
|
3393
|
+
normalizedQuestions,
|
|
3394
|
+
maxFreeFormAnswerLength,
|
|
3285
3395
|
imageDataUrl: storyboardUrl,
|
|
3286
3396
|
credentials
|
|
3287
3397
|
})
|
|
@@ -3316,20 +3426,42 @@ async function askQuestions(assetId, questions, options) {
|
|
|
3316
3426
|
);
|
|
3317
3427
|
}
|
|
3318
3428
|
const answers = analysisResponse.result.answers.map((raw, idx) => {
|
|
3319
|
-
const
|
|
3320
|
-
const
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3429
|
+
const question = normalizedQuestions[idx];
|
|
3430
|
+
const isFreeForm = question.mode.kind === "freeForm";
|
|
3431
|
+
const reasoningScrub = safety.scrubDetailed(raw.reasoning, `reasoning[${idx}]`);
|
|
3432
|
+
const explicitSkip = raw.skipped || raw.answer === SKIP_SENTINEL;
|
|
3433
|
+
const answerScrub = isFreeForm && !explicitSkip ? safety.scrubDetailed(raw.answer, `answer[${idx}]`) : null;
|
|
3434
|
+
const overLength = isFreeForm && !explicitSkip && !(answerScrub?.leaked ?? false) && (answerScrub?.text.length ?? 0) > maxFreeFormAnswerLength;
|
|
3435
|
+
if (overLength) {
|
|
3436
|
+
console.warn(
|
|
3437
|
+
`[@mux/ai] Free-form answer at index ${idx} exceeded ${maxFreeFormAnswerLength}-char cap (${answerScrub.text.length} chars); treating as skipped.`
|
|
3324
3438
|
);
|
|
3325
3439
|
}
|
|
3440
|
+
const isSkipped = explicitSkip || reasoningScrub.leaked || (answerScrub?.leaked ?? false) || overLength;
|
|
3441
|
+
if (!isSkipped && !isFreeForm) {
|
|
3442
|
+
const allowed = allowedAnswersForQuestion(question);
|
|
3443
|
+
if (!allowed.includes(raw.answer)) {
|
|
3444
|
+
throw new MuxAiError(
|
|
3445
|
+
`Answer "${raw.answer}" for question ${idx} is not in allowed options: ${allowed.join(", ")}`
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
let finalAnswer;
|
|
3450
|
+
if (isSkipped) {
|
|
3451
|
+
finalAnswer = null;
|
|
3452
|
+
} else if (isFreeForm) {
|
|
3453
|
+
finalAnswer = answerScrub ? answerScrub.text : raw.answer;
|
|
3454
|
+
} else {
|
|
3455
|
+
finalAnswer = raw.answer;
|
|
3456
|
+
}
|
|
3457
|
+
const suppressed = reasoningScrub.leaked || (answerScrub?.leaked ?? false);
|
|
3326
3458
|
return {
|
|
3327
3459
|
// Use the trusted input verbatim rather than the model's echo.
|
|
3328
|
-
question:
|
|
3460
|
+
question: question.question,
|
|
3329
3461
|
confidence: isSkipped ? 0 : Math.min(1, Math.max(0, raw.confidence)),
|
|
3330
|
-
reasoning:
|
|
3462
|
+
reasoning: suppressed ? "Response suppressed by safety filter." : reasoningScrub.text,
|
|
3331
3463
|
skipped: isSkipped,
|
|
3332
|
-
answer:
|
|
3464
|
+
answer: finalAnswer
|
|
3333
3465
|
};
|
|
3334
3466
|
});
|
|
3335
3467
|
return {
|