AI_adjudication / backend /ai /document_extractor.py
stellar413's picture
Initial commit: Plum OPD Claim Adjudication Engine with Hybrid Decision Pipeline
f047704
Raw
History Blame Contribute Delete
5.92 kB
import json
import time
import logging
from typing import Dict, Any, Tuple, Optional, Type
from pydantic import BaseModel, ValidationError
from google.genai import types
from google.genai.errors import APIError
from backend.ai.gemini_client import get_gemini_client, get_model_name
from backend.schemas.schemas import PrescriptionExtraction, BillExtraction, LabReportExtraction
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Fallback schema for "Other" document types
class GenericExtraction(BaseModel):
document_date: Optional[str] = None # YYYY-MM-DD
patient_name: Optional[str] = None
institution_name: Optional[str] = None
summary: str
total_amount: Optional[float] = None
key_findings: Optional[str] = None
def get_schema_for_doc_type(doc_type: str) -> Type[BaseModel]:
doc_type_lower = doc_type.lower()
if "prescription" in doc_type_lower:
return PrescriptionExtraction
elif "bill" in doc_type_lower or "invoice" in doc_type_lower or "receipt" in doc_type_lower:
return BillExtraction
elif "report" in doc_type_lower or "lab" in doc_type_lower or "diagnostic" in doc_type_lower:
return LabReportExtraction
else:
return GenericExtraction
def clean_json_string(text: str) -> str:
"""Repairs and extracts clean JSON from markdown code blocks if necessary."""
text = text.strip()
if text.startswith("```json"):
text = text[7:]
if text.endswith("```"):
text = text[:-3]
return text.strip()
def extract_document_data(
file_bytes: bytes,
mime_type: str,
doc_type: str,
retries: int = 3,
backoff_factor: float = 2.0
) -> Tuple[Dict[str, Any], str, float]:
"""
Extracts structured data from medical documents using Gemini 2.5 Flash.
Returns: (parsed_json_dict, raw_response_text, extraction_confidence)
"""
client = get_gemini_client()
model = get_model_name()
schema = get_schema_for_doc_type(doc_type)
# Prompt setup
prompt = (
f"You are an expert insurance claims document processor. "
f"Extract information from this medical {doc_type} according to the provided schema. "
f"Ensure all names, dates, amounts, and medical terms are transcribed with 100% precision. "
f"If a value is not visible or cannot be found, leave it as null/empty. "
f"Double-check doctor registration numbers and billing items."
)
# Multimodal parts
contents = [
types.Part.from_bytes(
data=file_bytes,
mime_type=mime_type
),
prompt
]
config = types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=schema,
temperature=0.1 # Low temperature for structured extraction
)
raw_response = ""
attempt = 0
delay = 1.0
while attempt < retries:
try:
logger.info(f"Extracting {doc_type} data, attempt {attempt + 1}")
response = client.models.generate_content(
model=model,
contents=contents,
config=config
)
raw_response = response.text
clean_json = clean_json_string(raw_response)
# Validate with Pydantic
parsed_data = schema.model_validate_json(clean_json)
parsed_dict = parsed_data.model_dump()
# Calculate extraction confidence
# Since Gemini 2.5 Flash generated it under response_schema, structure is guaranteed.
# We estimate extraction confidence based on completeness of required fields
filled_fields = 0
total_fields = 0
# Simple heuristic for completeness
for k, v in parsed_dict.items():
total_fields += 1
if v is not None and v != "" and v != []:
filled_fields += 1
completeness_ratio = (filled_fields / total_fields) if total_fields > 0 else 1.0
# Base confidence on successful API validation and completeness
extraction_confidence = min(0.95, 0.70 + (completeness_ratio * 0.25))
return parsed_dict, raw_response, extraction_confidence
except (ValidationError, json.JSONDecodeError) as e:
logger.warning(f"JSON validation failed on attempt {attempt + 1}: {str(e)}")
# Try to repair the JSON via prompt retry
attempt += 1
if attempt == retries:
# If all retries fail, return a fallback empty/partially parsed structure
break
time.sleep(delay)
delay *= backoff_factor
except APIError as e:
logger.error(f"Gemini API error on attempt {attempt + 1}: {str(e)}")
attempt += 1
if attempt == retries:
raise e
time.sleep(delay)
delay *= backoff_factor
except Exception as e:
logger.error(f"Unexpected error during extraction: {str(e)}")
attempt += 1
if attempt == retries:
raise e
time.sleep(delay)
delay *= backoff_factor
# Fallback return in case of persistent validation failure
logger.error("Failed to parse document extraction JSON after maximum retries. Routing to manual review.")
# Return empty representation of the schema
try:
empty_instance = schema.model_validate({})
return empty_instance.model_dump(), raw_response or "{}", 0.50
except Exception:
# If schema requires fields, return a manual fallback dict
return {"patient_name": None, "error": "Extraction failure"}, raw_response or "{}", 0.30