Skip to content

Commit 29421fb

Browse files
committed
Merge branch 'feature/distillation-pipeline' into fine-tuning/student-explainability
# Conflicts: # .gitignore # codebenders-dashboard/app/api/students/[guid]/sis-link/route.ts
2 parents 0c4c420 + cd7b42b commit 29421fb

27 files changed

Lines changed: 3272 additions & 21 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,6 @@ operations/verify_institution_id.py
186186

187187
# Test upload fixtures (generated — do not commit)
188188
data/test_uploads/
189+
190+
# Training pipeline artifacts
191+
training_data/

codebenders-dashboard/app/api/courses/explain-pairing/route.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { type NextRequest, NextResponse } from "next/server"
22
import { getPool } from "@/lib/db"
33
import { canAccess, type Role } from "@/lib/roles"
4-
import { generateText } from "ai"
5-
import { createOpenAI } from "@ai-sdk/openai"
6-
7-
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY || "" })
4+
import { generateExplanation } from "@/lib/model-client"
85

96
const DELIVERY_LABELS: Record<string, string> = {
107
F: "Face-to-Face",
@@ -18,7 +15,7 @@ export async function POST(request: NextRequest) {
1815
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
1916
}
2017

21-
if (!process.env.OPENAI_API_KEY) {
18+
if (process.env.MODEL_BACKEND !== "ollama" && !process.env.OPENAI_API_KEY) {
2219
return NextResponse.json({ error: "OpenAI API key not configured" }, { status: 500 })
2320
}
2421

@@ -189,11 +186,7 @@ Write a concise analysis (3-4 sentences) that:
189186
190187
Be practical and data-driven. Do not speculate beyond what the numbers show.`
191188

192-
const result = await generateText({
193-
model: openai("gpt-4o-mini"),
194-
prompt: llmPrompt,
195-
maxOutputTokens: 320,
196-
})
189+
const result = { text: await generateExplanation(llmPrompt, 320) }
197190

198191
return NextResponse.json({ stats, explanation: result.text })
199192
} catch (error) {

codebenders-dashboard/app/api/query-summary/route.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import { type NextRequest, NextResponse } from "next/server"
22
import { canAccess, type Role } from "@/lib/roles"
3-
import { generateText } from "ai"
4-
import { createOpenAI } from "@ai-sdk/openai"
5-
6-
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY || "" })
3+
import { generateSummary } from "@/lib/model-client"
74

85
export async function POST(request: NextRequest) {
96
const role = request.headers.get("x-user-role") as Role | null
107
if (!role || !canAccess("/api/query-summary", role)) {
118
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
129
}
1310

14-
if (!process.env.OPENAI_API_KEY) {
11+
if (process.env.MODEL_BACKEND !== "ollama" && !process.env.OPENAI_API_KEY) {
1512
return NextResponse.json({ error: "OpenAI API key not configured" }, { status: 500 })
1613
}
1714

@@ -47,12 +44,8 @@ ${JSON.stringify(sampleRows, null, 2)}
4744
Write a 2-3 sentence plain-English summary of what these results show. Be specific about the numbers. Do not speculate beyond the data. Address the advisor directly.`
4845

4946
try {
50-
const result = await generateText({
51-
model: openai("gpt-4o-mini"),
52-
prompt: llmPrompt,
53-
maxOutputTokens: 200,
54-
})
55-
return NextResponse.json({ summary: result.text })
47+
const summary = await generateSummary(llmPrompt, 200)
48+
return NextResponse.json({ summary })
5649
} catch (error) {
5750
console.error("[query-summary] Error:", error)
5851
return NextResponse.json(
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Model client adapter — routes inference to Ollama (fine-tuned) or
3+
* OpenAI (fallback) based on MODEL_BACKEND env var.
4+
*/
5+
6+
import { generateText } from "ai"
7+
import { createOpenAI } from "@ai-sdk/openai"
8+
9+
const MODEL_BACKEND = process.env.MODEL_BACKEND || "openai"
10+
const SCHOOL_CODE = process.env.SCHOOL_CODE || "bishop-state"
11+
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || "http://localhost:11434"
12+
const MODEL_SIZE = process.env.MODEL_SIZE || "9b"
13+
14+
let _openai: ReturnType<typeof createOpenAI> | null = null
15+
16+
function getOpenAI() {
17+
if (!_openai) {
18+
_openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY || "" })
19+
}
20+
return _openai
21+
}
22+
23+
async function callOllama(model: string, prompt: string, maxTokens: number): Promise<string> {
24+
const response = await fetch(`${OLLAMA_BASE_URL}/api/generate`, {
25+
method: "POST",
26+
headers: { "Content-Type": "application/json" },
27+
body: JSON.stringify({
28+
model,
29+
prompt,
30+
stream: false,
31+
options: {
32+
temperature: 0.3,
33+
num_predict: maxTokens,
34+
},
35+
}),
36+
})
37+
38+
if (!response.ok) {
39+
throw new Error(`Ollama error: ${response.status} ${response.statusText}`)
40+
}
41+
42+
const data = await response.json()
43+
return data.response
44+
}
45+
46+
async function generate(
47+
task: "explainer" | "summarizer",
48+
prompt: string,
49+
maxTokens: number,
50+
): Promise<string> {
51+
if (MODEL_BACKEND === "ollama") {
52+
const model = `${SCHOOL_CODE}-${task}:${MODEL_SIZE}`
53+
return callOllama(model, prompt, maxTokens)
54+
}
55+
const result = await generateText({
56+
model: getOpenAI()("gpt-4o-mini"),
57+
prompt,
58+
maxOutputTokens: maxTokens,
59+
})
60+
return result.text
61+
}
62+
63+
/**
64+
* Generate a course pairing explanation.
65+
*/
66+
export async function generateExplanation(
67+
prompt: string,
68+
maxTokens: number = 320,
69+
): Promise<string> {
70+
return generate("explainer", prompt, maxTokens)
71+
}
72+
73+
/**
74+
* Generate a query result summary.
75+
*/
76+
export async function generateSummary(
77+
prompt: string,
78+
maxTokens: number = 200,
79+
): Promise<string> {
80+
return generate("summarizer", prompt, maxTokens)
81+
}

0 commit comments

Comments
 (0)