Files
openmaic/OpenMAIC/lib/makelore-runtime/generation-request.ts

194 lines
6.1 KiB
TypeScript

import type { GenerateClassroomInput } from '@/lib/server/classroom-generation';
import {
CourseMaterialError,
extractManagedCourseMaterials,
} from '@/lib/server/managed-course-materials';
export const LEARNING_GENERATION_CONTRACT_VERSION = 2 as const;
export type LearningGenerationMode = 'single' | 'large';
export interface LearningGenerationRequest {
contractVersion: typeof LEARNING_GENERATION_CONTRACT_VERSION;
ownerUserId: string;
mode: LearningGenerationMode;
input: GenerateClassroomInput;
materialCount: number;
}
export class LearningGenerationRequestError extends Error {
constructor(
readonly code: string,
readonly status: number,
message: string,
) {
super(message);
this.name = 'LearningGenerationRequestError';
}
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function compatibilityBoolean(
value: unknown,
snakeCaseValue: unknown,
fallback = false,
): boolean {
const selected = value ?? snakeCaseValue;
if (selected === undefined) return fallback;
if (typeof selected !== 'boolean') {
throw new LearningGenerationRequestError(
'invalid_request',
400,
'Generation option values must be booleans',
);
}
return selected;
}
async function requestPayload(
request: Request,
): Promise<{ body: Record<string, unknown>; files: File[] }> {
const contentType = request.headers.get('content-type')?.toLowerCase() ?? '';
if (contentType.includes('multipart/form-data')) {
const contentLength = Number(request.headers.get('content-length') ?? '');
if (Number.isFinite(contentLength) && contentLength > 155 * 1024 * 1024) {
throw new LearningGenerationRequestError(
'materials_too_large',
413,
'Multipart request exceeds the course-material upload limit',
);
}
const form = await request.formData().catch(() => null);
if (!form) {
throw new LearningGenerationRequestError('invalid_request', 400, 'Malformed multipart body');
}
const rawOptions = form.get('options');
if (typeof rawOptions !== 'string') {
throw new LearningGenerationRequestError(
'invalid_request',
400,
'Multipart generation requires an options JSON field',
);
}
const body = asRecord(JSON.parse(rawOptions) as unknown);
if (!body) {
throw new LearningGenerationRequestError('invalid_request', 400, 'Invalid options JSON');
}
const files = form
.getAll('materials')
.filter((part): part is File => part instanceof File);
if (files.length !== form.getAll('materials').length) {
throw new LearningGenerationRequestError(
'invalid_request',
400,
'Every materials field must be a file',
);
}
return { body, files };
}
if (!contentType.includes('application/json')) {
throw new LearningGenerationRequestError(
'unsupported_media_type',
415,
'Expected application/json or multipart/form-data',
);
}
const body = asRecord(await request.json().catch(() => null));
if (!body) throw new LearningGenerationRequestError('invalid_request', 400, 'Invalid JSON body');
return { body, files: [] };
}
export async function parseLearningGenerationRequest(
request: Request,
): Promise<LearningGenerationRequest> {
const { body, files } = await requestPayload(request);
const version = body.contractVersion ?? body.contract_version;
if (version !== undefined && version !== LEARNING_GENERATION_CONTRACT_VERSION) {
throw new LearningGenerationRequestError(
'unsupported_contract_version',
400,
`Only generation contract version ${LEARNING_GENERATION_CONTRACT_VERSION} is supported`,
);
}
const owner = body.owner_user_id;
if (
typeof owner !== 'string' ||
!owner.trim() ||
owner.trim().length > 256 ||
/[\u0000-\u001f\u007f]/.test(owner)
) {
throw new LearningGenerationRequestError('invalid_owner', 400, 'Invalid owner_user_id');
}
if (body.mode !== 'single' && body.mode !== 'large') {
throw new LearningGenerationRequestError(
'invalid_mode',
400,
'mode must be "single" or "large"',
);
}
if (
typeof body.requirement !== 'string' ||
!body.requirement.trim() ||
body.requirement.trim().length > 20_000
) {
throw new LearningGenerationRequestError(
'invalid_requirement',
400,
'requirement must contain 1-20000 characters',
);
}
const forbidden = [
'providerId',
'apiKey',
'baseUrl',
'storageKey',
'webSearchApiKey',
'webSearchProviderId',
].find((field) => body[field] !== undefined);
if (forbidden) {
throw new LearningGenerationRequestError(
'caller_provider_configuration_forbidden',
400,
`${forbidden} is server-managed and cannot be supplied by the caller`,
);
}
let pdfContent: { text: string; images: string[] } | undefined;
try {
if (files.length > 0) pdfContent = await extractManagedCourseMaterials(files);
} catch (error) {
if (error instanceof CourseMaterialError) {
throw new LearningGenerationRequestError(error.code, error.status, error.message);
}
throw error;
}
return {
contractVersion: LEARNING_GENERATION_CONTRACT_VERSION,
ownerUserId: owner.trim(),
mode: body.mode,
materialCount: files.length,
input: {
requirement: body.requirement.trim(),
enableWebSearch: compatibilityBoolean(body.enableWebSearch, body.enable_web_search),
enableImageGeneration: compatibilityBoolean(
body.enableImageGeneration,
body.enable_image_generation,
),
enableVideoGeneration: compatibilityBoolean(
body.enableVideoGeneration,
body.enable_video_generation,
),
enableTTS: compatibilityBoolean(body.enableTTS, body.enable_tts),
interactiveMode: compatibilityBoolean(body.interactiveMode, body.interactive_mode),
taskEngineMode: compatibilityBoolean(body.taskEngineMode, body.task_engine_mode),
...(pdfContent ? { pdfContent } : {}),
},
};
}