@epic-web/workshop-mcp 6.54.0 → 6.54.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -10
- package/dist/prompts.js +4 -3
- package/dist/resources.js +26 -21
- package/dist/server-metadata.d.ts +476 -0
- package/dist/server-metadata.js +666 -0
- package/dist/tools.js +413 -314
- package/dist/utils.js +1 -1
- package/package.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -12,15 +12,93 @@ import * as client from 'openid-client';
|
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { quizMe, quizMeInputSchema } from "./prompts.js";
|
|
14
14
|
import { diffBetweenAppsResource, exerciseContextResource, exerciseStepContextResource, exerciseStepProgressDiffResource, userAccessResource, userInfoResource, userProgressResource, workshopContextResource, } from "./resources.js";
|
|
15
|
+
import { formatToolDescription, toolDocs, } from "./server-metadata.js";
|
|
15
16
|
import { handleWorkshopDirectory, readInWorkshop, safeReadFile, workshopDirectoryInputSchema, } from "./utils.js";
|
|
16
17
|
// not enough support for this yet
|
|
17
18
|
const clientSupportsEmbeddedResources = false;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
function formatToolResponseText({ title, summary, details, nextSteps, }) {
|
|
20
|
+
const lines = [`## ${title}`, '', summary];
|
|
21
|
+
if (details?.length) {
|
|
22
|
+
lines.push('', ...details);
|
|
23
|
+
}
|
|
24
|
+
if (nextSteps?.length) {
|
|
25
|
+
lines.push('', 'Next steps:');
|
|
26
|
+
for (const step of nextSteps) {
|
|
27
|
+
lines.push(`- ${step}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return lines.join('\n').trim();
|
|
31
|
+
}
|
|
32
|
+
function createToolResponse({ toolName, summary, details, nextSteps, includeMetaNextSteps = true, content = [], structuredContent, statusEmoji = '✅', }) {
|
|
33
|
+
const meta = toolDocs[toolName];
|
|
34
|
+
const steps = nextSteps ?? (includeMetaNextSteps ? meta.nextSteps : []);
|
|
35
|
+
const summaryLine = statusEmoji === null ? summary : `${statusEmoji} ${summary}`;
|
|
36
|
+
const text = formatToolResponseText({
|
|
37
|
+
title: meta.title,
|
|
38
|
+
summary: summaryLine,
|
|
39
|
+
details,
|
|
40
|
+
nextSteps: steps,
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: 'text', text }, ...content],
|
|
44
|
+
structuredContent,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createToolErrorResult(toolName, error) {
|
|
48
|
+
const meta = toolDocs[toolName];
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
const nextSteps = meta.errorNextSteps ?? meta.nextSteps;
|
|
51
|
+
const response = createToolResponse({
|
|
52
|
+
toolName,
|
|
53
|
+
summary: `Error: ${message}`,
|
|
54
|
+
statusEmoji: '⚠️',
|
|
55
|
+
nextSteps,
|
|
56
|
+
includeMetaNextSteps: false,
|
|
57
|
+
structuredContent: {
|
|
58
|
+
tool: toolName,
|
|
59
|
+
error: message,
|
|
60
|
+
nextSteps,
|
|
23
61
|
},
|
|
62
|
+
});
|
|
63
|
+
return { ...response, isError: true };
|
|
64
|
+
}
|
|
65
|
+
function parseResourceText(resource) {
|
|
66
|
+
if (typeof resource.text === 'string') {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(resource.text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return resource.text;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function createResourceStructuredContent(resource) {
|
|
77
|
+
return {
|
|
78
|
+
uri: resource.uri,
|
|
79
|
+
mimeType: resource.mimeType,
|
|
80
|
+
data: parseResourceText(resource),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function registerTool(server, toolName, inputSchema, handler) {
|
|
84
|
+
const meta = toolDocs[toolName];
|
|
85
|
+
return server.registerTool(toolName, {
|
|
86
|
+
title: meta.title,
|
|
87
|
+
description: formatToolDescription(meta),
|
|
88
|
+
inputSchema,
|
|
89
|
+
annotations: meta.annotations,
|
|
90
|
+
}, async (args) => {
|
|
91
|
+
try {
|
|
92
|
+
return await handler(args);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return createToolErrorResult(toolName, error);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function initTools(server) {
|
|
100
|
+
registerTool(server, 'login', {
|
|
101
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
24
102
|
}, async ({ workshopDirectory }) => {
|
|
25
103
|
await handleWorkshopDirectory(workshopDirectory);
|
|
26
104
|
const { product: { host }, } = getWorkshopConfig();
|
|
@@ -28,14 +106,22 @@ export function initTools(server) {
|
|
|
28
106
|
const config = await client.discovery(new URL(ISSUER), 'EPICSHOP_APP');
|
|
29
107
|
const deviceResponse = await client.initiateDeviceAuthorization(config, {});
|
|
30
108
|
void handleAuthFlow().catch(() => { });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
109
|
+
const verificationUrl = deviceResponse.verification_uri_complete;
|
|
110
|
+
const userCode = deviceResponse.user_code;
|
|
111
|
+
return createToolResponse({
|
|
112
|
+
toolName: 'login',
|
|
113
|
+
summary: 'Login started. Ask the user to complete device verification.',
|
|
114
|
+
details: [
|
|
115
|
+
`Verification URL: ${verificationUrl}`,
|
|
116
|
+
`User code: ${userCode}`,
|
|
117
|
+
`Expires in: ${deviceResponse.expires_in} seconds`,
|
|
37
118
|
],
|
|
38
|
-
|
|
119
|
+
structuredContent: {
|
|
120
|
+
verificationUrl,
|
|
121
|
+
userCode,
|
|
122
|
+
expiresInSeconds: deviceResponse.expires_in,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
39
125
|
async function handleAuthFlow() {
|
|
40
126
|
const UserInfoSchema = z.object({
|
|
41
127
|
id: z.string(),
|
|
@@ -97,65 +183,32 @@ export function initTools(server) {
|
|
|
97
183
|
}
|
|
98
184
|
}
|
|
99
185
|
});
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
inputSchema: {
|
|
103
|
-
workshopDirectory: workshopDirectoryInputSchema,
|
|
104
|
-
},
|
|
186
|
+
registerTool(server, 'logout', {
|
|
187
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
105
188
|
}, async ({ workshopDirectory }) => {
|
|
106
189
|
await handleWorkshopDirectory(workshopDirectory);
|
|
107
190
|
await logout();
|
|
108
191
|
await deleteCache();
|
|
109
|
-
return {
|
|
110
|
-
|
|
111
|
-
|
|
192
|
+
return createToolResponse({
|
|
193
|
+
toolName: 'logout',
|
|
194
|
+
summary: 'Logged out and cleared cached credentials.',
|
|
195
|
+
structuredContent: { loggedOut: true },
|
|
196
|
+
});
|
|
112
197
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
A. If logged in and there is an incomplete exercise step, set to next incomplete exercise step based on the user's progress - Most common
|
|
128
|
-
- [No arguments]
|
|
129
|
-
B. If not logged in or all exercises are complete, set to next exercise step from current (or first if there is none)
|
|
130
|
-
- [No arguments]
|
|
131
|
-
C. Set to a specific exercise step
|
|
132
|
-
- exerciseNumber: 1
|
|
133
|
-
- stepNumber: 1
|
|
134
|
-
- type: 'solution'
|
|
135
|
-
D. Set to the solution of the current exercise step
|
|
136
|
-
- type: 'solution'
|
|
137
|
-
E. Set to the second step problem of the current exercise
|
|
138
|
-
- stepNumber: 2
|
|
139
|
-
F. Set to the first step problem of the fifth exercise
|
|
140
|
-
- exerciseNumber: 5
|
|
141
|
-
|
|
142
|
-
An error will be returned if no app is found for the given arguments.
|
|
143
|
-
`.trim(),
|
|
144
|
-
inputSchema: {
|
|
145
|
-
workshopDirectory: workshopDirectoryInputSchema,
|
|
146
|
-
exerciseNumber: z.coerce
|
|
147
|
-
.number()
|
|
148
|
-
.optional()
|
|
149
|
-
.describe('The exercise number to set the playground to'),
|
|
150
|
-
stepNumber: z.coerce
|
|
151
|
-
.number()
|
|
152
|
-
.optional()
|
|
153
|
-
.describe('The step number to set the playground to'),
|
|
154
|
-
type: z
|
|
155
|
-
.enum(['problem', 'solution'])
|
|
156
|
-
.optional()
|
|
157
|
-
.describe('The type of app to set the playground to'),
|
|
158
|
-
},
|
|
198
|
+
registerTool(server, 'set_playground', {
|
|
199
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
200
|
+
exerciseNumber: z.coerce
|
|
201
|
+
.number()
|
|
202
|
+
.optional()
|
|
203
|
+
.describe('Exercise number to open (1-based). Omit to use the next incomplete step.'),
|
|
204
|
+
stepNumber: z.coerce
|
|
205
|
+
.number()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe('Step number to open within the exercise (1-based). Omit to keep the current step or advance.'),
|
|
208
|
+
type: z
|
|
209
|
+
.enum(['problem', 'solution'])
|
|
210
|
+
.optional()
|
|
211
|
+
.describe('Step type to open ("problem" or "solution"). Omit to keep the current type or default to problem.'),
|
|
159
212
|
}, async ({ workshopDirectory, exerciseNumber, stepNumber, type }) => {
|
|
160
213
|
await handleWorkshopDirectory(workshopDirectory);
|
|
161
214
|
const authInfo = await getAuthInfo();
|
|
@@ -190,14 +243,19 @@ An error will be returned if no app is found for the given arguments.
|
|
|
190
243
|
});
|
|
191
244
|
invariant(exerciseApp, 'No exercise app found');
|
|
192
245
|
await setPlayground(exerciseApp.fullPath);
|
|
193
|
-
return {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
246
|
+
return createToolResponse({
|
|
247
|
+
toolName: 'set_playground',
|
|
248
|
+
summary: `Playground set to ${exerciseApp.exerciseNumber}.${exerciseApp.stepNumber}.${exerciseApp.type}.`,
|
|
249
|
+
structuredContent: {
|
|
250
|
+
playground: {
|
|
251
|
+
exerciseNumber: exerciseApp.exerciseNumber,
|
|
252
|
+
stepNumber: exerciseApp.stepNumber,
|
|
253
|
+
type: exerciseApp.type,
|
|
254
|
+
appName: exerciseApp.name,
|
|
255
|
+
fullPath: exerciseApp.fullPath,
|
|
198
256
|
},
|
|
199
|
-
|
|
200
|
-
};
|
|
257
|
+
},
|
|
258
|
+
});
|
|
201
259
|
}
|
|
202
260
|
if (nextProgress.type === 'instructions' ||
|
|
203
261
|
nextProgress.type === 'finished') {
|
|
@@ -236,43 +294,38 @@ An error will be returned if no app is found for the given arguments.
|
|
|
236
294
|
}
|
|
237
295
|
invariant(desiredApp, `No app found for values derived by the arguments: ${exerciseNumber}.${stepNumber}.${type}`);
|
|
238
296
|
await setPlayground(desiredApp.fullPath);
|
|
239
|
-
return {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
297
|
+
return createToolResponse({
|
|
298
|
+
toolName: 'set_playground',
|
|
299
|
+
summary: `Playground set to ${desiredApp.name}.`,
|
|
300
|
+
structuredContent: {
|
|
301
|
+
playground: {
|
|
302
|
+
exerciseNumber: desiredApp.exerciseNumber,
|
|
303
|
+
stepNumber: desiredApp.stepNumber,
|
|
304
|
+
type: desiredApp.type,
|
|
305
|
+
appName: desiredApp.name,
|
|
306
|
+
fullPath: desiredApp.fullPath,
|
|
244
307
|
},
|
|
245
|
-
|
|
246
|
-
};
|
|
308
|
+
},
|
|
309
|
+
});
|
|
247
310
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
.describe('The slug of the Epic lesson to mark as complete (can be retrieved from the `get_exercise_context` tool or the `get_workshop_context` tool)'),
|
|
259
|
-
complete: z
|
|
260
|
-
.boolean()
|
|
261
|
-
.optional()
|
|
262
|
-
.default(true)
|
|
263
|
-
.describe('Whether to mark the lesson as complete or incomplete (defaults to true)'),
|
|
264
|
-
},
|
|
311
|
+
registerTool(server, 'update_progress', {
|
|
312
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
313
|
+
epicLessonSlug: z
|
|
314
|
+
.string()
|
|
315
|
+
.describe('Lesson slug to update (from `get_exercise_context`, `get_workshop_context`, or `get_what_is_next`).'),
|
|
316
|
+
complete: z
|
|
317
|
+
.boolean()
|
|
318
|
+
.optional()
|
|
319
|
+
.default(true)
|
|
320
|
+
.describe('Mark complete or incomplete (default: true).'),
|
|
265
321
|
}, async ({ workshopDirectory, epicLessonSlug, complete }) => {
|
|
266
322
|
await handleWorkshopDirectory(workshopDirectory);
|
|
267
323
|
await updateProgress({ lessonSlug: epicLessonSlug, complete });
|
|
268
|
-
return {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
};
|
|
324
|
+
return createToolResponse({
|
|
325
|
+
toolName: 'update_progress',
|
|
326
|
+
summary: `Lesson "${epicLessonSlug}" marked as ${complete ? 'complete' : 'incomplete'}.`,
|
|
327
|
+
structuredContent: { epicLessonSlug, complete },
|
|
328
|
+
});
|
|
276
329
|
});
|
|
277
330
|
// TODO: add a tool to run the dev/test script for the given app
|
|
278
331
|
}
|
|
@@ -280,136 +333,125 @@ This will mark the Epic lesson as complete or incomplete and update the user's p
|
|
|
280
333
|
// accessible via tools, but allowing the LLM to access them on demand is useful
|
|
281
334
|
// for some situations.
|
|
282
335
|
export function initResourceTools(server) {
|
|
283
|
-
|
|
284
|
-
description: `
|
|
285
|
-
Indended to help you get wholistic context of the topics covered in this
|
|
286
|
-
workshop. This doesn't go into as much detail per exercise as the
|
|
287
|
-
\`get_exercise_context\` tool, but it is a good starting point to orient
|
|
288
|
-
yourself on the workshop as a whole.
|
|
289
|
-
`.trim(),
|
|
290
|
-
inputSchema: workshopContextResource.inputSchema,
|
|
291
|
-
}, async ({ workshopDirectory }) => {
|
|
336
|
+
registerTool(server, 'get_workshop_context', workshopContextResource.inputSchema, async ({ workshopDirectory }) => {
|
|
292
337
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
293
338
|
const resource = await workshopContextResource.getResource({
|
|
294
339
|
workshopDirectory,
|
|
295
340
|
});
|
|
296
|
-
|
|
341
|
+
const structured = createResourceStructuredContent(resource);
|
|
342
|
+
const data = structured.data;
|
|
343
|
+
const exerciseCount = Array.isArray(data?.exercises)
|
|
344
|
+
? data.exercises.length
|
|
345
|
+
: 0;
|
|
346
|
+
return createToolResponse({
|
|
347
|
+
toolName: 'get_workshop_context',
|
|
348
|
+
summary: 'Workshop context retrieved.',
|
|
349
|
+
details: exerciseCount ? [`Exercises: ${exerciseCount}`] : undefined,
|
|
297
350
|
content: [getEmbeddedResourceContent(resource)],
|
|
298
|
-
|
|
351
|
+
structuredContent: {
|
|
352
|
+
workshopContext: structured,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
299
355
|
});
|
|
300
|
-
|
|
301
|
-
description: `
|
|
302
|
-
Intended to help a student understand what they need to do for the current
|
|
303
|
-
exercise step.
|
|
304
|
-
|
|
305
|
-
This returns the instructions MDX content for the current exercise and each
|
|
306
|
-
exercise step. If the user is has the paid version of the workshop, it will also
|
|
307
|
-
include the transcript from each of the videos as well.
|
|
308
|
-
|
|
309
|
-
The output for this will rarely change, so it's unnecessary to call this tool
|
|
310
|
-
more than once.
|
|
311
|
-
|
|
312
|
-
\`get_exercise_context\` is often best when used with the
|
|
313
|
-
\`get_exercise_step_progress_diff\` tool to help a student understand what
|
|
314
|
-
work they still need to do and answer any questions about the exercise.
|
|
315
|
-
`.trim(),
|
|
316
|
-
inputSchema: exerciseContextResource.inputSchema,
|
|
317
|
-
}, async ({ workshopDirectory, exerciseNumber }) => {
|
|
356
|
+
registerTool(server, 'get_exercise_context', exerciseContextResource.inputSchema, async ({ workshopDirectory, exerciseNumber }) => {
|
|
318
357
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
319
358
|
const resource = await exerciseContextResource.getResource({
|
|
320
359
|
workshopDirectory,
|
|
321
360
|
exerciseNumber,
|
|
322
361
|
});
|
|
323
|
-
|
|
362
|
+
const structured = createResourceStructuredContent(resource);
|
|
363
|
+
const data = structured.data;
|
|
364
|
+
const stepCount = Array.isArray(data?.steps) ? data.steps.length : 0;
|
|
365
|
+
const number = data?.exerciseInfo?.number;
|
|
366
|
+
return createToolResponse({
|
|
367
|
+
toolName: 'get_exercise_context',
|
|
368
|
+
summary: number
|
|
369
|
+
? `Exercise ${number} context retrieved.`
|
|
370
|
+
: 'Exercise context retrieved.',
|
|
371
|
+
details: stepCount ? [`Steps: ${stepCount}`] : undefined,
|
|
324
372
|
content: [getEmbeddedResourceContent(resource)],
|
|
325
|
-
|
|
373
|
+
structuredContent: {
|
|
374
|
+
exerciseContext: structured,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
326
377
|
});
|
|
327
|
-
|
|
328
|
-
description: `
|
|
329
|
-
Intended to give context about the changes between two apps.
|
|
330
|
-
|
|
331
|
-
The output is a git diff of the playground directory as BASE (their work in
|
|
332
|
-
progress) against the solution directory as HEAD (the final state they're trying
|
|
333
|
-
to achieve).
|
|
334
|
-
|
|
335
|
-
The output is formatted as a git diff.
|
|
336
|
-
|
|
337
|
-
App IDs are formatted as \`{exerciseNumber}.{stepNumber}.{type}\`.
|
|
338
|
-
|
|
339
|
-
If the user asks for the diff for 2.3, then use 02.03.problem for app1 and 02.03.solution for app2.
|
|
340
|
-
`,
|
|
341
|
-
inputSchema: diffBetweenAppsResource.inputSchema,
|
|
342
|
-
}, async ({ workshopDirectory, app1, app2 }) => {
|
|
378
|
+
registerTool(server, 'get_diff_between_apps', diffBetweenAppsResource.inputSchema, async ({ workshopDirectory, app1, app2 }) => {
|
|
343
379
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
344
380
|
const resource = await diffBetweenAppsResource.getResource({
|
|
345
381
|
workshopDirectory,
|
|
346
382
|
app1,
|
|
347
383
|
app2,
|
|
348
384
|
});
|
|
349
|
-
|
|
385
|
+
const diff = parseResourceText(resource);
|
|
386
|
+
const diffText = typeof diff === 'string' ? diff : JSON.stringify(diff ?? '');
|
|
387
|
+
return createToolResponse({
|
|
388
|
+
toolName: 'get_diff_between_apps',
|
|
389
|
+
summary: `Diff generated for ${app1} vs ${app2}.`,
|
|
390
|
+
details: diffText
|
|
391
|
+
? [`Diff length: ${diffText.length} chars`]
|
|
392
|
+
: undefined,
|
|
350
393
|
content: [getEmbeddedResourceContent(resource)],
|
|
351
|
-
|
|
394
|
+
structuredContent: {
|
|
395
|
+
app1,
|
|
396
|
+
app2,
|
|
397
|
+
diff,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
352
400
|
});
|
|
353
|
-
|
|
354
|
-
description: `
|
|
355
|
-
Intended to help a student understand what work they still have to complete.
|
|
356
|
-
`.trim(),
|
|
357
|
-
inputSchema: exerciseStepProgressDiffResource.inputSchema,
|
|
358
|
-
}, async ({ workshopDirectory }) => {
|
|
401
|
+
registerTool(server, 'get_exercise_step_progress_diff', exerciseStepProgressDiffResource.inputSchema, async ({ workshopDirectory }) => {
|
|
359
402
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
360
403
|
const resource = await exerciseStepProgressDiffResource.getResource({
|
|
361
404
|
workshopDirectory,
|
|
362
405
|
});
|
|
363
|
-
|
|
406
|
+
const diff = parseResourceText(resource);
|
|
407
|
+
const diffText = typeof diff === 'string' ? diff : JSON.stringify(diff ?? '');
|
|
408
|
+
return createToolResponse({
|
|
409
|
+
toolName: 'get_exercise_step_progress_diff',
|
|
410
|
+
summary: 'Progress diff generated for the current step.',
|
|
411
|
+
details: diffText
|
|
412
|
+
? [`Diff length: ${diffText.length} chars`]
|
|
413
|
+
: undefined,
|
|
364
414
|
content: [
|
|
365
415
|
createText(getDiffInstructionText()),
|
|
366
416
|
getEmbeddedResourceContent(resource),
|
|
367
417
|
],
|
|
368
|
-
|
|
418
|
+
structuredContent: {
|
|
419
|
+
diff,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
369
422
|
});
|
|
370
|
-
|
|
371
|
-
description: `
|
|
372
|
-
Intended to help a student understand what they need to do for a specific
|
|
373
|
-
exercise step.
|
|
374
|
-
|
|
375
|
-
This returns the instructions MDX content for the specified exercise step's
|
|
376
|
-
problem and solution. If the user has the paid version of the workshop, it will also
|
|
377
|
-
include the transcript from each of the videos as well.
|
|
378
|
-
|
|
379
|
-
The output for this will rarely change, so it's unnecessary to call this tool
|
|
380
|
-
more than once for the same exercise step.
|
|
381
|
-
|
|
382
|
-
\`get_exercise_step_context\` is often best when used with the
|
|
383
|
-
\`get_exercise_step_progress_diff\` tool to help a student understand what
|
|
384
|
-
work they still need to do and answer any questions about the exercise step.
|
|
385
|
-
`.trim(),
|
|
386
|
-
inputSchema: exerciseStepContextResource.inputSchema,
|
|
387
|
-
}, async ({ workshopDirectory, exerciseNumber, stepNumber }) => {
|
|
423
|
+
registerTool(server, 'get_exercise_step_context', exerciseStepContextResource.inputSchema, async ({ workshopDirectory, exerciseNumber, stepNumber }) => {
|
|
388
424
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
389
425
|
const resource = await exerciseStepContextResource.getResource({
|
|
390
426
|
workshopDirectory,
|
|
391
427
|
exerciseNumber,
|
|
392
428
|
stepNumber,
|
|
393
429
|
});
|
|
394
|
-
|
|
430
|
+
const structured = createResourceStructuredContent(resource);
|
|
431
|
+
const data = structured.data;
|
|
432
|
+
const step = data?.stepInfo?.number;
|
|
433
|
+
const exercise = data?.exerciseInfo?.number;
|
|
434
|
+
return createToolResponse({
|
|
435
|
+
toolName: 'get_exercise_step_context',
|
|
436
|
+
summary: exercise && step
|
|
437
|
+
? `Exercise ${exercise} step ${step} context retrieved.`
|
|
438
|
+
: 'Exercise step context retrieved.',
|
|
395
439
|
content: [getEmbeddedResourceContent(resource)],
|
|
396
|
-
|
|
440
|
+
structuredContent: {
|
|
441
|
+
exerciseStepContext: structured,
|
|
442
|
+
},
|
|
443
|
+
});
|
|
397
444
|
});
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
If the user ever asks you to show them a video, use this tool to do so.
|
|
403
|
-
`.trim(),
|
|
404
|
-
inputSchema: {
|
|
405
|
-
videoUrl: z.string().describe(`
|
|
406
|
-
The URL of the video to view. If you don't know the URL to use already, you can get this from the \`get_exercise_step_context\` tool or the \`get_exercise_context\` tool depending on whether you're trying to show the exercise intro/outro video or a specific step's problem/solution video. If you use the \`get_what_is_next\` tool, this will be handled automatically.
|
|
407
|
-
`.trim()),
|
|
408
|
-
},
|
|
445
|
+
registerTool(server, 'view_video', {
|
|
446
|
+
videoUrl: z
|
|
447
|
+
.string()
|
|
448
|
+
.describe('Video URL from exercise context or `get_what_is_next`.'),
|
|
409
449
|
}, async ({ videoUrl }) => {
|
|
410
450
|
const url = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
411
451
|
url.searchParams.set('url', videoUrl);
|
|
412
|
-
return {
|
|
452
|
+
return createToolResponse({
|
|
453
|
+
toolName: 'view_video',
|
|
454
|
+
summary: 'Video ready in the embedded player.',
|
|
413
455
|
content: [
|
|
414
456
|
createUIResource({
|
|
415
457
|
content: {
|
|
@@ -420,16 +462,14 @@ The URL of the video to view. If you don't know the URL to use already, you can
|
|
|
420
462
|
encoding: 'text',
|
|
421
463
|
}),
|
|
422
464
|
],
|
|
423
|
-
|
|
465
|
+
structuredContent: {
|
|
466
|
+
videoUrl,
|
|
467
|
+
iframeUrl: url.toString(),
|
|
468
|
+
},
|
|
469
|
+
});
|
|
424
470
|
});
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
description: `
|
|
428
|
-
Call this to open the files for the exercise step the playground is currently set to.
|
|
429
|
-
`.trim(),
|
|
430
|
-
inputSchema: {
|
|
431
|
-
workshopDirectory: workshopDirectoryInputSchema,
|
|
432
|
-
},
|
|
471
|
+
registerTool(server, 'open_exercise_step_files', {
|
|
472
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
433
473
|
}, async ({ workshopDirectory }) => {
|
|
434
474
|
await handleWorkshopDirectory(workshopDirectory);
|
|
435
475
|
const playgroundApp = await getPlaygroundApp();
|
|
@@ -445,125 +485,126 @@ Call this to open the files for the exercise step the playground is currently se
|
|
|
445
485
|
const fullPath = path.join(playgroundApp.fullPath, file.path);
|
|
446
486
|
await launchEditor(fullPath, file.line);
|
|
447
487
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
488
|
+
const openedFiles = diffFiles.map((file) => ({
|
|
489
|
+
path: file.path,
|
|
490
|
+
line: file.line,
|
|
491
|
+
}));
|
|
492
|
+
return createToolResponse({
|
|
493
|
+
toolName: 'open_exercise_step_files',
|
|
494
|
+
summary: `Opened ${diffFiles.length} file${diffFiles.length === 1 ? '' : 's'}.`,
|
|
495
|
+
details: openedFiles.map((file) => `- ${file.path}:${file.line}`),
|
|
496
|
+
structuredContent: {
|
|
497
|
+
count: openedFiles.length,
|
|
498
|
+
files: openedFiles,
|
|
499
|
+
},
|
|
500
|
+
});
|
|
456
501
|
});
|
|
457
|
-
|
|
458
|
-
description: `
|
|
459
|
-
Intended to help you get information about the current user.
|
|
460
|
-
|
|
461
|
-
This includes the user's name, email, etc. It's mostly useful to determine
|
|
462
|
-
whether the user is logged in and know who they are.
|
|
463
|
-
|
|
464
|
-
If the user is not logged in, tell them to log in by running the \`login\` tool.
|
|
465
|
-
`.trim(),
|
|
466
|
-
inputSchema: userInfoResource.inputSchema,
|
|
467
|
-
}, async ({ workshopDirectory }) => {
|
|
502
|
+
registerTool(server, 'get_user_info', userInfoResource.inputSchema, async ({ workshopDirectory }) => {
|
|
468
503
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
469
504
|
const resource = await userInfoResource.getResource({ workshopDirectory });
|
|
470
|
-
|
|
505
|
+
const userInfoRaw = parseResourceText(resource);
|
|
506
|
+
const userInfo = userInfoRaw && typeof userInfoRaw === 'object'
|
|
507
|
+
? userInfoRaw
|
|
508
|
+
: null;
|
|
509
|
+
const summary = userInfo
|
|
510
|
+
? `User info retrieved for ${userInfo.email ?? 'unknown email'}.`
|
|
511
|
+
: 'No authenticated user found.';
|
|
512
|
+
return createToolResponse({
|
|
513
|
+
toolName: 'get_user_info',
|
|
514
|
+
summary,
|
|
515
|
+
statusEmoji: userInfo ? '✅' : '⚠️',
|
|
471
516
|
content: [getEmbeddedResourceContent(resource)],
|
|
472
|
-
|
|
517
|
+
structuredContent: {
|
|
518
|
+
userInfo,
|
|
519
|
+
},
|
|
520
|
+
});
|
|
473
521
|
});
|
|
474
|
-
|
|
475
|
-
description: `
|
|
476
|
-
Will tell you whether the user has access to the paid features of the workshop.
|
|
477
|
-
|
|
478
|
-
Paid features include:
|
|
479
|
-
- Transcripts
|
|
480
|
-
- Progress tracking
|
|
481
|
-
- Access to videos
|
|
482
|
-
- Access to the discord chat
|
|
483
|
-
- Test tab support
|
|
484
|
-
- Diff tab support
|
|
485
|
-
|
|
486
|
-
Encourage the user to upgrade if they need access to the paid features.
|
|
487
|
-
`.trim(),
|
|
488
|
-
inputSchema: userAccessResource.inputSchema,
|
|
489
|
-
}, async ({ workshopDirectory }) => {
|
|
522
|
+
registerTool(server, 'get_user_access', userAccessResource.inputSchema, async ({ workshopDirectory }) => {
|
|
490
523
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
491
524
|
const resource = await userAccessResource.getResource({
|
|
492
525
|
workshopDirectory,
|
|
493
526
|
});
|
|
494
|
-
|
|
527
|
+
const access = parseResourceText(resource);
|
|
528
|
+
const userHasAccess = typeof access?.userHasAccess === 'boolean'
|
|
529
|
+
? access.userHasAccess
|
|
530
|
+
: undefined;
|
|
531
|
+
const statusEmoji = userHasAccess === false ? '⚠️' : '✅';
|
|
532
|
+
return createToolResponse({
|
|
533
|
+
toolName: 'get_user_access',
|
|
534
|
+
summary: typeof userHasAccess === 'boolean'
|
|
535
|
+
? `User access: ${userHasAccess ? 'paid' : 'free'}`
|
|
536
|
+
: 'User access retrieved.',
|
|
537
|
+
statusEmoji,
|
|
495
538
|
content: [getEmbeddedResourceContent(resource)],
|
|
496
|
-
|
|
539
|
+
structuredContent: {
|
|
540
|
+
userHasAccess,
|
|
541
|
+
},
|
|
542
|
+
});
|
|
497
543
|
});
|
|
498
|
-
|
|
499
|
-
description: `
|
|
500
|
-
Intended to help you get the progress of the current user. Can often be helpful
|
|
501
|
-
to know what the next step that needs to be completed is. Make sure to provide
|
|
502
|
-
the user with the URL of relevant incomplete lessons so they can watch them and
|
|
503
|
-
then mark them as complete.
|
|
504
|
-
`.trim(),
|
|
505
|
-
inputSchema: userProgressResource.inputSchema,
|
|
506
|
-
}, async ({ workshopDirectory }) => {
|
|
544
|
+
registerTool(server, 'get_user_progress', userProgressResource.inputSchema, async ({ workshopDirectory }) => {
|
|
507
545
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
508
546
|
const resource = await userProgressResource.getResource({
|
|
509
547
|
workshopDirectory,
|
|
510
548
|
});
|
|
511
|
-
|
|
549
|
+
const progress = parseResourceText(resource);
|
|
550
|
+
const items = Array.isArray(progress)
|
|
551
|
+
? progress
|
|
552
|
+
: [];
|
|
553
|
+
const incompleteCount = items.filter((item) => !item.epicCompletedAt).length;
|
|
554
|
+
return createToolResponse({
|
|
555
|
+
toolName: 'get_user_progress',
|
|
556
|
+
summary: `Progress retrieved. Incomplete items: ${incompleteCount}.`,
|
|
512
557
|
content: [getEmbeddedResourceContent(resource)],
|
|
513
|
-
|
|
558
|
+
structuredContent: {
|
|
559
|
+
progress: items,
|
|
560
|
+
incompleteCount,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
514
563
|
});
|
|
515
564
|
}
|
|
516
565
|
// Sometimes the user will ask the LLM to select a prompt to use so they don't have to.
|
|
517
566
|
export function initPromptTools(server) {
|
|
518
|
-
|
|
519
|
-
description: `
|
|
520
|
-
If the user asks you to quiz them on a topic from the workshop, use this tool to
|
|
521
|
-
retrieve the instructions for how to do so.
|
|
522
|
-
|
|
523
|
-
- If the user asks for a specific exercise, supply that exercise number.
|
|
524
|
-
- If they ask for a specific exericse, supply that exercise number.
|
|
525
|
-
- If they ask for a topic and you don't know which exercise that topic is in, use \`get_workshop_context\` to get the list of exercises and their topics and then supply the appropriate exercise number.
|
|
526
|
-
`.trim(),
|
|
527
|
-
inputSchema: quizMeInputSchema,
|
|
528
|
-
}, async ({ workshopDirectory, exerciseNumber }) => {
|
|
567
|
+
registerTool(server, 'get_quiz_instructions', quizMeInputSchema, async ({ workshopDirectory, exerciseNumber }) => {
|
|
529
568
|
workshopDirectory = await handleWorkshopDirectory(workshopDirectory);
|
|
530
569
|
const result = await quizMe({ workshopDirectory, exerciseNumber });
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
570
|
+
const promptContent = result.messages.map((m) => {
|
|
571
|
+
if (m.content.type === 'resource') {
|
|
572
|
+
return getEmbeddedResourceContent(m.content.resource);
|
|
573
|
+
}
|
|
574
|
+
return m.content;
|
|
575
|
+
});
|
|
576
|
+
return createToolResponse({
|
|
577
|
+
toolName: 'get_quiz_instructions',
|
|
578
|
+
summary: 'Quiz instructions prepared.',
|
|
579
|
+
details: [
|
|
580
|
+
exerciseNumber
|
|
581
|
+
? `Exercise: ${exerciseNumber}`
|
|
582
|
+
: 'Exercise: random selection',
|
|
583
|
+
],
|
|
584
|
+
content: promptContent,
|
|
585
|
+
structuredContent: {
|
|
586
|
+
exerciseNumber: exerciseNumber ?? null,
|
|
587
|
+
messages: result.messages,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
542
590
|
});
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
description: `
|
|
546
|
-
Intended to help you get the next step that the user needs to complete.
|
|
547
|
-
|
|
548
|
-
This is often useful to know what the user should do next to continue their learning.
|
|
549
|
-
|
|
550
|
-
This could be that they need to login, watch a video, complete an exercise, etc.
|
|
551
|
-
`.trim(),
|
|
552
|
-
inputSchema: {
|
|
553
|
-
workshopDirectory: workshopDirectoryInputSchema,
|
|
554
|
-
},
|
|
591
|
+
registerTool(server, 'get_what_is_next', {
|
|
592
|
+
workshopDirectory: workshopDirectoryInputSchema,
|
|
555
593
|
}, async ({ workshopDirectory }) => {
|
|
556
594
|
await handleWorkshopDirectory(workshopDirectory);
|
|
557
595
|
const authInfo = await getAuthInfo();
|
|
558
596
|
if (!authInfo) {
|
|
559
|
-
return {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
597
|
+
return createToolResponse({
|
|
598
|
+
toolName: 'get_what_is_next',
|
|
599
|
+
summary: 'User is not logged in.',
|
|
600
|
+
statusEmoji: '⚠️',
|
|
601
|
+
details: ['Use `login` to authenticate the user.'],
|
|
602
|
+
nextSteps: ['Call `login` to start device authorization.'],
|
|
603
|
+
includeMetaNextSteps: false,
|
|
604
|
+
structuredContent: {
|
|
605
|
+
status: 'not_authenticated',
|
|
606
|
+
},
|
|
607
|
+
});
|
|
567
608
|
}
|
|
568
609
|
const progress = await getProgress();
|
|
569
610
|
const scoreProgress = (a) => {
|
|
@@ -586,21 +627,25 @@ This could be that they need to login, watch a video, complete an exercise, etc.
|
|
|
586
627
|
});
|
|
587
628
|
const nextProgress = sortedProgress.find((p) => !p.epicCompletedAt);
|
|
588
629
|
if (!nextProgress) {
|
|
589
|
-
return {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
630
|
+
return createToolResponse({
|
|
631
|
+
toolName: 'get_what_is_next',
|
|
632
|
+
summary: 'Workshop complete.',
|
|
633
|
+
details: ['Invite the user to request a quiz on the material.'],
|
|
634
|
+
includeMetaNextSteps: false,
|
|
635
|
+
content: [createText(await createWorkshopSummary())],
|
|
636
|
+
structuredContent: {
|
|
637
|
+
status: 'complete',
|
|
638
|
+
},
|
|
639
|
+
});
|
|
598
640
|
}
|
|
599
641
|
invariant(nextProgress.type !== 'unknown', `Next progress type is unknown. This is unexpected. Sorry, we don't know what to do here. Here's a summary of the workshop:\n\n${await createWorkshopSummary()}`);
|
|
600
642
|
if (nextProgress.type === 'workshop-instructions') {
|
|
601
643
|
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
602
644
|
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
603
|
-
return {
|
|
645
|
+
return createToolResponse({
|
|
646
|
+
toolName: 'get_what_is_next',
|
|
647
|
+
summary: 'User should complete workshop instructions.',
|
|
648
|
+
includeMetaNextSteps: false,
|
|
604
649
|
content: [
|
|
605
650
|
createText(`The user has just begun! They need to watch the workshop instructions video and read the instructions to get started. When they say they're done or ready for what's next, mark it as complete using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}" and then call \`get_what_is_next\` again to get the next step. Relevant info is below:`),
|
|
606
651
|
createText(await createWorkshopSummary()),
|
|
@@ -614,12 +659,22 @@ This could be that they need to login, watch a video, complete an exercise, etc.
|
|
|
614
659
|
},
|
|
615
660
|
}),
|
|
616
661
|
],
|
|
617
|
-
|
|
662
|
+
structuredContent: {
|
|
663
|
+
nextStep: {
|
|
664
|
+
type: nextProgress.type,
|
|
665
|
+
epicLessonSlug: nextProgress.epicLessonSlug,
|
|
666
|
+
epicLessonUrl: nextProgress.epicLessonUrl,
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
});
|
|
618
670
|
}
|
|
619
671
|
if (nextProgress.type === 'workshop-finished') {
|
|
620
672
|
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
621
673
|
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
622
|
-
return {
|
|
674
|
+
return createToolResponse({
|
|
675
|
+
toolName: 'get_what_is_next',
|
|
676
|
+
summary: 'User should complete workshop finished instructions.',
|
|
677
|
+
includeMetaNextSteps: false,
|
|
623
678
|
content: [
|
|
624
679
|
createText(`The user has almost completed the workshop. They just need to watch the workshop finished video and read the finished instructions to get started. When they say they're done or ready for what's next, mark it as complete using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}" and then call \`get_what_is_next\` again to get the next step. Relevant info is below:`),
|
|
625
680
|
createText(`Finished instructions:\n${await readInWorkshop('exercises', 'FINISHED.mdx')}`),
|
|
@@ -632,14 +687,24 @@ This could be that they need to login, watch a video, complete an exercise, etc.
|
|
|
632
687
|
},
|
|
633
688
|
}),
|
|
634
689
|
],
|
|
635
|
-
|
|
690
|
+
structuredContent: {
|
|
691
|
+
nextStep: {
|
|
692
|
+
type: nextProgress.type,
|
|
693
|
+
epicLessonSlug: nextProgress.epicLessonSlug,
|
|
694
|
+
epicLessonUrl: nextProgress.epicLessonUrl,
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
});
|
|
636
698
|
}
|
|
637
699
|
const ex = nextProgress.exerciseNumber.toString().padStart(2, '0');
|
|
638
700
|
if (nextProgress.type === 'instructions') {
|
|
639
701
|
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
640
702
|
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
641
703
|
const exercise = await getExercise(nextProgress.exerciseNumber);
|
|
642
|
-
return {
|
|
704
|
+
return createToolResponse({
|
|
705
|
+
toolName: 'get_what_is_next',
|
|
706
|
+
summary: `User should complete exercise ${ex} intro.`,
|
|
707
|
+
includeMetaNextSteps: false,
|
|
643
708
|
content: [
|
|
644
709
|
createText(`The user needs to complete the intro for exercise ${ex}. When they say they're done or ready for what's next, mark it as complete using the \`update_progress\` tool with the slug "${nextProgress.epicLessonSlug}" and then call \`get_what_is_next\` again to get the next step. Relevant info is below:`),
|
|
645
710
|
createText(`Exercise instructions:\n${await readReadme(exercise?.fullPath)}`),
|
|
@@ -652,13 +717,24 @@ This could be that they need to login, watch a video, complete an exercise, etc.
|
|
|
652
717
|
},
|
|
653
718
|
}),
|
|
654
719
|
],
|
|
655
|
-
|
|
720
|
+
structuredContent: {
|
|
721
|
+
nextStep: {
|
|
722
|
+
type: nextProgress.type,
|
|
723
|
+
exerciseNumber: nextProgress.exerciseNumber,
|
|
724
|
+
epicLessonSlug: nextProgress.epicLessonSlug,
|
|
725
|
+
epicLessonUrl: nextProgress.epicLessonUrl,
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
});
|
|
656
729
|
}
|
|
657
730
|
if (nextProgress.type === 'finished') {
|
|
658
731
|
const embedUrl = new URL('mcp-ui/epic-video', 'http://localhost:5639');
|
|
659
732
|
embedUrl.searchParams.set('url', nextProgress.epicLessonUrl);
|
|
660
733
|
const exercise = await getExercise(nextProgress.exerciseNumber);
|
|
661
|
-
return {
|
|
734
|
+
return createToolResponse({
|
|
735
|
+
toolName: 'get_what_is_next',
|
|
736
|
+
summary: `User should complete exercise ${ex} outro.`,
|
|
737
|
+
includeMetaNextSteps: false,
|
|
662
738
|
content: [
|
|
663
739
|
createText(`The user is almost finished with exercise ${ex}. They need to complete the outro for exercise ${ex}. Relevant info is below:`),
|
|
664
740
|
createText(`Exercise finished instructions:\n${await readReadme(exercise?.fullPath)}`),
|
|
@@ -671,7 +747,15 @@ This could be that they need to login, watch a video, complete an exercise, etc.
|
|
|
671
747
|
},
|
|
672
748
|
}),
|
|
673
749
|
],
|
|
674
|
-
|
|
750
|
+
structuredContent: {
|
|
751
|
+
nextStep: {
|
|
752
|
+
type: nextProgress.type,
|
|
753
|
+
exerciseNumber: nextProgress.exerciseNumber,
|
|
754
|
+
epicLessonSlug: nextProgress.epicLessonSlug,
|
|
755
|
+
epicLessonUrl: nextProgress.epicLessonUrl,
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
});
|
|
675
759
|
}
|
|
676
760
|
const st = nextProgress.stepNumber.toString().padStart(2, '0');
|
|
677
761
|
if (nextProgress.type === 'step') {
|
|
@@ -682,7 +766,10 @@ This could be that they need to login, watch a video, complete an exercise, etc.
|
|
|
682
766
|
solutionEmbedUrl.searchParams.set('url', `${nextProgress.epicLessonUrl}/solution`);
|
|
683
767
|
const step = exercise?.steps.find((s) => s.stepNumber === nextProgress.stepNumber);
|
|
684
768
|
invariant(step, `No step found for exercise ${nextProgress.exerciseNumber} step ${nextProgress.stepNumber}`);
|
|
685
|
-
return {
|
|
769
|
+
return createToolResponse({
|
|
770
|
+
toolName: 'get_what_is_next',
|
|
771
|
+
summary: `User is on step ${st} of exercise ${ex}.`,
|
|
772
|
+
includeMetaNextSteps: false,
|
|
686
773
|
content: [
|
|
687
774
|
createText(`
|
|
688
775
|
The user is on step ${st} of exercise ${ex}. To complete this step they need to:
|
|
@@ -716,7 +803,19 @@ Then you can call \`get_what_is_next\` again to get the next step.
|
|
|
716
803
|
},
|
|
717
804
|
}),
|
|
718
805
|
],
|
|
719
|
-
|
|
806
|
+
structuredContent: {
|
|
807
|
+
nextStep: {
|
|
808
|
+
type: nextProgress.type,
|
|
809
|
+
exerciseNumber: nextProgress.exerciseNumber,
|
|
810
|
+
stepNumber: nextProgress.stepNumber,
|
|
811
|
+
epicLessonSlug: nextProgress.epicLessonSlug,
|
|
812
|
+
videoUrls: {
|
|
813
|
+
problem: nextProgress.epicLessonUrl,
|
|
814
|
+
solution: `${nextProgress.epicLessonUrl}/solution`,
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
},
|
|
818
|
+
});
|
|
720
819
|
}
|
|
721
820
|
throw new Error(`This is unexpected, but I do not know what the next step for the user is. Sorry!`);
|
|
722
821
|
});
|