/** * 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 relationTypeLoose = z.enum(['related_to', 'depends_on', 'part_of', 'executed_by', 'executes']); 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 handbookTopicSchema = extractionTopicSchema.extend({ metadata: z.object({ source: z.string() }).optional(), }); const handbookRelationSchema = z.object({ source: z.string().min(1), target: z.string().min(1), type: relationTypeLoose, description: z.string().optional(), }); export const handbookResultSchema = z.object({ topics: z.array(handbookTopicSchema), relations: z.array(handbookRelationSchema), }); /** * Normalise legacy `executes` relations into the canonical `executed_by` * vocabulary by swapping source and target. The handbook prompt previously * emitted `role → executes → process`; the canonical form is * `process → executed_by → role`. */ export function normalizeHandbookResult(parsed) { return { ...parsed, relations: parsed.relations.map((r) => r.type === 'executes' ? { ...r, type: 'executed_by', source: r.target, target: r.source } : r, ), }; } 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']); const quizQuestionSchema = z.object({ id: z.string().min(1), question: z.string().min(1), topicLabel: z.string().min(1), options: z.array(z.string().min(1)).length(4), correctIndex: z.number().int().min(0).max(3), explanation: z.string().min(1), difficulty: quizDifficultyEnum, }); export const quizQuestionsSchema = z.object({ questions: 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), }); /** * 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_handbook_delta: handbookResultSchema, 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, };