openai_service.py 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import os
  2. import base64
  3. import io
  4. import logging
  5. from openai import AsyncOpenAI
  6. from dotenv import load_dotenv
  7. from PIL import Image
  8. from backend.schemas import ExtractionResponse
  9. load_dotenv()
  10. # Setup logging
  11. logging.basicConfig(level=logging.INFO)
  12. logger = logging.getLogger(__name__)
  13. client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
  14. MAX_IMAGE_SIZE = (2000, 2000)
  15. IMAGE_QUALITY = 85
  16. def compress_image(image_content: bytes) -> bytes:
  17. """Resizes and compresses the image to reduce upload size."""
  18. img = Image.open(io.BytesIO(image_content))
  19. # Convert to RGB if necessary (to save as JPEG)
  20. if img.mode in ("RGBA", "P"):
  21. img = img.convert("RGB")
  22. # Resize if larger than max dimensions
  23. img.thumbnail(MAX_IMAGE_SIZE, Image.Resampling.LANCZOS)
  24. # Save to bytes
  25. output_buffer = io.BytesIO()
  26. img.save(output_buffer, format="JPEG", quality=IMAGE_QUALITY, optimize=True)
  27. return output_buffer.getvalue()
  28. async def extract_receipt_data(image_content: bytes, user_name: str, department: str) -> ExtractionResponse:
  29. # 1. Compress Image
  30. compressed_content = compress_image(image_content)
  31. base64_image = base64.b64encode(compressed_content).decode("utf-8")
  32. # 2. Refined Prompt
  33. prompt = (
  34. f"You are a cautious auditor helping an HR department in Malaysia. "
  35. f"Extract the requested fields from the provided medical receipt image. "
  36. f"The employee submitting this is {user_name} from {department}. "
  37. f"IMPORTANT: The context is Malaysia (MYR). "
  38. f"For the fields `receipt_ref_no` and `clinic_reg_no`, only provide a value if you can read it clearly without any guessing or inference. If the text is smudged, handwritten, or ambiguous, return `null`. "
  39. f"Map the clinic/services to a `claim_category` from: [General, Dental, Optical, Specialist] based on the clinic name or invoice items. "
  40. f"Provide a 1-sentence `diagnosis_brief` summarizing the services seen (e.g. 'Fever consultation and medicine'). "
  41. f"Set `needs_manual_review` to `true` and provide a low `confidence_score` if: "
  42. f"1. The 'Total' does not match the sum of the individual items. "
  43. f"2. The receipt looks hand-written and lacks an official stamp. "
  44. f"3. The provider name is missing or the amount looks altered. "
  45. f"4. The user's name ({user_name}) is not clearly visible on the receipt. "
  46. f"IMPORTANT: Fill the `ai_reasoning` field with a 1-sentence explanation of how you identified the clinic and category."
  47. )
  48. # 3. Async Extraction
  49. completion = await client.beta.chat.completions.parse(
  50. model="gpt-4o-mini",
  51. messages=[
  52. {
  53. "role": "system",
  54. "content": "You are an HR data entry assistant. Extract medical receipt data accurately into structured JSON."
  55. },
  56. {
  57. "role": "user",
  58. "content": [
  59. {"type": "text", "text": prompt},
  60. {
  61. "type": "image_url",
  62. "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
  63. },
  64. ],
  65. }
  66. ],
  67. response_format=ExtractionResponse,
  68. )
  69. result = completion.choices[0].message.parsed
  70. # 4. Logging for Demo
  71. if result:
  72. logger.info(f"Extraction complete for {user_name}. Confidence Score: {result.confidence_score}")
  73. if result.needs_manual_review:
  74. logger.warning(f"Manual review required for receipt submitted by {user_name}")
  75. return result