vineelagampa AyushSankar13 commited on
Commit
645b705
·
verified ·
1 Parent(s): ee41b72

Update backend.py (#53)

Browse files

- Update backend.py (84ee812a8c4bfdbcc90279df65cea8a82501cd5d)


Co-authored-by: Ayush Sankar <AyushSankar13@users.noreply.huggingface.co>

Files changed (1) hide show
  1. backend.py +391 -93
backend.py CHANGED
@@ -3,16 +3,17 @@ from ast import List
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pydantic import BaseModel
5
  import io
6
- #import fitz
7
  import traceback
8
  import pandas as pd
9
-
10
  import base64
11
  import json
12
  import re
13
  import asyncio
14
  import functools
15
  from typing import Any, Optional
 
 
16
 
17
  import google.generativeai as genai
18
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException, APIRouter, Request
@@ -22,9 +23,15 @@ import firebase_admin
22
  from firebase_admin import credentials, firestore
23
  from google.generativeai import generative_models
24
  from pydantic import BaseModel
25
- from past_reports import router as reports_router, db_fetch_reports
 
 
 
 
 
26
 
27
- from api_key import GEMINI_API_KEY
 
28
 
29
  app = FastAPI()
30
  api = APIRouter(prefix="/api")
@@ -51,20 +58,13 @@ class AnalyzeRequest(BaseModel):
51
  image_base64: str
52
  prompt: Optional[str] = None
53
 
54
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", GEMINI_API_KEY)
55
-
56
- if not GEMINI_API_KEY:
57
- raise RuntimeError(
58
- "No Gemini API key found. Put it in api_key.py as `GEMINI_API_KEY = '...'` or set env var GEMINI_API_KEY."
59
- )
60
-
61
  genai.configure(api_key=GEMINI_API_KEY)
62
 
63
  generation_config = {
64
- "temperature": 0.2,
65
- "top_p": 0.95,
66
- "top_k": 40,
67
- "max_output_tokens": 2048,
68
  }
69
 
70
  safety_settings = [
@@ -84,60 +84,87 @@ class ChatResponse(BaseModel):
84
  class TextRequest(BaseModel):
85
  text: str
86
 
87
- system_prompt = """ You are a highly skilled medical practitioner specializing in medical image and document analysis. You will be given either a medical image or a PDF.
 
88
  Your responsibilities are:
 
89
  1. **Extract Text**: If the input is a PDF or image, first extract all the text content (lab values, notes, measurements, etc.). Do not summarize — keep the extracted text verbatim.
 
90
  2. **Detailed Analysis**: Use both the extracted text and the visual features of the image to identify any anomalies, diseases, or health issues.
91
- 3. **Finding Report**: Document all observed anomalies or signs of disease.
92
- - Include any measurements (e.g., triglycerides, HBa1c, HDL) in the format:
93
- `{"findings": "Condition only if risky: measurement type -- value with unit(current range)"}`
94
- - Simplify the finding in **3 words** at the beginning when helpful.
95
- 4. **Checking for Past**: If a disease is family history or previously recovered, mark severity as:
96
- `"severity": "severity of anomaly (Past Anomaly but Still Under Risk)"`
97
- 5. **Recommendations and Next Steps**: Provide detailed recommendations (tests, follow-ups, consultations).
98
- 6. **Treatment Suggestions**: Offer preliminary treatments or interventions.
99
- 7. **Output Format**: Always return a JSON object containing both the raw extracted text and the structured analysis, like this:
100
- ```json
101
  {
102
  "ocr_text": "<<<FULL VERBATIM TEXT FROM THE PDF/IMAGE>>>",
103
- "analysis": [
104
  {
105
- "findings": "UPPERCASE MAIN CONCERN. Description of the first disease or condition.",
106
- "severity": "MILD/SEVERE/CRITICAL",
107
- "recommendations": ["Follow-up test 1", "Follow-up test 2"],
108
- "treatment_suggestions": ["Treatment 1", "Treatment 2"],
109
- "home_care_guidance": ["Care tip 1", "Care tip 2"]
 
 
110
  },
111
  {
112
- "findings": "UPPERCASE MAIN CONCERN. Description of the second disease or condition.",
113
- "severity": "MILD/SEVERE/CRITICAL",
114
- "recommendations": ["Follow-up test A", "Follow-up test B"],
115
- "treatment_suggestions": ["Treatment A", "Treatment B"],
116
- "home_care_guidance": ["Care tip A", "Care tip B"]
 
 
 
 
 
 
 
 
 
 
 
117
  }
118
  ]
119
  }
120
- Important Notes:
121
- 1. Scope of Response: Only respond if the image pertains to a human health issue.
122
- 2. Clarity of Image: Ensure the image is clear and suitable for accurate analysis.
123
- 3. As an AI, don't produce any output than doesn't follow this format and also remember that we need your input on the medical report for a team of renowned doctors to recheck your readings and determine AI's role in the medical industry.
124
- 4. Your Insights are Invaluable: Your insights play a crucial role in guiding clinical decisions. Please proceed with your analysis, adhering to the structured approach outlined above.
125
- 5. Completely UPPERCASE the main concern in the finding """
126
-
127
- system_prompt_chat = """
128
- *** Role: Medical Chat Assistant ***
129
- You are a concise and empathetic medical chatbot. Your job is to give clear, short answers (max 3-4 sentences) based only on the provided medical report text.
130
 
131
- Rules:
132
- - Avoid repeating the entire report; focus only on what is directly relevant to the user’s question.
133
- - Give top 2 actionable steps if needed.
134
- - If condition is serious, suggest consulting a doctor immediately.
135
- - Always end with: "Check with your physician before acting."
 
 
 
 
 
 
 
 
 
 
 
136
 
137
- Input:
138
- Report Text: {document_text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  User Question: {user_question}
140
- Response:
141
  """
142
 
143
  model = genai.GenerativeModel(model_name="gemini-2.5-flash-lite")
@@ -152,7 +179,45 @@ async def _call_model_blocking(request_inputs, generation_cfg, safety_cfg):
152
  loop = asyncio.get_event_loop()
153
  return await loop.run_in_executor(None, fn)
154
 
155
- async def analyze_image(image_bytes: bytes, mime_type: str, prompt: Optional[str] = None) -> Any:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  base64_img = base64.b64encode(image_bytes).decode("utf-8")
157
  text_prompt = (prompt or system_prompt).strip()
158
 
@@ -164,7 +229,9 @@ async def analyze_image(image_bytes: bytes, mime_type: str, prompt: Optional[str
164
  try:
165
  response = await _call_model_blocking(request_inputs, generation_config, safety_settings)
166
  except Exception as e:
 
167
  raise RuntimeError(f"Model call failed: {e}")
 
168
  text = getattr(response, "text", None)
169
  if not text and isinstance(response, dict):
170
  candidates = response.get("candidates") or []
@@ -173,25 +240,93 @@ async def analyze_image(image_bytes: bytes, mime_type: str, prompt: Optional[str
173
  if not text:
174
  text = str(response)
175
 
176
- clean = re.sub(r"```(?:json)?", "", text).strip()
177
- print(f"Cleaned text: {clean}")
 
 
 
 
 
178
  try:
179
  parsed = json.loads(clean)
180
- ocr_text = parsed["ocr_text"]
181
- analysis = parsed["analysis"]
182
- print(f"Parsed JSON: {parsed}")
183
- return analysis,ocr_text
184
- except json.JSONDecodeError:
185
- match = re.search(r"(\[.*\]|\{.*\})", clean, re.DOTALL)
186
- if match:
187
- try:
188
- parsed = json.loads(match.group(1)), None
189
- ocr_text = parsed["ocr_text"]
190
- analysis = parsed["analysis"]
191
- return analysis, ocr_text
192
- except json.JSONDecodeError:
193
- return {"raw_found_json": match.group(1)}, None
194
- return {"raw_output": clean}, None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
  def get_past_reports_from_sqllite(user_id: str):
197
  try:
@@ -200,19 +335,23 @@ def get_past_reports_from_sqllite(user_id: str):
200
  history_text = ""
201
  for report in reports:
202
  history_text += f"Report from {report.get('report_date', 'N/A')}:\n{report.get('ocr_text', 'No OCR text found')}\n\n"
 
 
 
203
  except Exception as e:
204
- history_text = "No past reports found for this user."
205
- return history_text
206
 
207
  @app.post("/chat/", response_model=ChatResponse)
208
  async def chat_endpoint(request: ChatRequest):
209
  global result
210
- print(f"Received chat request for user: {request.user_id}")
 
211
  full_document_text = get_past_reports_from_sqllite(request.user_id.strip())
 
 
212
 
213
- full_document_text = EXTRACTED_TEXT_CACHE+"\n\n" + "PAST REPORTS:\n" + full_document_text
214
- print(f"Full document text: {full_document_text}")
215
- if not full_document_text:
216
  raise HTTPException(status_code=400, detail="No past reports or current data exists for this user")
217
 
218
  try:
@@ -220,38 +359,197 @@ async def chat_endpoint(request: ChatRequest):
220
  document_text=full_document_text,
221
  user_question=request.question
222
  )
223
- print(f"Full prompt: {full_prompt}")
224
 
225
  response = model.generate_content(full_prompt)
226
  return ChatResponse(answer=response.text)
227
  except Exception as e:
 
228
  raise HTTPException(status_code=500, detail=f"Chat error: {e}")
229
 
230
  @app.post("/analyze")
231
- async def analyze_endpoint(file: UploadFile = File(...), prompt: str = Form(None)):
232
- global result,EXTRACTED_TEXT_CACHE
 
 
 
 
233
 
234
  filename = file.filename.lower()
235
- print(f"Received analyze request for file {filename}")
236
  contents = await file.read()
237
  mime = file.content_type or "image/png"
238
 
239
  try:
240
- result, ocr_text = await analyze_image(contents, mime, prompt)
241
  EXTRACTED_TEXT_CACHE = ocr_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  except Exception as e:
 
 
243
  raise HTTPException(status_code=500, detail=str(e))
244
- return JSONResponse(content={
245
- "ocr_text": ocr_text,
246
- "Detected_Anomolies": result
247
- })
248
 
249
  @app.post("/analyze_json")
250
  async def analyze_json(req: AnalyzeRequest):
251
  import base64
252
  image_bytes = base64.b64decode(req.image_base64)
253
- result = await analyze_image(image_bytes, "image/png", req.prompt)
254
- return {"result": result}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  @app.get("/health/")
257
  def health():
@@ -274,7 +572,7 @@ def main():
274
  uvicorn.run(
275
  "main:app",
276
  host="localhost",
277
- port="8000",
278
  reload=True,
279
  log_level="debug"
280
  )
@@ -282,7 +580,7 @@ def main():
282
  uvicorn.run(
283
  app,
284
  host="localhost",
285
- port="8000",
286
  reload=False,
287
  log_level="info"
288
  )
@@ -292,4 +590,4 @@ def main():
292
  raise
293
 
294
  if __name__ == "__main__":
295
- main()
 
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pydantic import BaseModel
5
  import io
 
6
  import traceback
7
  import pandas as pd
8
+ import logging
9
  import base64
10
  import json
11
  import re
12
  import asyncio
13
  import functools
14
  from typing import Any, Optional
15
+ from datetime import datetime
16
+ import uvicorn
17
 
18
  import google.generativeai as genai
19
  from fastapi import FastAPI, UploadFile, File, Form, HTTPException, APIRouter, Request
 
23
  from firebase_admin import credentials, firestore
24
  from google.generativeai import generative_models
25
  from pydantic import BaseModel
26
+ from past_reports import router as reports_router, db_fetch_reports, db_insert_report, db_get_report
27
+
28
+ GEMINI_API_KEY="AIzaSyAK0HJWN-WLuG5BxkHawu6_qFpcXU71cT0"
29
+
30
+ logger = logging.getLogger(__name__)
31
+ logging.basicConfig(level=logging.INFO)
32
 
33
+ class Config:
34
+ DEBUG = True
35
 
36
  app = FastAPI()
37
  api = APIRouter(prefix="/api")
 
58
  image_base64: str
59
  prompt: Optional[str] = None
60
 
 
 
 
 
 
 
 
61
  genai.configure(api_key=GEMINI_API_KEY)
62
 
63
  generation_config = {
64
+ "temperature": 0.1,
65
+ "top_p": 0.8,
66
+ "top_k": 20,
67
+ "max_output_tokens": 4096,
68
  }
69
 
70
  safety_settings = [
 
84
  class TextRequest(BaseModel):
85
  text: str
86
 
87
+ system_prompt = """You are a highly skilled medical practitioner specializing in medical image and document analysis. You will be given either a medical image or a PDF.
88
+
89
  Your responsibilities are:
90
+
91
  1. **Extract Text**: If the input is a PDF or image, first extract all the text content (lab values, notes, measurements, etc.). Do not summarize — keep the extracted text verbatim.
92
+
93
  2. **Detailed Analysis**: Use both the extracted text and the visual features of the image to identify any anomalies, diseases, or health issues.
94
+
95
+ 3. **Output Format**: You MUST return ONLY a valid JSON object with this EXACT structure (no additional text, no markdown, no code blocks):
96
+
 
 
 
 
 
 
 
97
  {
98
  "ocr_text": "<<<FULL VERBATIM TEXT FROM THE PDF/IMAGE>>>",
99
+ "measurements": [
100
  {
101
+ "type": "HbA1c",
102
+ "value": 8.5,
103
+ "unit": "%",
104
+ "min": "4.0",
105
+ "max": "5.6",
106
+ "status": "HIGH",
107
+ "severity": "SEVERE"
108
  },
109
  {
110
+ "type": "Total Cholesterol",
111
+ "value": 280,
112
+ "unit": "mg/dL",
113
+ "min": "0",
114
+ "max": "200",
115
+ "status": "HIGH",
116
+ "severity": "SEVERE"
117
+ }
118
+ ],
119
+ "analysis": [
120
+ {
121
+ "findings": "DIABETES. Elevated HbA1c indicates poor glucose control over past 2-3 months.",
122
+ "severity": "SEVERE",
123
+ "recommendations": ["Consult endocrinologist immediately", "Review medication regimen"],
124
+ "treatment_suggestions": ["Adjust insulin dosage", "Consider metformin"],
125
+ "home_care_guidance": ["Monitor blood sugar 4x daily", "Follow diabetic diet"]
126
  }
127
  ]
128
  }
 
 
 
 
 
 
 
 
 
 
129
 
130
+ 4. **Measurement Extraction Rules**:
131
+ - Extract EVERY numerical health measurement found in the document
132
+ - Include lab values, vital signs, body measurements, test results
133
+ - For each measurement provide: type, value, unit, min, max, status, severity
134
+ - To provide the min and max, first check the document for a provided min or max, if not just use your AI knowledge to provide the min and max for that specific measurement type
135
+ - Status should be LOW, NORMAL, BORDER-LINE HIGH, and HIGH based on min and max.
136
+
137
+ 5. **Finding Analysis**:
138
+ - Document all observed anomalies or diseases in the analysis section
139
+ - UPPERCASE the main concern in each finding
140
+ - Link findings to relevant measurements when applicable
141
+ - If a disease is family history or previously recovered, mark severity as: "severity of anomaly (Past Anomaly but Still Under Risk)"
142
+ - Provide actionable recommendations and treatment suggestions
143
+
144
+ CRITICAL: Return ONLY the JSON object. No explanatory text, no markdown formatting, no code blocks. Also make sure to check all your information twice before sending.
145
+ """
146
 
147
+ system_prompt_chat = """
148
+ *** Role: Medical Guidance Facilitator
149
+ *** Objective:
150
+ Analyze medical data, provide concise, evidence-based insights, and recommend actionable next steps for patient care. This includes suggesting local physicians or specialists within a user-specified mile radius, prioritizing in-network options when insurance information is available, and maintaining strict safety compliance with appropriate disclaimers.
151
+ *** Capabilities:
152
+ 1. Report Analysis – Review and interpret findings in uploaded medical reports.
153
+ 2. Historical Context – Compare current findings with any available previous reports.
154
+ 3. Medical Q&A – Answer specific questions about the report using trusted medical sources.
155
+ 4. Specialist Matching – Recommend relevant physician specialties for identified conditions.
156
+ 5. Local Physician Recommendations – List at least two real physician or clinic options within the user-specified mile radius (include name, specialty, address, distance from user, and contact info) based on the patient's location and clinical need.
157
+ 6. Insurance Guidance – If insurance/network information is provided, prioritize in-network physicians.
158
+ 7. Safety Protocols – Include a brief disclaimer encouraging users to verify information, confirm insurance coverage, and consult providers directly.
159
+ *** Response Structure:
160
+ Start with a direct answer to the user's primary question (maximum 4 concise sentences, each on a new line).
161
+ If a physician/specialist is needed, recommend at least two local providers within the requested radius (include name, specialty, address, distance, and contact info).
162
+ If insurance details are available, indicate which physicians are in-network.
163
+ End with a short safety disclaimer.
164
+ ***Input Fields:
165
+ Provided Document Text: {document_text}
166
  User Question: {user_question}
167
+ Assistant Answer:
168
  """
169
 
170
  model = genai.GenerativeModel(model_name="gemini-2.5-flash-lite")
 
179
  loop = asyncio.get_event_loop()
180
  return await loop.run_in_executor(None, fn)
181
 
182
+ def extract_measurements_from_gemini_structured(measurements_data):
183
+ measurements = []
184
+
185
+ if not measurements_data:
186
+ logger.warning("No measurements data provided")
187
+ return measurements
188
+
189
+ for measurement in measurements_data:
190
+ try:
191
+ measurement_type = measurement.get("type") or measurement.get("measurement_type", "Unknown")
192
+ value = measurement.get("value", 0)
193
+ unit = measurement.get("unit", "")
194
+
195
+ ref_range = ""
196
+ if measurement.get("reference_range"):
197
+ ref_range = measurement.get("reference_range")
198
+ elif measurement.get("min") and measurement.get("max"):
199
+ ref_range = f"{measurement.get('min')}-{measurement.get('max')}"
200
+ elif measurement.get("min"):
201
+ ref_range = f">{measurement.get('min')}"
202
+ elif measurement.get("max"):
203
+ ref_range = f"<{measurement.get('max')}"
204
+
205
+ measurements.append({
206
+ "measurement_type": measurement_type,
207
+ "value": float(value) if value else 0.0,
208
+ "unit": unit,
209
+ "min": measurement.get('min'),
210
+ "max": measurement.get('max'),
211
+ "status": measurement.get("status", "UNKNOWN"),
212
+ "severity": measurement.get("severity", "UNKNOWN"),
213
+ })
214
+ except Exception as e:
215
+ logger.error(f"Error processing measurement: {measurement}, error: {e}")
216
+ continue
217
+
218
+ return measurements
219
+
220
+ async def analyze_image(image_bytes: bytes, mime_type: str, prompt: Optional[str] = None) -> tuple:
221
  base64_img = base64.b64encode(image_bytes).decode("utf-8")
222
  text_prompt = (prompt or system_prompt).strip()
223
 
 
229
  try:
230
  response = await _call_model_blocking(request_inputs, generation_config, safety_settings)
231
  except Exception as e:
232
+ logger.error(f"Model call failed: {e}")
233
  raise RuntimeError(f"Model call failed: {e}")
234
+
235
  text = getattr(response, "text", None)
236
  if not text and isinstance(response, dict):
237
  candidates = response.get("candidates") or []
 
240
  if not text:
241
  text = str(response)
242
 
243
+ logger.info(f"Raw Gemini response: {text[:500]}...")
244
+
245
+ clean = re.sub(r'```(?:json)?\s*', '', text).strip()
246
+ clean = re.sub(r'```\s*$', '', clean).strip()
247
+
248
+ logger.info(f"Cleaned response: {clean[:500]}...")
249
+
250
  try:
251
  parsed = json.loads(clean)
252
+
253
+ if "ocr_text" in parsed and "measurements" in parsed and "analysis" in parsed:
254
+ ocr_text = parsed.get("ocr_text", "")
255
+ measurements = parsed.get("measurements", [])
256
+ analysis = parsed.get("analysis", [])
257
+
258
+ logger.info(f"Successfully parsed structured response with {len(measurements)} measurements and {len(analysis)} analyses")
259
+ return analysis, ocr_text, measurements
260
+
261
+ logger.warning("Response not in expected format, attempting to extract...")
262
+
263
+ except json.JSONDecodeError as e:
264
+ logger.error(f"Initial JSON decode error: {e}")
265
+
266
+ json_match = re.search(r'\{[\s\S]*"ocr_text"[\s\S]*"measurements"[\s\S]*"analysis"[\s\S]*\}', clean)
267
+ if json_match:
268
+ try:
269
+ logger.info("Found structured JSON in response, attempting to parse...")
270
+ parsed = json.loads(json_match.group(0))
271
+
272
+ ocr_text = parsed.get("ocr_text", "")
273
+ measurements = parsed.get("measurements", [])
274
+ analysis = parsed.get("analysis", [])
275
+
276
+ logger.info(f"Successfully extracted structured data with {len(measurements)} measurements and {len(analysis)} analyses")
277
+ return analysis, ocr_text, measurements
278
+
279
+ except json.JSONDecodeError as e:
280
+ logger.error(f"Failed to parse extracted JSON: {e}")
281
+
282
+ if "raw_found_json" in clean:
283
+ try:
284
+ temp_parsed = json.loads(clean)
285
+ if "raw_found_json" in temp_parsed:
286
+ inner_json = temp_parsed["raw_found_json"]
287
+ if isinstance(inner_json, str):
288
+ inner_parsed = json.loads(inner_json)
289
+ else:
290
+ inner_parsed = inner_json
291
+
292
+ ocr_text = inner_parsed.get("ocr_text", "")
293
+ measurements = inner_parsed.get("measurements", [])
294
+ analysis = inner_parsed.get("analysis", [])
295
+
296
+ logger.info(f"Successfully unwrapped raw_found_json with {len(measurements)} measurements")
297
+ return analysis, ocr_text, measurements
298
+
299
+ except (json.JSONDecodeError, KeyError) as e:
300
+ logger.error(f"Failed to unwrap raw_found_json: {e}")
301
+
302
+ logger.warning("Using fallback parsing - structured data extraction failed")
303
+ return [{"findings": "Failed to parse structured response", "raw_output": clean[:1000]}], "", []
304
+
305
+ def save_analysis_with_measurements(user_id, ocr_text, analysis_data, measurements_data, report_date=None):
306
+ measurements = extract_measurements_from_gemini_structured(measurements_data)
307
+
308
+ report_data = {
309
+ "user_id": user_id,
310
+ "report_date": report_date or datetime.now().strftime("%Y-%m-%d"),
311
+ "ocr_text": ocr_text,
312
+ "anomalies": json.dumps(analysis_data) if analysis_data else None,
313
+ "measurements": json.dumps(measurements)
314
+ }
315
+
316
+ try:
317
+ logger.info(f"Saving report for user {user_id} with {len(measurements)} measurements")
318
+ report_id = db_insert_report(report_data)
319
+ logger.info(f"Report saved with ID: {report_id}")
320
+
321
+ for measurement in measurements:
322
+ status_indicator = "WARNING" if measurement['status'] in ['HIGH', 'LOW', 'CRITICAL'] else "OK"
323
+ logger.info(f" {status_indicator} {measurement['measurement_type']}: {measurement['value']} {measurement['unit']} ({measurement['status']})")
324
+
325
+ return report_id, measurements
326
+ except Exception as e:
327
+ logger.error(f"Failed to save report: {e}")
328
+ logger.error(f"Report data: {report_data}")
329
+ return None, measurements
330
 
331
  def get_past_reports_from_sqllite(user_id: str):
332
  try:
 
335
  history_text = ""
336
  for report in reports:
337
  history_text += f"Report from {report.get('report_date', 'N/A')}:\n{report.get('ocr_text', 'No OCR text found')}\n\n"
338
+
339
+ logger.info(f"Retrieved {len(reports)} past reports for user {user_id}")
340
+ return history_text
341
  except Exception as e:
342
+ logger.error(f"Error fetching past reports: {e}")
343
+ return "No past reports found for this user."
344
 
345
  @app.post("/chat/", response_model=ChatResponse)
346
  async def chat_endpoint(request: ChatRequest):
347
  global result
348
+ logger.info(f"Received chat request for user: {request.user_id}")
349
+
350
  full_document_text = get_past_reports_from_sqllite(request.user_id.strip())
351
+ full_document_text = EXTRACTED_TEXT_CACHE + "\n\n" + "PAST REPORTS:\n" + full_document_text
352
+ logger.info(f"Full document text length: {len(full_document_text)}")
353
 
354
+ if not full_document_text.strip():
 
 
355
  raise HTTPException(status_code=400, detail="No past reports or current data exists for this user")
356
 
357
  try:
 
359
  document_text=full_document_text,
360
  user_question=request.question
361
  )
362
+ logger.info(f"Generated chat prompt length: {len(full_prompt)}")
363
 
364
  response = model.generate_content(full_prompt)
365
  return ChatResponse(answer=response.text)
366
  except Exception as e:
367
+ logger.error(f"Chat error: {e}")
368
  raise HTTPException(status_code=500, detail=f"Chat error: {e}")
369
 
370
  @app.post("/analyze")
371
+ async def analyze_endpoint(
372
+ file: UploadFile = File(...),
373
+ prompt: str = Form(None),
374
+ user_id: str = Form("anonymous")
375
+ ):
376
+ global result, EXTRACTED_TEXT_CACHE
377
 
378
  filename = file.filename.lower()
379
+ logger.info(f"Received analyze request for file {filename} from user {user_id}")
380
  contents = await file.read()
381
  mime = file.content_type or "image/png"
382
 
383
  try:
384
+ analysis_result, ocr_text, measurements_data = await analyze_image(contents, mime, prompt)
385
  EXTRACTED_TEXT_CACHE = ocr_text
386
+ result = analysis_result
387
+
388
+ report_id, measurements = save_analysis_with_measurements(
389
+ user_id=user_id,
390
+ ocr_text=ocr_text,
391
+ analysis_data=analysis_result,
392
+ measurements_data=measurements_data
393
+ )
394
+
395
+ response_data = {
396
+ "report_id": report_id,
397
+ "ocr_text": ocr_text,
398
+ "Detected_Anomolies": analysis_result,
399
+ "measurements": measurements,
400
+ "measurement_count": len(measurements)
401
+ }
402
+
403
+ logger.info(f"Analysis complete. Report ID: {report_id}, Measurements: {len(measurements)}")
404
+ return JSONResponse(content=response_data)
405
+
406
  except Exception as e:
407
+ logger.error(f"Analysis error: {e}")
408
+ logger.error(f"Traceback: {traceback.format_exc()}")
409
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
410
 
411
  @app.post("/analyze_json")
412
  async def analyze_json(req: AnalyzeRequest):
413
  import base64
414
  image_bytes = base64.b64decode(req.image_base64)
415
+ result, ocr_text, measurements = await analyze_image(image_bytes, "image/png", req.prompt)
416
+ return {
417
+ "result": result,
418
+ "ocr_text": ocr_text,
419
+ "measurements": measurements
420
+ }
421
+
422
+ @app.get("/measurements/{report_id}")
423
+ async def get_report_measurements(report_id: int):
424
+ try:
425
+ report = db_get_report(report_id)
426
+ if not report:
427
+ raise HTTPException(status_code=404, detail="Report not found")
428
+
429
+ measurements_json = report.get('measurements', '[]')
430
+ if isinstance(measurements_json, str):
431
+ measurements = json.loads(measurements_json)
432
+ else:
433
+ measurements = measurements_json or []
434
+
435
+ logger.info(f"Retrieved {len(measurements)} measurements for report {report_id}")
436
+
437
+ return JSONResponse(content={
438
+ "report_id": report_id,
439
+ "measurements": measurements,
440
+ "measurement_count": len(measurements)
441
+ })
442
+ except Exception as e:
443
+ logger.error(f"Error getting measurements for report {report_id}: {e}")
444
+ raise HTTPException(status_code=500, detail=str(e))
445
+
446
+ @app.get("/user_measurements/")
447
+ async def get_user_measurements(user_id: str):
448
+ try:
449
+ reports = db_fetch_reports(user_id=user_id, limit=100, offset=0)
450
+ all_measurements = []
451
+
452
+ for report in reports:
453
+ measurements_json = report.get('measurements', '[]')
454
+ if isinstance(measurements_json, str):
455
+ measurements = json.loads(measurements_json)
456
+ else:
457
+ measurements = measurements_json or []
458
+
459
+ if measurements:
460
+ for measurement in measurements:
461
+ measurement['report_id'] = report['id']
462
+ measurement['report_date'] = report['report_date']
463
+ measurement['created_at'] = report['created_at']
464
+ all_measurements.append(measurement)
465
+
466
+ all_measurements.sort(key=lambda x: x.get('created_at', ''), reverse=True)
467
+
468
+ logger.info(f"Retrieved {len(all_measurements)} total measurements for user {user_id}")
469
+
470
+ return JSONResponse(content={
471
+ "user_id": user_id,
472
+ "total_measurements": len(all_measurements),
473
+ "measurements": all_measurements
474
+ })
475
+ except Exception as e:
476
+ logger.error(f"Error getting user measurements for {user_id}: {e}")
477
+ raise HTTPException(status_code=500, detail=str(e))
478
+
479
+ @app.get("/measurement_trends/")
480
+ async def get_measurement_trends(user_id: str, measurement_type: str = None):
481
+ try:
482
+ reports = db_fetch_reports(user_id=user_id, limit=100, offset=0)
483
+ trends = {}
484
+
485
+ for report in reports:
486
+ measurements_json = report.get('measurements', '[]')
487
+ if isinstance(measurements_json, str):
488
+ measurements = json.loads(measurements_json)
489
+ else:
490
+ measurements = measurements_json or []
491
+
492
+ if measurements:
493
+ for measurement in measurements:
494
+ m_type = measurement['measurement_type']
495
+
496
+ if measurement_type and m_type.lower() != measurement_type.lower():
497
+ continue
498
+
499
+ if m_type not in trends:
500
+ trends[m_type] = []
501
+
502
+ trends[m_type].append({
503
+ "date": report['report_date'] or report['created_at'],
504
+ "value": measurement['value'],
505
+ "unit": measurement['unit'],
506
+ "status": measurement['status'],
507
+ "severity": measurement['severity'],
508
+ "report_id": report['id']
509
+ })
510
+
511
+ for m_type in trends:
512
+ trends[m_type].sort(key=lambda x: x['date'])
513
+
514
+ logger.info(f"Retrieved trends for {len(trends)} measurement types for user {user_id}")
515
+
516
+ return JSONResponse(content={
517
+ "user_id": user_id,
518
+ "measurement_type_filter": measurement_type,
519
+ "trends": trends
520
+ })
521
+ except Exception as e:
522
+ logger.error(f"Error getting measurement trends for {user_id}: {e}")
523
+ raise HTTPException(status_code=500, detail=str(e))
524
+
525
+ @app.get("/test_db")
526
+ async def test_database():
527
+ try:
528
+ test_reports = db_fetch_reports(user_id="test_user", limit=5, offset=0)
529
+
530
+ test_data = {
531
+ "user_id": "test_user",
532
+ "report_date": datetime.now().strftime("%Y-%m-%d"),
533
+ "ocr_text": "Test OCR text",
534
+ "anomalies": json.dumps([{"test": "data"}]),
535
+ "measurements": json.dumps([{"measurement_type": "Test", "value": 100, "unit": "mg/dL", "status": "NORMAL"}])
536
+ }
537
+
538
+ test_report_id = db_insert_report(test_data)
539
+
540
+ return JSONResponse(content={
541
+ "database_status": "connected",
542
+ "existing_reports": len(test_reports),
543
+ "test_report_id": test_report_id,
544
+ "test_successful": True
545
+ })
546
+ except Exception as e:
547
+ logger.error(f"Database test failed: {e}")
548
+ return JSONResponse(content={
549
+ "database_status": "error",
550
+ "error": str(e),
551
+ "test_successful": False
552
+ }, status_code=500)
553
 
554
  @app.get("/health/")
555
  def health():
 
572
  uvicorn.run(
573
  "main:app",
574
  host="localhost",
575
+ port=8000,
576
  reload=True,
577
  log_level="debug"
578
  )
 
580
  uvicorn.run(
581
  app,
582
  host="localhost",
583
+ port=8000,
584
  reload=False,
585
  log_level="info"
586
  )
 
590
  raise
591
 
592
  if __name__ == "__main__":
593
+ main()