/** * Zod schemas for every structured LLM output the platform consumes. * * Field names mirror what callers already produce — do not rename them * without migrating the corresponding service module. */ import { z } from 'zod'; const topicTypeEnum = z.enum(['concept', 'role', 'process']); const relationTypeStrict = z.enum(['related_to', 'depends_on', 'part_of', 'executed_by']); const learningRelevanceEnum = z.enum(['core', 'standard', 'peripheral', 'exclude']); const extractionTopicSchema = z.object({ id: z.string().min(1), label: z.string().min(1), type: topicTypeEnum, description: z.string().min(1), learning_relevance: learningRelevanceEnum, }); const extractionRelationSchema = z.object({ source: z.string().min(1), target: z.string().min(1), type: relationTypeStrict, }); export const extractionResultSchema = z.object({ topics: z.array(extractionTopicSchema), relations: z.array(extractionRelationSchema), }); const curriculumWeekSchema = z.object({ week_number: z.number().int().min(1).max(26), theme: z.string().min(1), topic_ids: z.array(z.string().min(1)).min(1), estimated_duration: z.number().int().min(15).max(45), week_rationale: z.string().min(1), }); export const curriculumScheduleSchema = z.object({ weeks: z.array(curriculumWeekSchema).length(26), }); const topicEnrichmentSchemaDef = z.object({ id: z.string().min(1), theme: z.string().min(1), complexity_weight: z.number().int().min(1).max(5), difficulty: z.enum(['introductory', 'intermediate', 'advanced']), }); export const topicEnrichmentSchema = z.object({ topics: z.array(topicEnrichmentSchemaDef).min(1), }); const articleSectionSchema = z.object({ heading: z.string().min(1), body: z.string().min(1), }); const articleBodySchema = z.object({ title: z.string().min(1), intro: z.string().min(1), sections: z.array(articleSectionSchema).min(1), keyTakeaways: z.array(z.string().min(1)).min(1), }); export const learningArticleSchema = z.object({ article: articleBodySchema, }); const slideSchema = z.object({ title: z.string().min(1), bullets: z.array(z.string().min(1)).min(1), speakerNote: z.string().min(1), }); export const learningSlidesSchema = z.object({ slides: z.array(slideSchema).min(1), }); const infographicStatSchema = z.object({ value: z.string().min(1), label: z.string().min(1), icon: z.string().min(1), }); const infographicStepSchema = z.object({ number: z.number().int().min(1), title: z.string().min(1), description: z.string().min(1), icon: z.string().min(1), }); const infographicBodySchema = z.object({ headline: z.string().min(1), tagline: z.string().min(1), stats: z.array(infographicStatSchema).min(1), steps: z.array(infographicStepSchema).min(1), quote: z.string().min(1), colorTheme: z.string().min(1), }); export const learningInfographicSchema = z.object({ infographic: infographicBodySchema, }); export const learningAllSchema = z.object({ article: articleBodySchema, slides: z.array(slideSchema).min(1), infographic: infographicBodySchema, }); const quizDifficultyEnum = z.enum(['easy', 'medium', 'hard']).catch('medium'); const quizQuestionSchema = z.object({ id: z.string().min(1).catch(`gen-${Math.random().toString(36).slice(2, 8)}`), question: z.string().min(1), topicLabel: z.string().min(1).catch('General'), options: z.array(z.string().min(1)).length(4), correctIndex: z.preprocess( (v) => (typeof v === 'number' ? Math.round(v) : v), z.number().int().min(0).max(3), ), explanation: z.string().min(1).catch('See the correct answer above.'), difficulty: quizDifficultyEnum, }); export const quizQuestionsSchema = z.object({ questions: z.preprocess( (v) => { // If the model returned an array directly, wrap it if (Array.isArray(v)) return v; // If it returned a single object, wrap in array if (v && typeof v === 'object' && !Array.isArray(v)) return [v]; return v; }, z.array(quizQuestionSchema).min(1), ), }); export const customTopicSchema = z.object({ label: z.string().min(1), type: topicTypeEnum, description: z.string().min(1), }); const mergeActionSchema = z.object({ keepId: z.string().min(1), deleteId: z.string().min(1), }); const newRelationSchema = z.object({ source: z.string().min(1), target: z.string().min(1), type: relationTypeStrict, }); const relevanceUpdateSchema = z.object({ id: z.string().min(1), learning_relevance: learningRelevanceEnum, }); export const graphActionsSchema = z.object({ merges: z.array(mergeActionSchema).optional().default([]), deletions: z.array(z.string().min(1)).optional().default([]), newRelations: z.array(newRelationSchema).optional().default([]), relevanceUpdates: z.array(relevanceUpdateSchema).optional().default([]), }); const deltaTopicSchema = z.object({ id: z.string().min(1), label: z.string().min(1), type: topicTypeEnum, description: z.string().min(1), }); const deltaRelationSchema = z.object({ source: z.string().min(1), target: z.string().min(1), type: relationTypeStrict, }); export const proposeGraphDeltaSchema = z.object({ reason: z.string().min(1), topics: z.array(deltaTopicSchema).max(3).optional(), relations: z.array(deltaRelationSchema).max(5).optional(), }); // ── Article patch operation schemas (Phase 2.4) ────────────────────────────── export const setIntroPatchSchema = z.object({ intro: z.string().min(1), }); export const setSectionPatchSchema = z.object({ heading: z.string().min(1), body: z.string().min(1), }); export const addSectionPatchSchema = z.object({ heading: z.string().min(1), body: z.string().min(1), position: z.enum(['start', 'end']), }); export const removeSectionPatchSchema = z.object({ heading: z.string().min(1), }); export const replaceTakeawaysPatchSchema = z.object({ items: z.array(z.string().min(1)).min(1), }); // ── Theme session schema ───────────────────────────────────────────────────── const themeSessionTopicSection = z.object({ topic_id: z.string().min(1), label: z.string().min(1), summary: z.string().min(1), }); export const themeSessionSchema = z.object({ title: z.string().min(1), intro: z.string().min(1), topicSections: z.array(themeSessionTopicSection).min(1), connections: z.string().min(1), keyTakeaways: z.array(z.string().min(1)).min(3), }); /** * Registry mapping known tool names to their input schemas. `callLLM` * consults this when the caller does not pass an explicit `toolSchemas` * override. */ export const toolSchemaRegistry = { emit_knowledge_graph: extractionResultSchema, emit_curriculum_schedule: curriculumScheduleSchema, emit_topic_enrichment: topicEnrichmentSchema, emit_learning_article: learningArticleSchema, emit_learning_slides: learningSlidesSchema, emit_learning_infographic: learningInfographicSchema, emit_learning_all: learningAllSchema, emit_quiz_questions: quizQuestionsSchema, emit_custom_topic: customTopicSchema, emit_graph_actions: graphActionsSchema, propose_graph_delta: proposeGraphDeltaSchema, set_intro: setIntroPatchSchema, set_section: setSectionPatchSchema, add_section: addSectionPatchSchema, remove_section: removeSectionPatchSchema, replace_takeaways: replaceTakeawaysPatchSchema, emit_theme_session: themeSessionSchema, };