Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ yarn-error.log*

# typescript
*.tsbuildinfo
next-env.d.ts
.venv/
venv/
__pycache__/
**/__pycache__/
*.py[cod]

# macOS
.DS_Store
51 changes: 51 additions & 0 deletions app/api/utilization-model/health/route.ts
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";
}
Comment on lines +4 to +9
Copy link

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:

// app/api/utilization-model/utils.ts
export function getPythonBaseUrl(): string {
  return process.env.PY_UTILIZATION_BASE_URL || "http://127.0.0.1:8001";
}

Then update both routes:

-// 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";
-}
+import { getPythonBaseUrl } from "./utils";
🤖 Prompt for AI Agents
In app/api/utilization-model/health/route.ts around lines 4 to 9, the
getPythonBaseUrl implementation is duplicated from the predict route; extract
the shared logic into a new module app/api/utilization-model/utils.ts that
exports getPythonBaseUrl (returning process.env.PY_UTILIZATION_BASE_URL ||
"http://127.0.0.1:8001"), then replace the local function in this file (and the
predict route) with an import from that utils file and remove the duplicated
code.


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;
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
🤖 Prompt for AI Agents
In app/api/utilization-model/health/route.ts around line 45, the timeout
detection currently only checks message.includes("The user aborted a request");
update it to mirror the predict route's more robust logic: safely extract the
error message (handle null/undefined), check for AbortError (error.name ===
'AbortError'), and look for multiple timeout indicators (e.g., "The user aborted
a request", "timed out", or other provider-specific timeout phrases) and set
status = 504 when any match; otherwise leave status = 500. Ensure you handle
non-string messages without throwing.

return NextResponse.json(
{ status: "error", code: status, detail: message },
{ status }
);
}
}
113 changes: 113 additions & 0 deletions app/api/utilization-model/predict/route.ts
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;
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
🤖 Prompt for AI Agents
In app/api/utilization-model/predict/route.ts around line 107, the code
currently detects timeouts by string-matching the message "The user aborted a
request"; replace this brittle check with robust error-type checks: inspect the
thrown error's properties (e.g. error.name === 'AbortError' || error.code ===
'ETIMEDOUT' || error.type === 'aborted') and fallback to message substring only
if those properties are absent, then set status = 504 when any of those timeout
indicators are present and 500 otherwise; ensure the implementation safely
handles undefined error properties to avoid runtime exceptions.

return NextResponse.json(
{ status: "error", code: status, detail: message },
{ status }
);
}
}
36 changes: 18 additions & 18 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import type React from "react"
import "./globals.css"
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import type React from "react";
import "./globals.css";

import { AppSidebar } from "@/components/app-sidebar"
import { PolicyProvider } from "@/components/policy-context"
import { SidebarProvider } from "@/components/ui/sidebar"
import { AppSidebar } from "@/components/app-sidebar";
import { PolicyProvider } from "@/components/policy-context";
import { SidebarProvider } from "@/components/ui/sidebar";

const inter = Inter({ subsets: ["latin"] })
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Open Coverage - Health Insurance Comparator",
description: "Compare health insurance policies and SBC documents easily",
generator: 'v0.dev'
}
generator: "v0.dev",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<PolicyProvider>
<SidebarProvider>
<AppSidebar />
{children}
</SidebarProvider>
</PolicyProvider>
<PolicyProvider>
<SidebarProvider>
<AppSidebar />
{children}
</SidebarProvider>
</PolicyProvider>
</body>
</html>
)
);
}
Loading