소스 검색

Initial commit: AI-Assisted Data Entry Backend

Dr-Swopt 1 주 전
커밋
0973cdb88a

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+.env
+__pycache__/
+*.pyc
+venv/
+.pytest_cache/
+.vscode/
+.idea/

+ 57 - 0
README.md

@@ -0,0 +1,57 @@
+# AI-Assisted Smart Data Entry Backend
+
+This project provides a FastAPI-based backend for extracting structured data from medical receipts using OpenAI's GPT-4o model with structured outputs.
+
+## Features
+
+- **FastAPI**: Modern, fast web framework for building APIs.
+- **Structured Extraction**: Uses OpenAI's `beta.chat.completions.parse` to ensure extracted data adheres to a Pydantic schema.
+- **CORS Enabled**: Configured to allow access from frontend applications (e.g., Angular).
+- **Health Check**: Simple endpoint to verify server status.
+
+## Setup
+
+1. **Install Dependencies**:
+   ```bash
+   pip install -r requirements.txt
+   ```
+
+2. **Configure Environment**:
+   Create a `.env` file in the root directory and add your OpenAI API key:
+   ```env
+   OPENAI_API_KEY=your_api_key_here
+   ```
+
+3. **Run the Server**:
+   ```bash
+   python main.py
+   ```
+   Or using uvicorn:
+   ```bash
+   uvicorn main:app --reload
+   ```
+
+## API Endpoints
+
+### `GET /health`
+Returns the status of the server.
+
+### `POST /api/v1/extract`
+Extracts data from a medical receipt image.
+
+**Request Body**:
+- `file`: Multipart image file.
+- `user_name` (optional): Name of the employee.
+- `department` (optional): Department of the employee.
+
+**Response**:
+Returns a structured JSON matching the `ExtractionResponse` schema.
+
+## Project Structure
+
+- `main.py`: Entry point and API endpoints.
+- `schemas.py`: Pydantic models for data validation and structured output.
+- `services/`:
+  - `openai_service.py`: Logic for interacting with OpenAI API.
+- `requirements.txt`: Python dependencies.
+- `.env`: Environment variables (not tracked by git).

+ 58 - 0
main.py

@@ -0,0 +1,58 @@
+import os
+from fastapi import FastAPI, UploadFile, File, Header, HTTPException, status
+from fastapi.middleware.cors import CORSMiddleware
+from typing import Optional
+from services.openai_service import extract_receipt_data
+from schemas import ExtractionResponse
+from dotenv import load_dotenv
+
+load_dotenv()
+
+app = FastAPI(title="AI-Assisted Data Entry API")
+
+# Configure CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],  # Adjust as needed for Angular frontend
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+@app.get("/health")
+async def health_check():
+    return {"status": "healthy"}
+
+@app.post("/api/v1/extract", response_model=ExtractionResponse)
+async def extract_receipt(
+    file: UploadFile = File(...),
+    user_id: Optional[str] = Header(None),
+    user_name: str = "Unknown Employee",
+    department: str = "Unknown Department"
+):
+    if not file.content_type.startswith("image/"):
+        raise HTTPException(
+            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+            detail="File provided is not an image."
+        )
+    
+    try:
+        content = await file.read()
+        extraction_result = await extract_receipt_data(content, user_name, department)
+        
+        if extraction_result is None:
+             raise HTTPException(
+                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+                detail="Could not extract data from the provided image."
+            )
+            
+        return extraction_result
+    except Exception as e:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=f"An error occurred during extraction: {str(e)}"
+        )
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run(app, host="0.0.0.0", port=8000)

+ 6 - 0
requirements.txt

@@ -0,0 +1,6 @@
+fastapi
+uvicorn
+openai
+python-dotenv
+python-multipart
+pydantic

BIN
sample_medical_receipts/ET3NmELUEAEXVu3.jpg


BIN
sample_medical_receipts/MY-1.jpg


BIN
sample_medical_receipts/cuepacscare3.jpg


BIN
sample_medical_receipts/kpj1.jpg


BIN
sample_medical_receipts/sample_Med_claim.jpg


+ 11 - 0
schemas.py

@@ -0,0 +1,11 @@
+from pydantic import BaseModel, Field
+from typing import List, Optional
+
+class ExtractionResponse(BaseModel):
+    provider_name: str = Field(description="The name of the clinic or hospital.")
+    visit_date: str = Field(description="The date of service in YYYY-MM-DD format.")
+    total_amount: float = Field(description="The total amount paid.")
+    currency: str = Field(description="3-letter currency code (e.g. USD, SGD).")
+    items: List[str] = Field(description="Simplified list of services (e.g. 'Consultation', 'Medicine').")
+    confidence_score: float = Field(description="Model's confidence from 0.0 to 1.0.")
+    needs_manual_review: bool = Field(description="Set to true if text is blurry or data is ambiguous.")

+ 33 - 0
services/openai_service.py

@@ -0,0 +1,33 @@
+import os
+import base64
+from openai import OpenAI
+from dotenv import load_dotenv
+from schemas import ExtractionResponse
+
+load_dotenv()
+
+client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
+
+async def extract_receipt_data(image_content: bytes, user_name: str, department: str) -> ExtractionResponse:
+    base64_image = base64.b64encode(image_content).decode("utf-8")
+    
+    prompt = f"You are an HR data entry assistant. Extract the requested fields from the provided medical receipt image. The employee submitting this is {user_name} from {department}. If the date is missing, look for a 'Payment Date' as a fallback."
+    
+    completion = client.beta.chat.completions.parse(
+        model="gpt-4o",
+        messages=[
+            {
+                "role": "user",
+                "content": [
+                    {"type": "text", "text": prompt},
+                    {
+                        "type": "image_url",
+                        "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
+                    },
+                ],
+            }
+        ],
+        response_format=ExtractionResponse,
+    )
+    
+    return completion.choices[0].message.parsed