Skip to content

Commit ce52d34

Browse files
Serverless: Merge backend into frontend (no HTTP)
1 parent 23b897c commit ce52d34

1 file changed

Lines changed: 277 additions & 89 deletions

File tree

frontend/utils/api.py

Lines changed: 277 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,306 @@
1-
"""API Client for LumiClaim Backend."""
1+
"""Serverless API Layer - Direct backend imports (no HTTP)."""
22

33
import os
4-
import requests
4+
import sys
5+
import json
56
import streamlit as st
6-
from typing import Any, Optional, Dict
7+
from typing import Any, Optional, Dict, List
8+
from pathlib import Path
79

8-
# Default API Base URL (can be overridden by env var or UI)
9-
DEFAULT_API_BASE = os.getenv("LUMICLAIM_API_BASE", "http://127.0.0.1:8080")
10+
# Ensure backend is importable
11+
_ROOT = Path(__file__).parent.parent.parent
12+
sys.path.insert(0, str(_ROOT))
1013

11-
def get_api_base() -> str:
12-
"""Get the current API base URL from session state or default."""
13-
if "api_base" not in st.session_state:
14-
st.session_state.api_base = DEFAULT_API_BASE
15-
return st.session_state.api_base
14+
# --- Direct Backend Imports ---
15+
from backend import auth
16+
from backend.session import start_session, session_dir, load_profile, save_profile
1617

1718
def get_session_id() -> Optional[str]:
1819
"""Get the current session ID from session state."""
1920
return st.session_state.get("session_id")
2021

2122
def ensure_session():
22-
"""Ensure a session ID exists, creating one if necessary."""
23-
# 1. Check State
23+
"""Ensure a session ID exists."""
2424
if get_session_id():
2525
return
26-
27-
# 2. Check Query Params (Persistence)
28-
qp = st.query_params.get_all("session_id") if hasattr(st.query_params, "get_all") else [st.query_params.get("session_id")]
29-
# Streamlit 1.30+ uses st.query_params as a dict-like object
30-
# Older uses st.experimental_get_query_params
31-
# We will assume modern Streamlit (st.query_params is a dict-like proxy)
3226

33-
# query_params.get returns None or value.
27+
# Check query params
3428
qp_val = st.query_params.get("session_id")
35-
3629
if qp_val:
3730
st.session_state.session_id = qp_val
3831
st.session_state.session_created = True
3932
return
4033

41-
# 3. Create New (Anonymous)
34+
# --- Auth Functions (Direct Calls) ---
35+
def get_auth_status(username: str) -> Dict[str, Any]:
36+
"""Check user status - replaces GET /auth/status/{username}"""
37+
status = auth.get_user_status(username)
38+
return {"status": status}
39+
40+
def login(username: str, password: str) -> Dict[str, Any]:
41+
"""Verify credentials - replaces POST /auth/login"""
42+
success = auth.verify_credentials(username, password)
43+
return {"success": success}
44+
45+
def register(username: str, password: str) -> Dict[str, Any]:
46+
"""Register user - replaces POST /auth/register"""
47+
success = auth.register_user(username, password)
48+
return {"success": success}
49+
50+
def start_user_session(session_id: str) -> Dict[str, Any]:
51+
"""Start session - replaces POST /session/start"""
52+
result = start_session(session_id)
53+
return {"session_id": result}
54+
55+
# --- Document Functions ---
56+
def list_documents(sid: str) -> List[str]:
57+
"""List documents in session."""
58+
if not sid:
59+
return []
60+
extracted_dir = session_dir(sid) / "extracted"
61+
if not extracted_dir.exists():
62+
return []
63+
return [f.stem for f in extracted_dir.glob("*.json")]
64+
65+
def get_session_claims(sid: str) -> Dict[str, Any]:
66+
"""Get all claims from session - replaces GET /session/claims"""
67+
docs = list_documents(sid)
68+
all_rows = []
69+
doc_objs = []
70+
71+
for doc_id in docs:
72+
doc_path = session_dir(sid) / "extracted" / f"{doc_id}.json"
73+
if doc_path.exists():
74+
doc = json.loads(doc_path.read_text())
75+
doc_objs.append({"doc_id": doc_id})
76+
claims = doc.get("claims", [])
77+
for claim in claims:
78+
claim["doc_id"] = doc_id
79+
all_rows.append(claim)
80+
81+
return {"rows": all_rows, "docs": doc_objs}
82+
83+
def upload_eob(file_bytes: bytes, filename: str, mimetype: str, sid: str) -> Dict[str, Any]:
84+
"""Handle EOB upload - replaces POST /upload_eob"""
85+
from backend.upload_eob import handle_upload_file
86+
from io import BytesIO
87+
88+
# Create file-like object
89+
file_obj = type('UploadFile', (), {
90+
'filename': filename,
91+
'content_type': mimetype,
92+
'file': BytesIO(file_bytes),
93+
'read': lambda self: file_bytes
94+
})()
95+
4296
try:
43-
url = f"{get_api_base()}/session/start"
44-
resp = requests.post(url, timeout=30)
45-
if resp.ok:
46-
data = resp.json()
47-
sid = data.get("session_id")
48-
st.session_state.session_id = sid
49-
st.session_state.session_created = True
50-
# Set param for future refreshes
51-
st.query_params["session_id"] = sid
52-
else:
53-
st.error(f"Failed to start session: {resp.text}")
97+
result = handle_upload_file(file_obj, sid)
98+
return result
5499
except Exception as e:
55-
st.error(f"Could not connect to backend at {get_api_base()}: {e}")
100+
return {"error": str(e)}
56101

