-
Notifications
You must be signed in to change notification settings - Fork 1
New utilization model and interface #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bacbb92
43759f6
45e5ff9
ac80e4e
9516fe4
1e1eb70
79d4dfc
cc6598c
8115811
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,51 @@ | ||||||||||
import { NextResponse } from "next/server"; | ||||||||||
|
||||||||||
// Avoid direct Node `process` typing to satisfy edge runtimes and linting | ||||||||||
function getPythonBaseUrl(): string { | ||||||||||
const env = (globalThis as any)?.process?.env as | ||||||||||
| Record<string, string | undefined> | ||||||||||
| undefined; | ||||||||||
return env?.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001"; | ||||||||||
} | ||||||||||
|
||||||||||
export async function GET(): Promise<Response> { | ||||||||||
const baseUrl = getPythonBaseUrl(); | ||||||||||
|
||||||||||
const controller = new AbortController(); | ||||||||||
const timeout = setTimeout(() => controller.abort(), 8000); | ||||||||||
try { | ||||||||||
const res = await fetch(`${baseUrl}/health`, { | ||||||||||
method: "GET", | ||||||||||
signal: controller.signal, | ||||||||||
headers: { | ||||||||||
accept: "application/json", | ||||||||||
}, | ||||||||||
// ensure server-side only | ||||||||||
cache: "no-store", | ||||||||||
}); | ||||||||||
clearTimeout(timeout); | ||||||||||
|
||||||||||
if (!res.ok) { | ||||||||||
const text = await res.text().catch(() => ""); | ||||||||||
return NextResponse.json( | ||||||||||
{ | ||||||||||
status: "error", | ||||||||||
code: res.status, | ||||||||||
detail: text || "Python service health check failed", | ||||||||||
}, | ||||||||||
{ status: 502 } | ||||||||||
); | ||||||||||
} | ||||||||||
|
||||||||||
const data = await res.json().catch(() => ({})); | ||||||||||
return NextResponse.json({ status: "ok", upstream: data }, { status: 200 }); | ||||||||||
} catch (error: unknown) { | ||||||||||
clearTimeout(timeout); | ||||||||||
const message = error instanceof Error ? error.message : "Unknown error"; | ||||||||||
const status = message.includes("The user aborted a request") ? 504 : 500; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve timeout detection consistency Similar to the predict route, the timeout detection logic should be more robust. - const status = message.includes("The user aborted a request") ? 504 : 500;
+ const isTimeout = error instanceof Error &&
+ (error.name === 'AbortError' || message.includes("abort"));
+ const status = isTimeout ? 504 : 500; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||
return NextResponse.json( | ||||||||||
{ status: "error", code: status, detail: message }, | ||||||||||
{ status } | ||||||||||
); | ||||||||||
} | ||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,113 @@ | ||||||||||
/** | ||||||||||
* Utilization Prediction API Route | ||||||||||
* | ||||||||||
* Bridges the Next.js app to the Python FastAPI utilization service. Accepts a | ||||||||||
* JSON body of optional numeric features and forwards them to the upstream | ||||||||||
* `/predict` endpoint. Validates the upstream response to ensure it includes | ||||||||||
* all expected utilization count fields before returning to the client. | ||||||||||
* | ||||||||||
* Key behavior: | ||||||||||
* - Reads `PY_UTILIZATION_BASE_URL` env var (defaults to http://127.0.0.1:8001) | ||||||||||
* - 15s timeout with abort controller | ||||||||||
* - Returns 502 if upstream fails or response shape is invalid | ||||||||||
* - Returns 504 on client-aborted timeout, otherwise 500 on unknown errors | ||||||||||
*/ | ||||||||||
import { NextResponse } from "next/server"; | ||||||||||
|
||||||||||
// Minimal schema validation without bringing in zod here | ||||||||||
type PredictRequest = Record<string, unknown>; | ||||||||||
|
||||||||||
interface PredictResponse { | ||||||||||
pcp_visits: number; | ||||||||||
outpatient_visits: number; | ||||||||||
er_visits: number; | ||||||||||
inpatient_admits: number; | ||||||||||
home_health_visits: number; | ||||||||||
rx_fills: number; | ||||||||||
dental_visits: number; | ||||||||||
equipment_purchases: number; | ||||||||||
} | ||||||||||
|
||||||||||
// Avoid direct Node `process` typing to satisfy edge runtimes and linting | ||||||||||
function getPythonBaseUrl(): string { | ||||||||||
const env = (globalThis as any)?.process?.env as | ||||||||||
| Record<string, string | undefined> | ||||||||||
| undefined; | ||||||||||
return env?.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001"; | ||||||||||
} | ||||||||||
|
||||||||||
function isValidResponse(json: any): json is PredictResponse { | ||||||||||
if (!json || typeof json !== "object") return false; | ||||||||||
const keys = [ | ||||||||||
"pcp_visits", | ||||||||||
"outpatient_visits", | ||||||||||
"er_visits", | ||||||||||
"inpatient_admits", | ||||||||||
"home_health_visits", | ||||||||||
"rx_fills", | ||||||||||
"dental_visits", | ||||||||||
"equipment_purchases", | ||||||||||
] as const; | ||||||||||
return keys.every( | ||||||||||
(k) => typeof json[k] === "number" && Number.isFinite(json[k]) | ||||||||||
); | ||||||||||
} | ||||||||||
|
||||||||||
export async function POST(req: Request): Promise<Response> { | ||||||||||
const baseUrl = getPythonBaseUrl(); | ||||||||||
let body: PredictRequest; | ||||||||||
try { | ||||||||||
body = await req.json(); | ||||||||||
} catch { | ||||||||||
return NextResponse.json( | ||||||||||
{ status: "error", detail: "Invalid JSON body" }, | ||||||||||
{ status: 400 } | ||||||||||
); | ||||||||||
} | ||||||||||
|
||||||||||
const controller = new AbortController(); | ||||||||||
const timeout = setTimeout(() => controller.abort(), 15000); | ||||||||||
try { | ||||||||||
const res = await fetch(`${baseUrl}/predict`, { | ||||||||||
method: "POST", | ||||||||||
signal: controller.signal, | ||||||||||
headers: { | ||||||||||
"content-type": "application/json", | ||||||||||
accept: "application/json", | ||||||||||
}, | ||||||||||
body: JSON.stringify(body), | ||||||||||
cache: "no-store", | ||||||||||
}); | ||||||||||
clearTimeout(timeout); | ||||||||||
|
||||||||||
const text = await res.text().catch(() => ""); | ||||||||||
let json: unknown; | ||||||||||
try { | ||||||||||
json = text ? JSON.parse(text) : null; | ||||||||||
} catch { | ||||||||||
json = null; | ||||||||||
} | ||||||||||
|
||||||||||
if (!res.ok || !isValidResponse(json)) { | ||||||||||
return NextResponse.json( | ||||||||||
{ | ||||||||||
status: "error", | ||||||||||
code: res.status, | ||||||||||
detail: "Upstream prediction failed or returned invalid response", | ||||||||||
upstream: text?.slice(0, 500), | ||||||||||
}, | ||||||||||
{ status: 502 } | ||||||||||
); | ||||||||||
} | ||||||||||
|
||||||||||
return NextResponse.json(json as PredictResponse, { status: 200 }); | ||||||||||
} catch (error: unknown) { | ||||||||||
clearTimeout(timeout); | ||||||||||
const message = error instanceof Error ? error.message : "Unknown error"; | ||||||||||
const status = message.includes("The user aborted a request") ? 504 : 500; | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve timeout detection logic The current string matching approach for detecting timeout errors is brittle and may not work across different JavaScript environments or future API changes. - const status = message.includes("The user aborted a request") ? 504 : 500;
+ const isTimeout = error instanceof Error &&
+ (error.name === 'AbortError' || message.includes("abort"));
+ const status = isTimeout ? 504 : 500; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||
return NextResponse.json( | ||||||||||
{ status: "error", code: status, detail: message }, | ||||||||||
{ status } | ||||||||||
); | ||||||||||
} | ||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Code duplication: Extract shared environment access logic
This
getPythonBaseUrl
function is duplicated from the predict route. Consider extracting it to a shared utility.Create a shared utility file
app/api/utilization-model/utils.ts
:Then update both routes:
🤖 Prompt for AI Agents