feat(ai): my_progress tool so the assistant sees the learner's progress
Spec 9.2 has the assistant retrieve from the learner's notes and progress; progress was missing. Reads progressRepo(db).get for the signed-in user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6fb0019cd9
commit
a47e6e0971
5 files changed
+41
-3
No files matched your search
@@ -29,7 +29,7 @@ export function buildSystemPrompt(o: {
|
||||
parts.push(
|
||||
`You are the course assistant for "${title}" on STCourses, a self-hosted course platform. You help one learner with this course: you answer questions, explain ideas, and quiz them when they ask.`,
|
||||
[
|
||||
"Ground your answers in the course material. Use the retrieved sources below and your tools (search_course, read_lesson, read_file, list_notes) to look things up rather than guessing.",
|
||||
"Ground your answers in the course material. Use the retrieved sources below and your tools (search_course, read_lesson, read_file, list_notes, my_progress) to look things up rather than guessing.",
|
||||
"When you rely on a source, cite its file path and lesson in square brackets, for example [lessons/04-exercise.md · Adding numbers]. If the course does not cover something, say so plainly.",
|
||||
"Reference solutions are for authors only and are not available to you. Do not try to reconstruct them.",
|
||||
"Keep answers short and plain. Use Markdown for code.",
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface ToolSet {
|
||||
close?(): Promise<void>;
|
||||
}
|
||||
|
||||
type ProgressLike = { state: "not-started" | "in-progress" | "done"; positionSeconds: number | null };
|
||||
type NoteLike = { id: string; lessonId: string; body: string; anchorKind: string; anchorValue: string | null };
|
||||
const MAX_READ = 60_000;
|
||||
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
||||
@@ -24,6 +25,8 @@ export function courseTools(o: {
|
||||
course: LoadedCourse;
|
||||
index: CourseIndex;
|
||||
listNotes: (lessonId?: string) => Promise<NoteLike[]>;
|
||||
/** The learner's progress in this course, keyed by lesson id (shape of `progressRepo(db).get`). */
|
||||
getProgress?: () => Promise<Record<string, ProgressLike>>;
|
||||
}): ToolSet {
|
||||
const sources: Chunk[] = [];
|
||||
const { course } = o;
|
||||
@@ -54,6 +57,13 @@ export function courseTools(o: {
|
||||
description: "List the learner's own notes in this course, optionally for one lesson.",
|
||||
inputSchema: { type: "object", properties: { lessonId: { type: "string" } }, additionalProperties: false },
|
||||
},
|
||||
...(o.getProgress
|
||||
? [{
|
||||
name: "my_progress",
|
||||
description: "The learner's progress in this course: which lessons are done, in progress (with the video position) or not started.",
|
||||
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
||||
}]
|
||||
: []),
|
||||
];
|
||||
const lessonList = () =>
|
||||
(course.manifest?.modules ?? []).map((m) => `${m.title}: ${m.lessons.map((l) => `${l.id} (${l.title})`).join(", ")}`).join("\n");
|
||||
@@ -100,6 +110,19 @@ export function courseTools(o: {
|
||||
if (!notes.length) return "The learner has no notes here yet.";
|
||||
return noteChunks(course, notes).map((c) => `- ${[c.lessonTitle ?? c.lessonId, c.heading].filter(Boolean).join(", ")}: ${c.text}`).join("\n");
|
||||
}
|
||||
case "my_progress": {
|
||||
if (!o.getProgress) return `Unknown tool "${name}".`;
|
||||
const p = await o.getProgress();
|
||||
const lessons = (course.manifest?.modules ?? []).flatMap((m) => m.lessons);
|
||||
const done = lessons.filter((l) => p[l.id]?.state === "done").length;
|
||||
const clock = (s: number) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
|
||||
const line = (l: { id: string; title: string }) => {
|
||||
const e = p[l.id];
|
||||
const state = e?.state === "done" ? "done" : e?.state === "in-progress" ? `in progress${e.positionSeconds ? `, at ${clock(e.positionSeconds)}` : ""}` : "not started";
|
||||
return `- ${l.title} (${l.id}): ${state}`;
|
||||
};
|
||||
return [`${done} of ${lessons.length} lessons done.`, ...(course.manifest?.modules ?? []).flatMap((m) => [`${m.title}:`, ...m.lessons.map(line)])].join("\n");
|
||||
}
|
||||
default:
|
||||
return `Unknown tool "${name}".`;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { extname, join, relative, sep } from "node:path";
|
||||
import type { AiProvider, AuthUser, ChatMessage, Feature, JobContext, RequestContext, ServerContext } from "@stc/contracts";
|
||||
import { canView } from "@stc/core";
|
||||
import type { LoadedCourse } from "@stc/course-format";
|
||||
import { aclRepo, notesRepo } from "@stc/db";
|
||||
import { aclRepo, notesRepo, progressRepo } from "@stc/db";
|
||||
import { buildSystemPrompt, citationsFor, readMentorText } from "./assistant/prompt";
|
||||
import { combineTools, courseTools, mcpTools, type ToolSet } from "./assistant/tools";
|
||||
import { runAssistant } from "./assistant/loop";
|
||||
@@ -138,6 +138,7 @@ export function aiFeature(ctx: ServerContext, opts: AiFeatureOptions = {}): Feat
|
||||
const settings = aiSettingsRepo(ctx.db, createSecretBox(ctx.config.secret));
|
||||
const acl = aclRepo(ctx.db);
|
||||
const notes = notesRepo(ctx.db);
|
||||
const progress = progressRepo(ctx.db);
|
||||
const sink = opts.draftSink ?? createFsDraftSink(ctx.config.dataDir);
|
||||
const makeProvider = opts.providerFactory ?? ((cfg: ProviderConfig, key: string | null) => createProvider(cfg, key, opts.fetch));
|
||||
|
||||
@@ -236,7 +237,7 @@ export function aiFeature(ctx: ServerContext, opts: AiFeatureOptions = {}): Feat
|
||||
send({ type: "meta", conversationId: convId, mentorOn, mentorAvailable: !!mentorText, source: resolved.source, citations: citationsFor(sources) });
|
||||
let tools: ToolSet | null = null;
|
||||
try {
|
||||
const course_ = courseTools({ repo: ctx.courses, course, index, listNotes: (lid) => notes.list(user.id, course.id, lid) });
|
||||
const course_ = courseTools({ repo: ctx.courses, course, index, listNotes: (lid) => notes.list(user.id, course.id, lid), getProgress: () => progress.get(user.id, course.id) });
|
||||
const servers = mcpServersFor(course.id, (await settings.getServer()).mcpServers);
|
||||
const mcp = servers.length ? await mcpTools(servers, { fetch: opts.fetch, timeoutMs: opts.mcpTimeoutMs }) : null;
|
||||
for (const w of mcp?.warnings ?? []) send({ type: "notice", message: w });
|
||||
|
||||
Reference in new issue
Block a user