57-
def _attach_session(params: Dict[str, Any]) -> Dict[str, Any]:
58-
"""Attach session_id to request parameters (query or body)."""
59-
sid = get_session_id()
60-
if not sid:
61-
return params
62-
63-
# If param matches "params" (query args), inject session_id
64-
if "params" in params:
65-
params["params"]["session_id"] = sid
66-
# If param matches "json" (body), inject session_id
67-
elif "json" in params:
68-
if isinstance(params["json"], dict):
69-
params["json"]["session_id"] = sid
70-
# If param matches "data" (form data), inject if dict
71-
elif "data" in params:
72-
if isinstance(params["data"], dict):
73-
params["data"]["session_id"] = sid
74-
75-
# Fallback: if we simply have a dict wrapper, try to inject
76-
# This covers cases where the caller passes just the payload dict
77-
# But usually requests.post(..., json=payload) is how it's called.
78-
return params
79-
80-
def fetch(method: str, endpoint: str, **kwargs) -> Optional[Any]:
81-
"""Make an API request with automatic session handling."""
82-
url = f"{get_api_base()}{endpoint}"
83-
84-
# Auto-inject session ID for known parameterized keys if not present
85-
# Case 1: GET request uses 'params'
86-
if method.upper() == "GET":
87-
kwargs.setdefault("params", {})
88-
if get_session_id():
89-
kwargs["params"].setdefault("session_id", get_session_id())
90-
91-
# Case 2: POST/PUT uses 'json' or 'data'
92-
if method.upper() in ["POST", "PUT", "PATCH"]:
93-
if "json" in kwargs and isinstance(kwargs["json"], dict) and get_session_id():
94-
kwargs["json"].setdefault("session_id", get_session_id())
102+
def get_explanation(doc_id: str, sid: str, persona: str = "patient", level: str = "grade6") -> Dict[str, Any]:
103+
"""Get document explanation - replaces GET /explain/{doc_id}"""
104+
from backend.explain import get_breakdown
105+
106+
doc_path = session_dir(sid) / "extracted" / f"{doc_id}.json"
107+
if not doc_path.exists():
108+
return {"error": "Document not found"}
109+
110+
doc = json.loads(doc_path.read_text())
95111

