|
|
|
@@ -0,0 +1,311 @@
|
|
|
|
|
import { z } from 'zod';
|
|
|
|
|
import { CoursePlanSchema } from '../types';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mutation tool schemas available only to teachers.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
export const createSectionSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Create a new section in the course. Sections group lessons together. Use this when building a course structure from a plan.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
title: z.string().min(1).describe('Section title'),
|
|
|
|
|
order: z.number().int().min(0).describe('Display order (0-based). Set after existing sections when appending.')
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateSectionSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Update metadata for an existing section in the course. Use this to rename a section or change its display order instead of creating a new section.',
|
|
|
|
|
parameters: z
|
|
|
|
|
.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
sectionId: z.string().describe('The section ID to update'),
|
|
|
|
|
title: z.string().min(1).optional().describe('New section title'),
|
|
|
|
|
order: z.number().int().min(0).optional().describe('New section display order (0-based)')
|
|
|
|
|
})
|
|
|
|
|
.refine((data) => data.title !== undefined || data.order !== undefined, {
|
|
|
|
|
message: 'Provide at least one field to update'
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const createLessonSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Create a new lesson within a section. The lesson is created with a title but no content — use update_lesson_content to add content after creation.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
sectionId: z.string().describe('The section ID to place the lesson in'),
|
|
|
|
|
title: z.string().min(1).describe('Lesson title'),
|
|
|
|
|
order: z
|
|
|
|
|
.number()
|
|
|
|
|
.int()
|
|
|
|
|
.min(0)
|
|
|
|
|
.describe(
|
|
|
|
|
'Display order within the section (0-based). Lessons and exercises share the SAME order space within a section, so increment by 1 across both. Example: lesson1=0, lesson2=1, exercise=2.'
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateLessonSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Update metadata for an existing lesson in the course. Use this to rename, move, reorder, schedule, or change visibility/unlock settings for a lesson instead of creating a new lesson.',
|
|
|
|
|
parameters: z
|
|
|
|
|
.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
lessonId: z.string().describe('The lesson ID to update'),
|
|
|
|
|
title: z.string().min(1).optional().describe('New lesson title'),
|
|
|
|
|
sectionId: z.string().optional().describe('Move the lesson into a different section'),
|
|
|
|
|
order: z.number().int().min(0).optional().describe('New order within the section (0-based)'),
|
|
|
|
|
lessonAt: z.string().optional().describe('Scheduled lesson datetime'),
|
|
|
|
|
callUrl: z.string().optional().describe('Optional live session URL'),
|
|
|
|
|
isUnlocked: z.boolean().optional().describe('Whether the lesson is unlocked for students'),
|
|
|
|
|
public: z.boolean().optional().describe('Whether the lesson is public')
|
|
|
|
|
})
|
|
|
|
|
.refine(
|
|
|
|
|
(data) =>
|
|
|
|
|
data.title !== undefined ||
|
|
|
|
|
data.sectionId !== undefined ||
|
|
|
|
|
data.order !== undefined ||
|
|
|
|
|
data.lessonAt !== undefined ||
|
|
|
|
|
data.callUrl !== undefined ||
|
|
|
|
|
data.isUnlocked !== undefined ||
|
|
|
|
|
data.public !== undefined,
|
|
|
|
|
{
|
|
|
|
|
message: 'Provide at least one field to update'
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateLessonContentSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Update the text content of a lesson. Replaces the full lesson HTML for the given locale. When modifying only part of a lesson, always preserve all existing content and only change the specific section requested.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
lessonId: z.string().describe('The lesson ID to update'),
|
|
|
|
|
locale: z.string().default('en').describe('The locale to write content in'),
|
|
|
|
|
content: z
|
|
|
|
|
.string()
|
|
|
|
|
.min(1)
|
|
|
|
|
.describe(
|
|
|
|
|
'The full lesson HTML content. Use only: h2, h3, p, ul, ol, li, strong, em, blockquote, code, pre, a. Never use div, span, table, img, iframe, script, or style.'
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const createExerciseSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Create a new exercise with questions and answer options. Each question can be multiple choice (RADIO), multiple select (CHECKBOX), true/false, or other types. Quality bar: aim for 6-10 questions covering the full lesson, mixing recall, applied, and scenario-based questions; RADIO and CHECKBOX questions must have at least 4 plausible options.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
lessonId: z.string().optional().describe('Optional lesson ID to link the exercise to'),
|
|
|
|
|
sectionId: z.string().optional().describe('Optional section ID'),
|
|
|
|
|
title: z.string().min(1).describe('Exercise title'),
|
|
|
|
|
description: z.string().optional().describe('Optional short description shown to students above the questions.'),
|
|
|
|
|
order: z
|
|
|
|
|
.number()
|
|
|
|
|
.int()
|
|
|
|
|
.min(0)
|
|
|
|
|
.describe(
|
|
|
|
|
'Display order within the section (0-based). REQUIRED whenever sectionId is provided. Lessons and exercises share the SAME order space within a section, so an end-of-section quiz must have a higher order than every lesson in that section. Example: lesson1=0, lesson2=1, end-of-section exercise=2.'
|
|
|
|
|
),
|
|
|
|
|
questions: z
|
|
|
|
|
.array(
|
|
|
|
|
z.object({
|
|
|
|
|
question: z.string().min(1).describe('The question text'),
|
|
|
|
|
questionTypeId: z
|
|
|
|
|
.number()
|
|
|
|
|
.int()
|
|
|
|
|
.min(1)
|
|
|
|
|
.max(14)
|
|
|
|
|
.default(1)
|
|
|
|
|
.describe('Question type: 1=RADIO (single answer), 2=CHECKBOX (multiple), 4=TRUE_FALSE, 3=TEXTAREA'),
|
|
|
|
|
points: z.number().min(0).default(1).describe('Points for this question'),
|
|
|
|
|
order: z.number().int().min(0).describe('Display order'),
|
|
|
|
|
options: z
|
|
|
|
|
.array(
|
|
|
|
|
z.object({
|
|
|
|
|
label: z.string().min(1).describe('Option text'),
|
|
|
|
|
isCorrect: z.boolean().describe('Whether this is a correct answer')
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.describe(
|
|
|
|
|
'Answer options. RADIO and CHECKBOX questions MUST have at least 4 options with plausible distractors (not obvious filler). RADIO needs exactly one correct option; CHECKBOX needs at least 2 correct and at least 1 incorrect. TRUE_FALSE uses exactly 2 options. Leave empty for NUMERIC/STAR/WORD_BANK.'
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.min(1)
|
|
|
|
|
.describe(
|
|
|
|
|
'Aim for 6-10 questions per exercise when assessing a full lesson. Spread across all sub-topics, mix difficulty, and vary question types.'
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateExerciseSchema = {
|
|
|
|
|
description:
|
|
|
|
|
"Update an existing exercise's metadata (title, description, linked lesson, section, order, due date, lock state, allow-multiple-attempts). Use this to edit the exercise itself — not its questions. To change questions, use update_questions; to add questions, use add_questions.",
|
|
|
|
|
parameters: z
|
|
|
|
|
.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
exerciseId: z.string().describe('The exercise to update. Must be a real UUID returned by a prior tool call.'),
|
|
|
|
|
title: z.string().min(1).optional().describe('New exercise title. Omit to keep unchanged.'),
|
|
|
|
|
description: z
|
|
|
|
|
.string()
|
|
|
|
|
.optional()
|
|
|
|
|
.describe('New short description shown to students. Pass an empty string to clear it.'),
|
|
|
|
|
lessonId: z.string().optional().describe('Link the exercise to a lesson in this course'),
|
|
|
|
|
sectionId: z.string().optional().describe('Place the exercise in a section in this course'),
|
|
|
|
|
order: z.number().int().min(0).optional().describe('New order within the section (0-based)'),
|
|
|
|
|
dueBy: z
|
|
|
|
|
.string()
|
|
|
|
|
.optional()
|
|
|
|
|
.describe('Due date in ISO 8601 format (e.g. 2026-05-01T23:59:00Z). Omit to keep unchanged.'),
|
|
|
|
|
isUnlocked: z.boolean().optional().describe('Whether the exercise is unlocked for students.'),
|
|
|
|
|
allowMultipleAttempts: z.boolean().optional().describe('Whether students can re-take the exercise.')
|
|
|
|
|
})
|
|
|
|
|
.refine(
|
|
|
|
|
(data) =>
|
|
|
|
|
data.title !== undefined ||
|
|
|
|
|
data.description !== undefined ||
|
|
|
|
|
data.lessonId !== undefined ||
|
|
|
|
|
data.sectionId !== undefined ||
|
|
|
|
|
data.order !== undefined ||
|
|
|
|
|
data.dueBy !== undefined ||
|
|
|
|
|
data.isUnlocked !== undefined ||
|
|
|
|
|
data.allowMultipleAttempts !== undefined,
|
|
|
|
|
{
|
|
|
|
|
message: 'Provide at least one field to update'
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const addQuestionsSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Add questions to an existing exercise. Use this when the exercise already exists and you want to add more questions.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
exerciseId: z.string().describe('The existing exercise ID'),
|
|
|
|
|
questions: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
question: z.string().min(1).describe('The question text'),
|
|
|
|
|
questionTypeId: z
|
|
|
|
|
.number()
|
|
|
|
|
.int()
|
|
|
|
|
.min(1)
|
|
|
|
|
.max(14)
|
|
|
|
|
.default(1)
|
|
|
|
|
.describe('Question type: 1=RADIO, 2=CHECKBOX, 4=TRUE_FALSE, 3=TEXTAREA'),
|
|
|
|
|
points: z.number().min(0).default(1).describe('Points for this question'),
|
|
|
|
|
order: z.number().int().min(0).describe('Display order'),
|
|
|
|
|
options: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
label: z.string().min(1).describe('Option text'),
|
|
|
|
|
isCorrect: z.boolean().describe('Whether this is a correct answer')
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateQuestionsSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Update existing questions in an exercise. Pass only the fields you want to change; `id` identifies the question. For NUMERIC the correct answer is `settings.correctValue` (a number), not an option — do NOT add options to NUMERIC questions. For STAR use `settings.correctValue` (1..max). For WORD_BANK use `settings.correctAnswers` and `settings.template`. RADIO/CHECKBOX/TRUE_FALSE use `options[].isCorrect`: include an option `id` to edit it, omit `id` to add a new one. `settings` is shallow-merged with the existing settings. Omit `options` entirely to leave existing options unchanged — if you pass an options array, any existing option not listed in it will be removed.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
exerciseId: z.string().describe('The existing exercise ID'),
|
|
|
|
|
questions: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.number().int().describe('Existing question id. Required.'),
|
|
|
|
|
question: z.string().min(1).optional().describe('New question text. Omit to keep unchanged.'),
|
|
|
|
|
questionTypeId: z.number().int().min(1).max(14).optional(),
|
|
|
|
|
points: z.number().min(0).optional(),
|
|
|
|
|
order: z.number().int().min(0).optional(),
|
|
|
|
|
settings: z
|
|
|
|
|
.record(z.string(), z.unknown())
|
|
|
|
|
.optional()
|
|
|
|
|
.describe(
|
|
|
|
|
'Per-type correct-answer storage. NUMERIC: { correctValue: number, tolerance?: number }. STAR: { correctValue: number }. WORD_BANK: { correctAnswers: string[], template: string }. Shallow-merged with existing settings.'
|
|
|
|
|
),
|
|
|
|
|
options: z
|
|
|
|
|
.array(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z
|
|
|
|
|
.number()
|
|
|
|
|
.int()
|
|
|
|
|
.optional()
|
|
|
|
|
.describe('Include to update an existing option; omit to create a new one.'),
|
|
|
|
|
label: z.string().min(1).optional(),
|
|
|
|
|
isCorrect: z.boolean().optional(),
|
|
|
|
|
settings: z.record(z.string(), z.unknown()).optional()
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.optional()
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateCourseLandingPageSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Update public course landing-page fields, including title, description, overview, goals, requirements, instructor details, pricing, and banner image.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
title: z.string().min(1).optional().describe('Public course title'),
|
|
|
|
|
description: z.string().min(1).optional().describe('Short public course description'),
|
|
|
|
|
overview: z.string().optional().describe('Landing-page overview copy'),
|
|
|
|
|
cost: z.number().int().min(0).optional().describe('Course price in the selected currency'),
|
|
|
|
|
currency: z.enum(['NGN', 'USD']).optional().describe('Course price currency'),
|
|
|
|
|
imageUrl: z.string().url().optional().describe('Banner image URL to use directly'),
|
|
|
|
|
generateImage: z.boolean().optional().describe('Whether to generate/select a banner image from the image query'),
|
|
|
|
|
imageQuery: z.string().min(1).max(120).optional().describe('Search query for a generated/selected banner image'),
|
|
|
|
|
metadata: z
|
|
|
|
|
.object({
|
|
|
|
|
requirements: z.string().optional(),
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
goals: z.string().optional(),
|
|
|
|
|
videoUrl: z.string().optional(),
|
|
|
|
|
showDiscount: z.boolean().optional(),
|
|
|
|
|
discount: z.number().optional(),
|
|
|
|
|
paymentLink: z.string().optional(),
|
|
|
|
|
instructor: z
|
|
|
|
|
.object({
|
|
|
|
|
name: z.string().optional(),
|
|
|
|
|
role: z.string().optional(),
|
|
|
|
|
coursesNo: z.number().optional(),
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
imgUrl: z.string().optional()
|
|
|
|
|
})
|
|
|
|
|
.optional()
|
|
|
|
|
})
|
|
|
|
|
.optional()
|
|
|
|
|
.describe('Landing-page metadata fields')
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const checkCourseGoLiveReadinessSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Check whether a course is ready to publish. Returns blockers, warnings, suggested fixes, and a public course URL when available. Does not publish the course.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID')
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const goLiveCourseSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Publish the course after the teacher explicitly asks to go live. Runs readiness checks first and fails with blockers if required fields or content are missing.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
courseId: z.string().describe('The course ID'),
|
|
|
|
|
confirmPublish: z.literal(true).describe('Must be true after explicit teacher confirmation to publish')
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const generateCoursePlanSchema = {
|
|
|
|
|
description:
|
|
|
|
|
'Generate a structured course plan with sections and lessons based on a topic or uploaded document. Returns a JSON plan that the teacher can review and approve before any content is created. Always use this tool when the teacher asks you to design, structure, or plan a course.',
|
|
|
|
|
parameters: z.object({
|
|
|
|
|
plan: CoursePlanSchema.describe(
|
|
|
|
|
'The proposed course structure with sections and lessons. Each lesson includes a short description and whether it should have an exercise.'
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
};
|