AyushSankar13 commited on
Commit
ae27860
·
verified ·
1 Parent(s): d7f85fc

Update backend.py

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