96112
try:
97-
response = requests.request(method, url, timeout=kwargs.pop("timeout", 60), **kwargs)
98-
if response.status_code == 204:
99-
return None
100-
response.raise_for_status()
101-
return response.json()
102-
except requests.exceptions.HTTPError as e:
103-
# Try to return friendly error message from backend
104-
try:
105-
err_data = response.json()
106-
st.error(f"API Error ({response.status_code}): {err_data.get('detail', str(e))}")
107-
except:
108-
st.error(f"API Error: {e}")
113+
result = get_breakdown(doc, persona, level)
114+
return result
109115
except Exception as e:
110-
st.error(f"Connection Error: {e}")
116+
# Fallback
117+
return {
118+
"doc_id": doc_id,
119+
"takeaway": doc.get("summary", "No summary available"),
120+
"claims": doc.get("claims", []),
121+
"verifiability_score": 1.0
122+
}
123+
124+
def get_evidence_graph(doc_id: str, sid: str) -> Dict[str, Any]:
125+
"""Get evidence graph - replaces GET /egraph/{doc_id}"""
126+
# This is a visualization helper, return minimal structure
127+
return {"nodes": [], "edges": []}
128+
129+
# --- Legacy API compatibility (for pages that still use api.get/api.post) ---
130+
def get(endpoint: str, **kwargs) -> Optional[Any]:
131+
"""Compatibility layer - routes to direct functions."""
132+
params = kwargs.get("params", {})
133+
sid = params.get("session_id") or get_session_id()
134+
135+
# Parse endpoint
136+
if endpoint.startswith("/auth/status/"):
137+
username = endpoint.split("/")[-1]
138+
return get_auth_status(username)
139+
elif "/session/claims" in endpoint:
140+
return get_session_claims(sid)
141+
elif endpoint.startswith("/explain/"):
142+
doc_id = endpoint.split("/")[-1]
143+
return get_explanation(doc_id, sid, params.get("persona", "patient"))
144+
elif endpoint.startswith("/egraph/"):
145+
doc_id = endpoint.split("/")[-1]
146+
return get_evidence_graph(doc_id, sid)
147+
elif "/profile" in endpoint:
148+
return {"profile": load_profile(sid) or {}}
149+
elif "/documents/" in endpoint:
150+
return list_documents(sid)
151+
elif "/reconcile/" in endpoint:
152+
return {"anomalies": []} # Placeholder
153+
else:
154+
# Unknown endpoint - try to return empty
155+
return {}
156+
157+
def post(endpoint: str, **kwargs) -> Optional[Any]:
158+
"""Compatibility layer - routes to direct functions."""
159+
json_data = kwargs.get("json", {})
160+
files = kwargs.get("files", {})
161+
sid = json_data.get("session_id") or get_session_id()
111162

112-
return None
163+
if endpoint == "/auth/login":
164+
return login(json_data.get("username", ""), json_data.get("password", ""))
165+
elif endpoint == "/auth/register":
166+
return register(json_data.get("username", ""), json_data.get("password", ""))
167+
elif endpoint == "/session/start":
168+
return start_user_session(json_data.get("session_id", ""))
169+
elif endpoint == "/upload_eob":
170+
# Handle file upload
171+
if files and "file" in files:
172+
fname, fbytes, ftype = files["file"]
173+
return upload_eob(fbytes, fname, ftype, sid)
174+
return {"error": "No file provided"}
175+
elif endpoint == "/profile":
176+
return _set_profile(json_data)
177+
elif "/explain/ai" in endpoint:
178+
return _explain_ai(json_data)
179+
elif "/appeal/generate_ai" in endpoint:
180+
return _generate_appeal_ai(json_data)
181+
elif "/session/manual_entry" in endpoint:
182+
return _manual_entry(json_data)
183+
elif "/simulate" in endpoint:
184+
return _simulate(json_data)
185+
else:
186+
return {}
113187

114-
def get(endpoint: str, **kwargs):
115-
return fetch("GET", endpoint, **kwargs)
188+
# --- Helper functions ---
189+
def _set_profile(data: Dict[str, Any]) -> Dict[str, Any]:
190+
session_id = data.pop("session_id", get_session_id())
191+
save_profile(session_id, data)
192+
return {"success": True}
116193

