feat: add AI grading test endpoint and update connection logic
Test / backend (push) Successful in 13s
Test / frontend (push) Successful in 24s
Release Pipeline / build-and-release (push) Successful in 5m26s

This commit is contained in:
LaurensBartelds committed 2026-05-02 13:46:49 +02:00
1 parent 993265318b
commit 201d21a678
4 files changed
+52 -16

No files matched your search

+26
View File
@@ -1,4 +1,5 @@
import { Router } from 'express' import { Router } from 'express'
import { z } from 'zod'
import { validateBody, validateQuery, validateParams } from '../middleware/validate.js' import { validateBody, validateQuery, validateParams } from '../middleware/validate.js'
import { asyncHandler } from '../utils/asyncHandler.js' import { asyncHandler } from '../utils/asyncHandler.js'
import { requireAuth, requireAdmin } from '../middleware/auth.js' import { requireAuth, requireAdmin } from '../middleware/auth.js'
@@ -7,6 +8,7 @@ import * as userRepo from '../repositories/userRepo.js'
import * as resultRepo from '../repositories/resultRepo.js' import * as resultRepo from '../repositories/resultRepo.js'
import * as settingsRepo from '../repositories/settingsRepo.js' import * as settingsRepo from '../repositories/settingsRepo.js'
import { updateUserRoleSchema, updateSettingSchema, updateSettingsBatchSchema, listUsersQuerySchema, userIdParamSchema } from '../validators/adminSchemas.js' import { updateUserRoleSchema, updateSettingSchema, updateSettingsBatchSchema, listUsersQuerySchema, userIdParamSchema } from '../validators/adminSchemas.js'
import { gradeQuestionWithAI } from '../services/aiGrading.js'
import type { Request } from 'express' import type { Request } from 'express'
const router = Router() const router = Router()
@@ -140,4 +142,28 @@ router.put(
}) })
) )
// POST /api/admin/test-ai
router.post(
'/test-ai',
validateBody(z.object({
provider: z.string().min(1),
apiKey: z.string().min(1),
model: z.string().min(1),
baseURL: z.string().optional(),
})),
asyncHandler(async (req: Request, res) => {
const { provider, apiKey, model, baseURL } = req.body
const result = await gradeQuestionWithAI(
{
type: 'short-answer',
question: { prompt: 'What is 2+2?', answer: '4' },
answer: '4',
language: 'en',
},
{ provider, apiKey, model, baseURL },
)
res.json(result)
}),
)
export default router export default router
+10 -7
View File
@@ -14,12 +14,12 @@ interface GradeRequest {
language: string language: string
} }
function getClient(): OpenAI | null { function getClient(override?: { provider?: string; apiKey?: string; baseURL?: string }): OpenAI | null {
const provider = getSetting('ai_provider') const provider = override?.provider ?? getSetting('ai_provider')
const apiKey = getSetting('ai_api_key') const apiKey = override?.apiKey ?? getSetting('ai_api_key')
if (!provider || !apiKey) return null if (!provider || !apiKey) return null
const customBase = getSetting('ai_base_url') const customBase = override?.baseURL ?? getSetting('ai_base_url')
const baseURL = provider === 'openai' ? undefined : (customBase || undefined) const baseURL = provider === 'openai' ? undefined : (customBase || undefined)
return new OpenAI({ apiKey, baseURL }) return new OpenAI({ apiKey, baseURL })
} }
@@ -141,13 +141,16 @@ Use the human-readable text labels in the arrays.`
} }
} }
export async function gradeQuestionWithAI(req: GradeRequest): Promise<GradingResult> { export async function gradeQuestionWithAI(
const client = getClient() req: GradeRequest,
override?: { provider?: string; apiKey?: string; baseURL?: string; model?: string },
): Promise<GradingResult> {
const client = getClient(override)
if (!client) { if (!client) {
throw new Error('AI grading is not configured. Please set provider and API key in admin settings.') throw new Error('AI grading is not configured. Please set provider and API key in admin settings.')
} }
const model = getSetting('ai_model') || 'gpt-4o-mini' const model = (override?.model ?? getSetting('ai_model')) || 'gpt-4o-mini'
const prompt = buildPrompt(req) const prompt = buildPrompt(req)
const completion = await client.chat.completions.create({ const completion = await client.chat.completions.create({
+10 -8
View File
@@ -96,14 +96,16 @@ export function sendTestEmail(to: string | undefined, smtp: SmtpConfig): Promise
}) })
} }
export function testAiConnection(): Promise<GradingResult> { export interface AiTestPayload {
return apiFetch('/api/grade', { provider: string
apiKey: string
model: string
baseURL?: string
}
export function testAiConnection(payload: AiTestPayload): Promise<GradingResult> {
return apiFetch('/api/admin/test-ai', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify(payload),
type: 'short-answer',
question: { prompt: 'What is 2+2?', answer: '4' },
answer: '4',
language: 'en',
}),
}) })
} }
+6 -1
View File
@@ -135,7 +135,12 @@ export default function AdminDashboard({ onExit }: Readonly<Props>) {
setTestAiSending(true); setTestAiSending(true);
setTestAiMsg(null); setTestAiMsg(null);
try { try {
await adminApi.testAiConnection(); await adminApi.testAiConnection({
provider: settingsDraft.ai_provider,
apiKey: settingsDraft.ai_api_key,
model: settingsDraft.ai_model || 'gpt-4o-mini',
baseURL: settingsDraft.ai_base_url || undefined,
});
setTestAiMsg({ ok: true, text: t("admin.aiGrading.testSuccess") }); setTestAiMsg({ ok: true, text: t("admin.aiGrading.testSuccess") });
} catch (e) { } catch (e) {
setTestAiMsg({ ok: false, text: (e as Error).message }); setTestAiMsg({ ok: false, text: (e as Error).message });