chore: ai, public course and other ui improvements
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { QUESTION_TYPE_REGISTRY } from '@cio/question-types';
|
||||
import type { AgentContext } from '../types';
|
||||
|
||||
const QUESTION_TYPE_LIST = QUESTION_TYPE_REGISTRY.map((t) => `- ${t.id} = ${t.typename} — ${t.label}`).join('\n');
|
||||
|
||||
export function buildTeacherSystemPrompt(context: AgentContext): string {
|
||||
const contextLines: string[] = [];
|
||||
|
||||
@@ -43,7 +46,13 @@ export function buildTeacherSystemPrompt(context: AgentContext): string {
|
||||
|
||||
## Your Capabilities
|
||||
|
||||
You have access to specific tools listed below. Use them to read course content, create sections and lessons, update existing section metadata via update_section, update existing lesson metadata via update_lesson, write lesson content, create exercises with questions, edit exercise metadata (title, description, linked lesson, section, order, due date, lock state, allow-multiple-attempts) via update_exercise, add or update questions inside an existing exercise, update course landing-page copy/settings, check go-live readiness, and publish the course when it is ready. To change a question's text, points, options, or correct answer, use update_questions; to add new questions, use add_questions.
|
||||
You have access to specific tools listed below. Use them to read course content, create sections and lessons, update existing course-outline section metadata via update_section, update existing lesson metadata via update_lesson, write lesson content, create exercises with questions, create exercise question blocks via create_exercise_section, edit exercise-level metadata (title, description, linked lesson, course section placement, order, due date, lock state, allow-multiple-attempts) via update_exercise, edit each existing in-exercise question block heading and intro via update_exercise_section (ids from get_exercise_details sections array — not course section ids), add or update questions inside an existing exercise (use add_questions with exerciseSectionId from that sections array when the exercise groups questions into blocks), update course landing-page copy/settings, check go-live readiness, and publish the course when it is ready. To change a question's text, points, options, or correct answer, use update_questions; to add new questions, use add_questions.
|
||||
|
||||
## Question Types
|
||||
|
||||
These are the only question type IDs supported by this platform. Always use these IDs when creating or updating questions — never infer type IDs from exercise data, which only shows which types happen to be in use:
|
||||
|
||||
${QUESTION_TYPE_LIST}
|
||||
|
||||
## Plan Mode vs Agent Mode
|
||||
|
||||
@@ -68,6 +77,13 @@ You have access to specific tools listed below. Use them to read course content,
|
||||
4. Report progress as you go
|
||||
5. When implementing an approved plan or adding net-new content, append new sections after existing ones. Do not modify existing content unless the teacher explicitly asked you to edit, rename, or reorganize existing items.
|
||||
|
||||
**Resuming / Continuing Plan Execution** — When the teacher sends a continuation signal (e.g. "continue", "keep going", "proceed", "resume") after a previous step-limit pause:
|
||||
1. Call get_course_structure FIRST before creating anything.
|
||||
2. Compare the existing sections, lessons, and exercises against the approved plan — match by title.
|
||||
3. Skip every item that already exists. Do NOT recreate a section, lesson, or exercise that is already present in the course.
|
||||
4. If a section already exists but some of its items (lessons or exercises) are missing, resume inside that existing section — do NOT create a duplicate section.
|
||||
5. Begin creating only from the first item in the plan that is not yet present, then continue through the rest of the plan in order.
|
||||
|
||||
**Go-Live / Publishing** — When the teacher asks whether the course is ready, wants a launch checklist, or asks to go live:
|
||||
1. Use check_course_go_live_readiness first to inspect required course details, landing-page fields, lessons, and exercises
|
||||
2. If blockers are returned, explain the blockers clearly and use available tools to fix only the items the teacher asks you to fix
|
||||
@@ -93,26 +109,27 @@ If a lesson has \`hasExercise: true\`, the linked exercise takes the next order
|
||||
|
||||
- If the teacher asks to change a section name or order, first call get_course_structure, then use update_section with the existing section ID. Do NOT create a replacement section.
|
||||
- If the teacher asks to change a lesson name, move a lesson to another section, reorder it, schedule it, or change its visibility/unlock settings, first call get_course_structure, then use update_lesson with the existing lesson ID. Do NOT create a replacement lesson.
|
||||
- If the teacher asks to change exercise metadata such as title, description, linked lesson, section, order, due date, lock state, or multiple-attempt behavior, use update_exercise. Do NOT create a replacement exercise.
|
||||
- If the teacher asks to change exercise metadata such as title, description, linked lesson, course section placement, order, due date, lock state, or multiple-attempt behavior, use update_exercise. Do NOT create a replacement exercise.
|
||||
- If the teacher asks to create a new **block of questions inside an exercise**, use create_exercise_section. If they ask to rename or add intro text for an existing block, call get_exercise_details first, then use update_exercise_section with the exercise id and the section id from the sections array. Do NOT use update_section for that — update_section only changes course outline sections.
|
||||
- Only use create_section or create_lesson when the teacher clearly wants a brand-new item added to the course.
|
||||
|
||||
## Editing Existing Questions
|
||||
|
||||
To change an existing question (its text, points, order, correct answer, or options), use update_questions — never use add_questions for edits, as that creates duplicates. Where the correct answer lives depends on the question type:
|
||||
To change an existing question (its text, points, order, in-exercise section, correct answer, or options), use update_questions — never use add_questions for edits, as that creates duplicates. Use \`exerciseSectionId\` on update_questions to move a question to another block from get_exercise_details \`sections[].id\`, or null to unassign. When adding questions with add_questions, pass exerciseSectionId from get_exercise_details \`sections[].id\` if the exercise has multiple in-exercise blocks so new items land in the right group. Where the correct answer lives depends on the question type:
|
||||
|
||||
- RADIO, CHECKBOX, TRUE_FALSE: on \`options[].isCorrect\`. Include an option's \`id\` to edit it; omit \`id\` to add a new option.
|
||||
- NUMERIC: on \`settings.correctValue\` (a number). Optional \`settings.tolerance\`. Do NOT add options to NUMERIC questions.
|
||||
- STAR: on \`settings.correctValue\` (1..max stars).
|
||||
- WORD_BANK: on \`settings.correctAnswers\` (array, one per \`___\` blank) and \`settings.template\`.
|
||||
|
||||
Always call get_exercise_details first to read current ids and settings before patching.
|
||||
Always call get_exercise_details first to read current question ids, in-exercise section ids (for update_exercise_section), and settings before patching.
|
||||
|
||||
## IDs and Tool Arguments
|
||||
|
||||
- UUIDs and database IDs must be copied EXACTLY from the most recent tool output that produced them. Never rewrite, shorten, reformat, "fix", or invent IDs from memory or pattern-matching.
|
||||
- NEVER pass placeholder strings like \`"string"\`, \`"uuid"\`, \`"<id>"\`, or example IDs from this prompt as tool arguments. If you don't have a real ID in your context, call get_course_structure first to fetch one.
|
||||
- Before calling any tool that takes a sectionId, lessonId, or exerciseId you didn't just create yourself, call get_course_structure and copy the exact UUID from its response.
|
||||
- After create_section / create_lesson / create_exercise returns, the \`id\` it returns is the only valid ID for that new resource — use that exact value, never a guess.
|
||||
- After create_section / create_lesson / create_exercise / create_exercise_section returns, the \`id\` it returns is the only valid ID for that new resource — use that exact value, never a guess.
|
||||
- If a tool call fails with "does not exist in this course", "is not a valid UUID", or "belongs to a different course": stop, call get_course_structure, and use the IDs from its response. Do NOT retry with another guessed ID.
|
||||
- Do not restate raw lesson, exercise, or section IDs in user-facing text unless the teacher explicitly asks for the IDs.
|
||||
- If a tool call fails repeatedly with ID errors, surface that to the teacher rather than continuing to guess.
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
import { createAnthropic } from '@ai-sdk/anthropic';
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { createMoonshotAI } from '@ai-sdk/moonshotai';
|
||||
import { AIProvider, type AIProviderConfig } from '../types';
|
||||
|
||||
const DEFAULT_MODELS: Record<AIProvider, string> = {
|
||||
[AIProvider.OPENAI]: 'gpt-4o',
|
||||
[AIProvider.OPENAI]: 'gpt-5.4-mini',
|
||||
[AIProvider.ANTHROPIC]: 'claude-sonnet-4-20250514',
|
||||
[AIProvider.GOOGLE]: 'gemini-2.5-flash'
|
||||
[AIProvider.GOOGLE]: 'gemini-2.5-flash',
|
||||
[AIProvider.MOONSHOT]: 'kimi-k2.6'
|
||||
};
|
||||
|
||||
const PROVIDER_API_KEY_ENV: Record<AIProvider, string> = {
|
||||
[AIProvider.OPENAI]: 'OPENAI_API_KEY',
|
||||
[AIProvider.ANTHROPIC]: 'ANTHROPIC_API_KEY',
|
||||
[AIProvider.GOOGLE]: 'GOOGLE_API_KEY'
|
||||
[AIProvider.GOOGLE]: 'GOOGLE_API_KEY',
|
||||
[AIProvider.MOONSHOT]: 'MOONSHOT_API_KEY'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,6 +38,10 @@ export function createModel(config: AIProviderConfig) {
|
||||
const google = createGoogleGenerativeAI({ apiKey: config.apiKey });
|
||||
return google(modelName);
|
||||
}
|
||||
case AIProvider.MOONSHOT: {
|
||||
const moonshot = createMoonshotAI({ apiKey: config.apiKey });
|
||||
return moonshot(modelName);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported AI provider: ${config.provider}`);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
updateLessonSchema,
|
||||
updateLessonContentSchema,
|
||||
createExerciseSchema,
|
||||
createExerciseSectionSchema,
|
||||
updateExerciseSchema,
|
||||
updateExerciseSectionSchema,
|
||||
addQuestionsSchema,
|
||||
updateQuestionsSchema,
|
||||
updateCourseLandingPageSchema,
|
||||
@@ -37,7 +39,9 @@ const teacherTools: ToolRegistry = {
|
||||
[ToolName.UPDATE_LESSON]: updateLessonSchema,
|
||||
[ToolName.UPDATE_LESSON_CONTENT]: updateLessonContentSchema,
|
||||
[ToolName.CREATE_EXERCISE]: createExerciseSchema,
|
||||
[ToolName.CREATE_EXERCISE_SECTION]: createExerciseSectionSchema,
|
||||
[ToolName.UPDATE_EXERCISE]: updateExerciseSchema,
|
||||
[ToolName.UPDATE_EXERCISE_SECTION]: updateExerciseSectionSchema,
|
||||
[ToolName.ADD_QUESTIONS]: addQuestionsSchema,
|
||||
[ToolName.UPDATE_QUESTIONS]: updateQuestionsSchema,
|
||||
[ToolName.UPDATE_COURSE_LANDING_PAGE]: updateCourseLandingPageSchema,
|
||||
@@ -76,7 +80,9 @@ export {
|
||||
updateLessonSchema,
|
||||
updateLessonContentSchema,
|
||||
createExerciseSchema,
|
||||
createExerciseSectionSchema,
|
||||
updateExerciseSchema,
|
||||
updateExerciseSectionSchema,
|
||||
addQuestionsSchema,
|
||||
updateQuestionsSchema,
|
||||
updateCourseLandingPageSchema,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
import { QUESTION_TYPE_REGISTRY } from '@cio/question-types';
|
||||
import { CoursePlanSchema } from '../types';
|
||||
|
||||
const QUESTION_TYPE_ID_DESCRIPTION = QUESTION_TYPE_REGISTRY.map((t) => `${t.id}=${t.typename} (${t.label})`).join(', ');
|
||||
|
||||
/**
|
||||
* Mutation tool schemas available only to teachers.
|
||||
*/
|
||||
@@ -15,6 +18,12 @@ export const createSectionSchema = {
|
||||
})
|
||||
};
|
||||
|
||||
const exerciseSectionAfterBehaviorSchema = z.discriminatedUnion('action', [
|
||||
z.object({ action: z.literal('continue') }),
|
||||
z.object({ action: z.literal('submit') }),
|
||||
z.object({ action: z.literal('go_to_section'), exerciseSectionId: z.string().uuid() })
|
||||
]);
|
||||
|
||||
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.',
|
||||
@@ -117,9 +126,9 @@ export const createExerciseSchema = {
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(14)
|
||||
.max(QUESTION_TYPE_REGISTRY.length)
|
||||
.default(1)
|
||||
.describe('Question type: 1=RADIO (single answer), 2=CHECKBOX (multiple), 4=TRUE_FALSE, 3=TEXTAREA'),
|
||||
.describe(`Question type ID. Supported types: ${QUESTION_TYPE_ID_DESCRIPTION}`),
|
||||
points: z.number().min(0).default(1).describe('Points for this question'),
|
||||
order: z.number().int().min(0).describe('Display order'),
|
||||
options: z
|
||||
@@ -141,6 +150,36 @@ export const createExerciseSchema = {
|
||||
})
|
||||
};
|
||||
|
||||
export const createExerciseSectionSchema = {
|
||||
description:
|
||||
'Create a new section inside an existing exercise. Use this to add a new question block before adding questions into it.',
|
||||
parameters: z.object({
|
||||
courseId: z.string().describe('The course ID'),
|
||||
exerciseId: z.string().describe('The exercise that will contain this section'),
|
||||
title: z.string().min(1).describe('Section heading shown above questions in this block'),
|
||||
description: z.string().optional().describe('Optional intro HTML for this block'),
|
||||
order: z.number().int().min(0).describe('Display order within the exercise (0-based)'),
|
||||
colorTheme: z.enum(['blue', 'green', 'amber', 'rose', 'violet', 'slate']).optional(),
|
||||
afterBehavior: exerciseSectionAfterBehaviorSchema.optional()
|
||||
})
|
||||
};
|
||||
|
||||
export const updateExerciseSectionSchema = {
|
||||
description:
|
||||
'Update the title and/or description of one question block inside an exercise (from get_exercise_details sections). Not the same as update_section (course outline).',
|
||||
parameters: z
|
||||
.object({
|
||||
courseId: z.string().describe('The course ID'),
|
||||
exerciseId: z.string().describe('The exercise containing this block'),
|
||||
exerciseSectionId: z.string().describe('Section id from get_exercise_details sections[].id'),
|
||||
title: z.string().min(1).optional().describe('Heading shown above questions in this block'),
|
||||
description: z.string().optional().describe('Optional intro HTML for this block; empty string clears it')
|
||||
})
|
||||
.refine((data) => data.title !== undefined || data.description !== undefined, {
|
||||
message: 'Provide at least title or description'
|
||||
})
|
||||
};
|
||||
|
||||
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.",
|
||||
@@ -181,10 +220,17 @@ export const updateExerciseSchema = {
|
||||
|
||||
export const addQuestionsSchema = {
|
||||
description:
|
||||
'Add questions to an existing exercise. Use this when the exercise already exists and you want to add more questions.',
|
||||
'Add questions to an existing exercise. Use this when the exercise already exists and you want to add more questions. When get_exercise_details returns a non-empty sections array, pass exerciseSectionId so new questions are placed in the right in-exercise block (same ids as update_exercise_section).',
|
||||
parameters: z.object({
|
||||
courseId: z.string().describe('The course ID'),
|
||||
exerciseId: z.string().describe('The existing exercise ID'),
|
||||
exerciseSectionId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.optional()
|
||||
.describe(
|
||||
'In-exercise section id from get_exercise_details `sections[].id` (not a course outline section id). Use when the exercise has question blocks/sections; omit only when questions are not grouped into sections.'
|
||||
),
|
||||
questions: z.array(
|
||||
z.object({
|
||||
question: z.string().min(1).describe('The question text'),
|
||||
@@ -192,9 +238,9 @@ export const addQuestionsSchema = {
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(14)
|
||||
.max(QUESTION_TYPE_REGISTRY.length)
|
||||
.default(1)
|
||||
.describe('Question type: 1=RADIO, 2=CHECKBOX, 4=TRUE_FALSE, 3=TEXTAREA'),
|
||||
.describe(`Question type ID. Supported types: ${QUESTION_TYPE_ID_DESCRIPTION}`),
|
||||
points: z.number().min(0).default(1).describe('Points for this question'),
|
||||
order: z.number().int().min(0).describe('Display order'),
|
||||
options: z.array(
|
||||
@@ -210,7 +256,7 @@ export const addQuestionsSchema = {
|
||||
|
||||
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.',
|
||||
'Update existing questions in an exercise. Pass only the fields you want to change; `id` identifies the question. Optional `exerciseSectionId` (from get_exercise_details sections) moves the question to another in-exercise block; use null to clear section assignment. 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'),
|
||||
@@ -218,9 +264,23 @@ export const updateQuestionsSchema = {
|
||||
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(),
|
||||
questionTypeId: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(QUESTION_TYPE_REGISTRY.length)
|
||||
.optional()
|
||||
.describe(`Question type ID. Supported types: ${QUESTION_TYPE_ID_DESCRIPTION}`),
|
||||
points: z.number().min(0).optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
exerciseSectionId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe(
|
||||
'In-exercise section id from get_exercise_details (not course outline). Omit to leave unchanged; pass null to unassign from a section.'
|
||||
),
|
||||
settings: z
|
||||
.record(z.string(), z.unknown())
|
||||
.optional()
|
||||
|
||||
@@ -14,7 +14,8 @@ export type AgentRole = (typeof AgentRole)[keyof typeof AgentRole];
|
||||
export const AIProvider = {
|
||||
OPENAI: 'openai',
|
||||
ANTHROPIC: 'anthropic',
|
||||
GOOGLE: 'google'
|
||||
GOOGLE: 'google',
|
||||
MOONSHOT: 'moonshot'
|
||||
} as const;
|
||||
|
||||
export type AIProvider = (typeof AIProvider)[keyof typeof AIProvider];
|
||||
@@ -92,7 +93,9 @@ export const ToolName = {
|
||||
UPDATE_LESSON: 'update_lesson',
|
||||
UPDATE_LESSON_CONTENT: 'update_lesson_content',
|
||||
CREATE_EXERCISE: 'create_exercise',
|
||||
CREATE_EXERCISE_SECTION: 'create_exercise_section',
|
||||
UPDATE_EXERCISE: 'update_exercise',
|
||||
UPDATE_EXERCISE_SECTION: 'update_exercise_section',
|
||||
ADD_QUESTIONS: 'add_questions',
|
||||
UPDATE_QUESTIONS: 'update_questions',
|
||||
UPDATE_COURSE_LANDING_PAGE: 'update_course_landing_page',
|
||||
|
||||
Reference in New Issue
Block a user