117-
def post(endpoint: str, **kwargs):
118-
return fetch("POST", endpoint, **kwargs)
194+
def _explain_ai(data: Dict[str, Any]) -> Dict[str, Any]:
195+
"""AI explanation - replaces POST /explain/ai"""
196+
try:
197+
from backend.llm import summarize_bill
198+
except ImportError:
199+
return {"summary": "AI module not available"}
200+
201+
sid = data.get("session_id", get_session_id())
202+
doc_id = data.get("doc_id", "")
203+
persona = data.get("persona", "patient")
204+
grade = data.get("grade_level", "8th Grade")
205+
206+
doc_path = session_dir(sid) / "extracted" / f"{doc_id}.json"
207+
if not doc_path.exists():
208+
return {"summary": "Document not found."}
209+
210+
doc = json.loads(doc_path.read_text())
211+
breakdown = doc.get("breakdown_text", str(doc.get("claims", [])))
212+
213+
try:
214+
summary = summarize_bill(breakdown, persona, grade)
215+
return {"summary": summary}
216+
except Exception as e:
217+
return {"summary": f"AI unavailable: {e}"}
218+
219+
def _generate_appeal_ai(data: Dict[str, Any]) -> Dict[str, Any]:
220+
"""AI appeal - replaces POST /appeal/generate_ai"""
221+
try:
222+
from backend.llm import generate_appeal_letter
223+
except ImportError:
224+
return {"letter": "AI module not available"}
225+
226+
sid = data.get("session_id", get_session_id())
227+
doc_id = data.get("doc_id", "")
228+
user_context = data.get("user_context", "")
229+
230+
doc_path = session_dir(sid) / "extracted" / f"{doc_id}.json"
231+
if not doc_path.exists():
232+
return {"letter": "Document not found."}
233+
234+
doc = json.loads(doc_path.read_text())
235+
profile = load_profile(sid) or {}
236+
237+
try:
238+
letter = generate_appeal_letter(doc, profile, user_context)
239+
return {"letter": letter}
240+
except Exception as e:
241+
return {"letter": f"AI unavailable: {e}"}
242+
243+
def _manual_entry(data: Dict[str, Any]) -> Dict[str, Any]:
244+
"""Handle manual entry - replaces POST /session/manual_entry"""
245+
sid = data.get("session_id", get_session_id())
246+
if not sid:
247+
return {"error": "No session"}
248+
249+
# Create manual entry document
250+
manual_dir = session_dir(sid) / "extracted"
251+
manual_dir.mkdir(parents=True, exist_ok=True)
252+
253+
# Generate ID
254+
existing = list(manual_dir.glob("MANUAL-*.json"))
255+
next_id = len(existing) + 1
256+
doc_id = f"MANUAL-{next_id:03d}"
257+
258+
doc = {
259+
"doc_id": doc_id,
260+
"claims": [{
261+
"description": data.get("description", ""),
262+
"date": data.get("date", ""),
263+
"cpt": data.get("cpt", ""),
264+
"billed": data.get("billed", 0),
265+
"allowed": data.get("allowed", 0),
266+
"insurer_paid": data.get("insurer_paid", 0),
267+
"patient_resp": data.get("patient_resp", 0)
268+
}]
269+
}
270+
271+
doc_path = manual_dir / f"{doc_id}.json"
272+
doc_path.write_text(json.dumps(doc, indent=2))
273+
274+
return {"success": True, "doc_id": doc_id}
275+
276+
def _simulate(data: Dict[str, Any]) -> Dict[str, Any]:
277+
"""Cost simulation - replaces POST /simulate"""
278+
# Simplified simulation
279+
sid = data.get("session_id", get_session_id())
280+
doc_id = data.get("doc_id", "")
281+
282+
profile = load_profile(sid) or {}
283+
deductible_rem = float(profile.get("deductible_remaining", 500))
284+
coinsurance = float(profile.get("coinsurance", 0.2))
285+
286+
doc_path = session_dir(sid) / "extracted" / f"{doc_id}.json"
287+
if not doc_path.exists():
288+
return {"error": "Document not found"}
289+
290+
doc = json.loads(doc_path.read_text())
291+
claims = doc.get("claims", [])
292+
293+
total_allowed = sum(float(c.get("allowed", 0)) for c in claims)
294+
295+
# Simple calculation
296+
ded_applied = min(deductible_rem, total_allowed)
297+
after_ded = total_allowed - ded_applied
298+
coins_amount = after_ded * coinsurance
299+
expected = ded_applied + coins_amount
300+
301+
return {
302+
"doc_id": doc_id,
303+
"expected_patient_resp": round(expected, 2),
304+
"deductible_applied": round(ded_applied, 2),
305+
"coinsurance_amount": round(coins_amount, 2)
306+
}

0 commit comments

Comments
 (0)