|
|
|
|
|
|
|
|
|
|
|
|
|
|
| from google import genai
|
| from google.genai import types
|
| from pypdf import PdfReader
|
| from dotenv import load_dotenv
|
| import os
|
| import base64
|
|
|
| load_dotenv()
|
| client = genai.Client()
|
|
|
|
|
|
|
|
|
| def extract_pdf_text(file_path):
|
| try:
|
| reader = PdfReader(file_path)
|
| text = ""
|
| for page in reader.pages:
|
| text += page.extract_text() + "\n"
|
| return text.strip()
|
| except Exception as e:
|
| return None
|
|
|
|
|
|
|
|
|
| def image_to_base64(file_path):
|
| with open(file_path, "rb") as f:
|
| return base64.b64encode(f.read()).decode("utf-8")
|
|
|
|
|
|
|
|
|
| def get_mime_type(file_path):
|
| ext = os.path.splitext(file_path)[1].lower()
|
| mime_types = {
|
| ".jpg": "image/jpeg",
|
| ".jpeg": "image/jpeg",
|
| ".png": "image/png",
|
| ".pdf": "application/pdf",
|
| }
|
| return mime_types.get(ext, "image/jpeg")
|
|
|
|
|
|
|
|
|
| def analyze_with_gemini(content, is_image=False, mime_type=None):
|
| prompt = """You are a medical document analyzer.
|
| Analyze this medical document carefully and extract ALL information.
|
|
|
| Return your response in this EXACT format:
|
|
|
| DOCUMENT_TYPE: [blood report / X-ray report / MRI report / prescription / discharge summary / other]
|
| PATIENT_INFO: [any patient details found — name, age, gender, date]
|
| FINDINGS: [all medical values, test results, or observations found]
|
| ABNORMALITIES: [any values marked as high/low/abnormal, or any concerning findings]
|
| DOCTOR_NOTES: [any doctor observations or impressions]
|
| RAW_TEXT: [complete extracted text from document]
|
|
|
| Be thorough. Extract every single medical value and finding you can see.
|
| If information is not available for a field, write: Not found"""
|
|
|
| for attempt in range(3):
|
| try:
|
| if is_image:
|
| response = client.models.generate_content(
|
| model="gemini-2.5-flash",
|
| contents=[
|
| prompt,
|
| types.Part.from_bytes(
|
| data=base64.b64decode(content),
|
| mime_type=mime_type
|
| )
|
| ]
|
| )
|
| else:
|
| response = client.models.generate_content(
|
| model="gemini-2.5-flash",
|
| contents=f"{prompt}\n\nDOCUMENT TEXT:\n{content}"
|
| )
|
| return response.text
|
| except Exception as e:
|
| error_msg = str(e)
|
| if "503" in error_msg or "429" in error_msg or "UNAVAILABLE" in error_msg or "exhausted" in error_msg.lower():
|
| wait_time = (attempt + 1) * 5
|
| print(f"[Document Agent] ⚠️ Rate limit — retrying in {wait_time}s... (attempt {attempt+1}/3)")
|
| import time
|
| time.sleep(wait_time)
|
| else:
|
| return f"ERROR: {e}"
|
| return "ERROR: Gemini API unavailable after 3 retries"
|
|
|
|
|
|
|
|
|
| def is_valid_extraction(parsed: dict) -> bool:
|
| findings = parsed.get("FINDINGS", "")
|
| raw_text = parsed.get("RAW_TEXT", "")
|
| doc_type = parsed.get("DOCUMENT_TYPE", "")
|
|
|
| if len(findings) < 20 and len(raw_text) < 50:
|
| return False
|
|
|
| if doc_type.lower() in ["unknown", "not found", ""]:
|
| return False
|
|
|
| return True
|
|
|
|
|
|
|
|
|
|
|
| def run_document_agent(state: dict) -> dict:
|
| file_path = state.get("file_path")
|
| print(f"\n[Document Agent] Processing: {file_path}")
|
|
|
| if not file_path or not os.path.exists(file_path):
|
| state["error"] = "File not found"
|
| state["raw_text"] = ""
|
| state["document_type"] = "unknown"
|
| return state
|
|
|
| ext = os.path.splitext(file_path)[1].lower()
|
|
|
|
|
| if ext == ".pdf":
|
| print("[Document Agent] PDF detected — extracting text...")
|
| pdf_text = extract_pdf_text(file_path)
|
|
|
| if pdf_text and len(pdf_text) > 100:
|
| print(f"[Document Agent] Extracted {len(pdf_text)} characters from PDF")
|
| gemini_output = analyze_with_gemini(pdf_text, is_image=False)
|
| else:
|
| print("[Document Agent] Scanned PDF detected — using Gemini Vision...")
|
| image_data = image_to_base64(file_path)
|
| gemini_output = analyze_with_gemini(
|
| image_data,
|
| is_image=True,
|
| mime_type="application/pdf"
|
| )
|
|
|
|
|
| elif ext in [".jpg", ".jpeg", ".png"]:
|
| print("[Document Agent] Image detected — using Gemini Vision...")
|
| image_data = image_to_base64(file_path)
|
| mime_type = get_mime_type(file_path)
|
| gemini_output = analyze_with_gemini(
|
| image_data,
|
| is_image=True,
|
| mime_type=mime_type
|
| )
|
|
|
|
|
| parsed_check = parse_gemini_output(gemini_output)
|
| if not is_valid_extraction(parsed_check):
|
| state["error"] = (
|
| "Could not read the document clearly. Please:\n"
|
| "• Ensure good lighting\n"
|
| "• Place report flat on a surface\n"
|
| "• Take photo directly from above\n"
|
| "• Make sure text is clearly visible"
|
| )
|
| state["raw_text"] = ""
|
| state["document_type"] = "unknown"
|
| return state
|
|
|
| else:
|
| state["error"] = f"Unsupported format: {ext}"
|
| state["raw_text"] = ""
|
| state["document_type"] = "unknown"
|
| return state
|
|
|
|
|
| print("[Document Agent] Parsing Gemini output...")
|
| parsed = parse_gemini_output(gemini_output)
|
|
|
|
|
| state["raw_text"] = parsed.get("RAW_TEXT", gemini_output)
|
| state["document_type"] = parsed.get("DOCUMENT_TYPE", "unknown")
|
| state["findings"] = parsed.get("FINDINGS", "")
|
| state["abnormalities"] = parsed.get("ABNORMALITIES", "")
|
| state["patient_info"] = parsed.get("PATIENT_INFO", "")
|
| state["doctor_notes"] = parsed.get("DOCTOR_NOTES", "")
|
| state["gemini_raw"] = gemini_output
|
|
|
|
|
| if any(word in state["document_type"].lower()
|
| for word in ["x-ray", "xray", "mri", "ct", "scan", "ultrasound"]):
|
| state["is_imaging"] = True
|
| else:
|
| state["is_imaging"] = False
|
|
|
| print(f"[Document Agent] ✅ Done — Document type: {state['document_type']}")
|
| print(f"[Document Agent] Is imaging: {state['is_imaging']}")
|
| return state
|
|
|
|
|
|
|
|
|
| def parse_gemini_output(text):
|
| result = {}
|
| current_key = None
|
| current_value = []
|
|
|
| for line in text.split("\n"):
|
| found_key = False
|
| for key in ["DOCUMENT_TYPE", "PATIENT_INFO", "FINDINGS",
|
| "ABNORMALITIES", "DOCTOR_NOTES", "RAW_TEXT"]:
|
| if line.startswith(f"{key}:"):
|
| if current_key:
|
| result[current_key] = " ".join(current_value).strip()
|
| current_key = key
|
| current_value = [line[len(key)+1:].strip()]
|
| found_key = True
|
| break
|
|
|
| if not found_key and current_key:
|
| current_value.append(line.strip())
|
|
|
| if current_key:
|
| result[current_key] = " ".join(current_value).strip()
|
|
|
| return result |