feat: productionize learning engine and classroom
This commit is contained in:
7
OpenMAIC/app/api/runtime/v1/runs/[runId]/cancel/route.ts
Normal file
7
OpenMAIC/app/api/runtime/v1/runs/[runId]/cancel/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { authorizeLearningRuntime, runtimeUnauthorized } from '@/lib/makelore-runtime/auth';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!authorizeLearningRuntime(request)) return runtimeUnauthorized();
|
||||
return Response.json({ status: 'not_supported', message: 'The current turn finishes independently.' });
|
||||
}
|
||||
94
OpenMAIC/app/api/runtime/v1/runs/route.ts
Normal file
94
OpenMAIC/app/api/runtime/v1/runs/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { authorizeLearningRuntime, runtimeUnauthorized } from '@/lib/makelore-runtime/auth';
|
||||
import {
|
||||
courseKnowledge,
|
||||
loadMakeloreCourse,
|
||||
parseLearningBinding,
|
||||
resolveMakeloreCourseModule,
|
||||
runtimeSceneId,
|
||||
} from '@/lib/makelore-runtime/course-store';
|
||||
import { isRuntimeRunRequest } from '@/lib/makelore-runtime/protocol';
|
||||
import { retrieveChunks } from '@/lib/qa/knowledge';
|
||||
import { runQaAgent, type QaTurn } from '@/lib/qa/agent';
|
||||
import { resolveModel } from '@/lib/server/resolve-model';
|
||||
|
||||
export const maxDuration = 120;
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!authorizeLearningRuntime(request)) return runtimeUnauthorized();
|
||||
const body: unknown = await request.json().catch(() => null);
|
||||
if (!isRuntimeRunRequest(body)) return Response.json({ error: 'invalid_request' }, { status: 400 });
|
||||
const binding = parseLearningBinding(body.binding.key);
|
||||
if (!binding) return Response.json({ error: 'invalid_binding' }, { status: 400 });
|
||||
const course = await loadMakeloreCourse(binding.courseId, binding.contentHash);
|
||||
if (!course) return Response.json({ error: 'course_not_found' }, { status: 404 });
|
||||
const module = await resolveMakeloreCourseModule(course, {
|
||||
moduleId: body.input.anchor?.moduleId,
|
||||
moduleContentHash: body.input.anchor?.moduleContentHash,
|
||||
});
|
||||
if (!module) return Response.json({ error: 'course_module_not_found' }, { status: 404 });
|
||||
|
||||
const knowledge = courseKnowledge(module);
|
||||
const message = body.input.message!.trim().slice(0, 4000);
|
||||
const retrieved = retrieveChunks(knowledge.knowledge, message, 3);
|
||||
const anchor = body.input.anchor;
|
||||
const sceneIndex = module.manifest?.scenes.findIndex((scene, index) =>
|
||||
(Boolean(anchor?.sceneId) &&
|
||||
anchor?.sceneId === runtimeSceneId(course, module.descriptor, index)) ||
|
||||
(anchor?.sceneOrder !== undefined && scene.order === anchor.sceneOrder),
|
||||
) ?? -1;
|
||||
const anchored = sceneIndex >= 0
|
||||
? knowledge.knowledge.scenes[sceneIndex]
|
||||
: knowledge.knowledge.scenes.find((scene) =>
|
||||
(anchor?.sceneOrder !== undefined && scene.order === anchor.sceneOrder) ||
|
||||
(Boolean(anchor?.sceneId) && scene.sceneId === anchor?.sceneId),
|
||||
);
|
||||
const chunks = anchored && !retrieved.some((chunk) => chunk.sceneId === anchored.sceneId)
|
||||
? [{ ...anchored, score: Number.MAX_SAFE_INTEGER }, ...retrieved].slice(0, 3)
|
||||
: retrieved;
|
||||
const history: QaTurn[] = (body.input.history ?? [])
|
||||
.filter((turn): turn is QaTurn => Boolean(turn)
|
||||
&& (turn.role === 'user' || turn.role === 'assistant')
|
||||
&& typeof turn.content === 'string')
|
||||
.slice(-7)
|
||||
.map((turn) => ({ role: turn.role, content: turn.content.slice(0, 4000) }));
|
||||
history.push({ role: 'user', content: message });
|
||||
const resolved = await resolveModel({ stage: 'qa-assistant' });
|
||||
const encoder = new TextEncoder();
|
||||
let sequence = 0;
|
||||
const event = (type: string, payload: Record<string, unknown>, terminal = false) => `${JSON.stringify({
|
||||
event_id: `${body.run_id}:${++sequence}`,
|
||||
type,
|
||||
payload,
|
||||
schema_version: 1,
|
||||
terminal,
|
||||
...(terminal ? { outcome: 'completed' } : {}),
|
||||
})}\n`;
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const update of runQaAgent({ model: resolved.model, courseware: knowledge, chunks, thinkingConfig: resolved.thinkingConfig }, history)) {
|
||||
if (update.type === 'text') {
|
||||
controller.enqueue(encoder.encode(event('learning.assistant.delta', { delta: update.delta })));
|
||||
} else if (update.type === 'done') {
|
||||
controller.enqueue(encoder.encode(event('learning.assistant.completed', {
|
||||
sources: update.sources.map((source) => ({ sceneId: source.sceneId, order: source.order, title: source.title })),
|
||||
}, true)));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
controller.enqueue(encoder.encode(event('learning.assistant.failed', { code: 'learning_runtime_failed', message: '助教暂时无法回答' }, true)));
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-ndjson; charset=utf-8',
|
||||
'Content-Encoding': 'identity',
|
||||
'Cache-Control': 'no-store, no-transform',
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user