add dbiz, add history

This commit is contained in:
2026-04-13 13:45:18 +07:00
parent 409706457a
commit 77a0e38fa7
8 changed files with 415 additions and 54 deletions

View File

@@ -0,0 +1,92 @@
// Supabase Edge Function: writing-check-dbiz
// Uses DBIZ LLM API (qwen-35b, OpenAI-compatible) to analyze English writing.
// Deploy: supabase functions deploy writing-check-dbiz --no-verify-jwt
// Secrets: supabase secrets set DBIZ_API_KEY=<your_key>
import OpenAI from "npm:openai@^4"
const dbiz = new OpenAI({
apiKey: Deno.env.get("DBIZ_API_KEY") ?? "",
baseURL: "https://ai-api.dbiz.com/v1",
})
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
}
// Structure enforced via system prompt — qwen-35b via LiteLLM drops response_format params.
const SYSTEM_PROMPT = `You are an expert English writing teacher specialising in TOEIC and IELTS assessment.
Analyse the student's writing and respond ONLY with valid JSON — no markdown, no extra text:
{
"score": "<estimated band score, e.g. 6.5>",
"grammar": ["<issue 1 with correction, mix English example + Vietnamese explanation>"],
"vocabulary": ["<vocabulary observation in Vietnamese>"],
"structure": "<23 sentence structure assessment in Vietnamese>",
"improved_version": "<the full improved text in English>",
"summary": "<23 sentence overall assessment in Vietnamese>"
}
All string values must use double quotes. Do not use single quotes or unquoted keys.`
Deno.serve(async (req: Request) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: CORS_HEADERS })
}
try {
const { content } = await req.json() as { content: string }
if (!content || content.trim().length < 10) {
return new Response(
JSON.stringify({ error: "Bài viết quá ngắn. Vui lòng nhập ít nhất 10 ký tự." }),
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
)
}
const stream = await dbiz.chat.completions.create({
model: "qwen-35b",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: `Analyse this writing:\n\n${content.slice(0, 2000)}` },
],
temperature: 0.3,
max_tokens: 10000,
stream: true,
})
const encoder = new TextEncoder()
const body = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content ?? ""
if (text) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`))
}
}
} catch (err) {
console.error("writing-check-dbiz stream error:", err)
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." })}\n\n`),
)
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
},
})
return new Response(body, {
headers: {
...CORS_HEADERS,
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
})
} catch (err) {
console.error("writing-check-dbiz error:", err)
return new Response(
JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." }),
{ status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
)
}
})

View File

@@ -1,6 +1,6 @@
// Supabase Edge Function: writing-check
// Uses GLM API (OpenAI-compatible) to analyze English writing submissions.
// Deploy: supabase functions deploy writing-check
// Deploy: supabase functions deploy writing-check --no-verify-jwt
// Secrets: supabase secrets set GLM_API_KEY=<your_key>
import OpenAI from "npm:openai@^4"
@@ -13,10 +13,8 @@ const glm = new OpenAI({
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
"Content-Type": "application/json",
}
// Instructs the model to return a strict JSON structure with Vietnamese feedback.
const SYSTEM_PROMPT = `You are an expert English writing teacher specialising in TOEIC and IELTS assessment.
Analyse the student's writing and respond ONLY with valid JSON — no markdown, no extra text:
{
@@ -29,7 +27,6 @@ Analyse the student's writing and respond ONLY with valid JSON — no markdown,
}`
Deno.serve(async (req: Request) => {
// Handle CORS pre-flight
if (req.method === "OPTIONS") {
return new Response("ok", { headers: CORS_HEADERS })
}
@@ -40,13 +37,11 @@ Deno.serve(async (req: Request) => {
if (!content || content.trim().length < 10) {
return new Response(
JSON.stringify({ error: "Bài viết quá ngắn. Vui lòng nhập ít nhất 10 ký tự." }),
{ status: 400, headers: CORS_HEADERS },
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
)
}
const completion = await glm.chat.completions.create({
// GLM-4-32B-0414-128K: cheapest paid model at $0.1/$0.1 per 1M tokens.
// Override via: supabase secrets set GLM_MODEL=<other-model>
const stream = await glm.chat.completions.create({
model: Deno.env.get("GLM_MODEL") ?? "GLM-4.5-Flash",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
@@ -54,20 +49,42 @@ Deno.serve(async (req: Request) => {
],
temperature: 0.3,
max_tokens: 1500,
stream: true,
})
const raw = completion.choices[0]?.message?.content ?? "{}"
const encoder = new TextEncoder()
const body = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content ?? ""
if (text) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`))
}
}
} catch (err) {
console.error("writing-check stream error:", err)
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." })}\n\n`),
)
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
},
})
// Strip markdown code fences if the model adds them despite instructions
const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "").trim()
const feedback = JSON.parse(cleaned)
return new Response(JSON.stringify(feedback), { headers: CORS_HEADERS })
return new Response(body, {
headers: {
...CORS_HEADERS,
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
})
} catch (err) {
console.error("writing-check error:", err)
return new Response(
JSON.stringify({ error: "Đã có lỗi khi chấm bài. Vui lòng thử lại." }),
{ status: 500, headers: CORS_HEADERS },
{ status: 500, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
)
}
})