Distopia22 commited on
Commit
a8f12f6
·
1 Parent(s): c9bda9d

Add file upload functionality for provider notes

Browse files
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
  fastapi==0.104.1
2
- uvicorn[standard]==0.24.0
3
- groq==0.11.0
4
- pydantic==2.5.0
5
  python-dotenv==1.0.0
6
- httpx==0.27.0
 
 
 
1
  fastapi==0.104.1
2
+ uvicorn==0.24.0
 
 
3
  python-dotenv==1.0.0
4
+ groq==0.4.1
5
+ pydantic==2.5.0
6
+ python-multipart==0.0.6
src/api/routes.py CHANGED
@@ -1,34 +1,97 @@
1
- from fastapi import APIRouter, HTTPException
2
- import sys
3
- import os
4
-
5
- # Add parent directory to path for imports
6
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
-
8
- from models.request_models import ProviderNotesRequest
9
  from models.response_models import CodingResponse
10
  from services.groq_service import groq_service
 
 
 
 
 
11
 
12
- router = APIRouter(prefix="/api/v1", tags=["Medical Coding"])
13
 
14
- @router.post("/analyze", response_model=CodingResponse)
 
15
  async def analyze_provider_notes(request: ProviderNotesRequest):
16
  """
17
- Analyze provider notes and return ICD-10 and CPT codes with explanations.
18
 
19
- - **provider_notes**: The medical provider notes to analyze
20
-
21
- Returns ICD-10 codes, CPT codes, and explanations for each.
22
  """
23
  try:
24
- result = await groq_service.analyze_provider_notes(request.provider_notes)
25
- return CodingResponse(**result)
26
- except ValueError as e:
27
- raise HTTPException(status_code=422, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  except Exception as e:
29
- raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
 
 
 
 
 
30
 
31
- @router.get("/health")
32
- async def health_check():
33
- """Health check endpoint"""
34
- return {"status": "healthy", "service": "ICD-CPT Coding API"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, UploadFile, File
2
+ from models.request_models import ProviderNotesRequest, FileUploadResponse
 
 
 
 
 
 
3
  from models.response_models import CodingResponse
4
  from services.groq_service import groq_service
5
+ from services.file_service import file_service
6
+ import logging
7
+
8
+ router = APIRouter()
9
+ logger = logging.getLogger(__name__)
10
 
 
11
 
12
+ # EXISTING ENDPOINT - DO NOT CHANGE
13
+ @router.post("/coding", response_model=CodingResponse)
14
  async def analyze_provider_notes(request: ProviderNotesRequest):
15
  """
16
+ Analyze provider notes and extract ICD-10 and CPT codes
17
 
18
+ This endpoint accepts provider notes as text input.
 
 
19
  """
20
  try:
21
+ logger.info("Received coding request")
22
+
23
+ # Get provider notes from request
24
+ provider_notes = request.provider_notes
25
+
26
+ if not provider_notes or len(provider_notes.strip()) < 10:
27
+ raise HTTPException(
28
+ status_code=400,
29
+ detail="Provider notes must be at least 10 characters long"
30
+ )
31
+
32
+ # Process through Groq service
33
+ result = await groq_service.analyze_notes(provider_notes)
34
+
35
+ logger.info("Successfully processed coding request")
36
+ return result
37
+
38
+ except HTTPException:
39
+ raise
40
  except Exception as e:
41
+ logger.error(f"Error in analyze_provider_notes: {str(e)}")
42
+ raise HTTPException(
43
+ status_code=500,
44
+ detail=f"Error processing request: {str(e)}"
45
+ )
46
+
47
 
48
+ # NEW ENDPOINT - File Upload
49
+ @router.post("/upload-file", response_model=FileUploadResponse)
50
+ async def upload_provider_notes_file(file: UploadFile = File(...)):
51
+ """
52
+ Upload a TXT file containing provider notes and extract ICD-10 and CPT codes
53
+
54
+ This endpoint accepts a TXT file, extracts the text, and processes it through the LLM.
55
+
56
+ Args:
57
+ file: TXT file containing provider notes
58
+
59
+ Returns:
60
+ FileUploadResponse with extracted codes and explanations
61
+ """
62
+ try:
63
+ logger.info(f"Received file upload request: {file.filename}")
64
+
65
+ # Step 1: Extract text from uploaded file
66
+ extraction_result = await file_service.extract_text_from_file(file)
67
+
68
+ extracted_text = extraction_result["text"]
69
+ filename = extraction_result["filename"]
70
+ text_length = extraction_result["text_length"]
71
+
72
+ logger.info(f"Extracted {text_length} characters from {filename}")
73
+
74
+ # Step 2: Process extracted text through Groq LLM
75
+ coding_result = await groq_service.analyze_notes(extracted_text)
76
+
77
+ logger.info(f"Successfully processed file: {filename}")
78
+
79
+ # Step 3: Return combined response
80
+ return FileUploadResponse(
81
+ success=True,
82
+ filename=filename,
83
+ extracted_text_length=text_length,
84
+ cpt_codes=coding_result.cpt_codes,
85
+ cpt_explanation=coding_result.cpt_explanation,
86
+ icd_codes=coding_result.icd_codes,
87
+ icd_explanation=coding_result.icd_explanation
88
+ )
89
+
90
+ except HTTPException:
91
+ raise
92
+ except Exception as e:
93
+ logger.error(f"Error in upload_provider_notes_file: {str(e)}")
94
+ raise HTTPException(
95
+ status_code=500,
96
+ detail=f"Error processing uploaded file: {str(e)}"
97
+ )
src/main.py CHANGED
@@ -1,19 +1,17 @@
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
3
- import sys
4
- import os
5
-
6
- # Add current directory to path
7
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
8
-
9
  from api.routes import router
 
 
 
 
 
 
10
 
11
  app = FastAPI(
12
- title="ICD-10 and CPT Coding API",
13
- description="Analyzes medical provider notes and returns appropriate ICD-10 and CPT codes with explanations",
14
- version="1.0.0",
15
- docs_url="/docs",
16
- redoc_url="/redoc"
17
  )
18
 
19
  # CORS middleware
@@ -25,17 +23,24 @@ app.add_middleware(
25
  allow_headers=["*"],
26
  )
27
 
28
- # Include routers
29
- app.include_router(router)
30
 
31
  @app.get("/")
32
  async def root():
33
  return {
34
- "message": "ICD-10 and CPT Coding API",
35
- "docs": "/docs",
36
- "health": "/api/v1/health"
 
 
 
 
37
  }
38
 
 
 
 
 
39
  if __name__ == "__main__":
40
- import uvicorn
41
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  from fastapi import FastAPI
2
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
 
 
 
3
  from api.routes import router
4
+ import uvicorn
5
+ import logging
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger(__name__)
10
 
11
  app = FastAPI(
12
+ title="ICD-10 & CPT Code Analyzer",
13
+ description="AI-powered medical coding assistant",
14
+ version="1.1.0"
 
 
15
  )
16
 
17
  # CORS middleware
 
23
  allow_headers=["*"],
24
  )
25
 
26
+ # Include API routes
27
+ app.include_router(router, prefix="/api")
28
 
29
  @app.get("/")
30
  async def root():
31
  return {
32
+ "message": "ICD-10 & CPT Code Analyzer API",
33
+ "version": "1.1.0",
34
+ "endpoints": {
35
+ "text_input": "/api/coding",
36
+ "file_upload": "/api/upload-file",
37
+ "docs": "/docs"
38
+ }
39
  }
40
 
41
+ @app.get("/health")
42
+ async def health_check():
43
+ return {"status": "healthy"}
44
+
45
  if __name__ == "__main__":
 
46
  uvicorn.run(app, host="0.0.0.0", port=7860)
src/models/request_models.py CHANGED
@@ -22,4 +22,29 @@ class CodingResponse(BaseModel):
22
  cpt_codes: list
23
  cpt_explanation: str
24
  icd_codes: list
25
- icd_explanation: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  cpt_codes: list
23
  cpt_explanation: str
24
  icd_codes: list
25
+ icd_explanation: str
26
+
27
+
28
+ # NEW: File upload response model
29
+ class FileUploadResponse(BaseModel):
30
+ """Response model for file upload endpoint"""
31
+ success: bool = Field(description="Whether file processing was successful")
32
+ filename: str = Field(description="Name of uploaded file")
33
+ extracted_text_length: int = Field(description="Length of extracted text")
34
+ cpt_codes: list = Field(description="List of CPT codes")
35
+ cpt_explanation: str = Field(description="Explanation of CPT codes")
36
+ icd_codes: list = Field(description="List of ICD codes")
37
+ icd_explanation: str = Field(description="Explanation of ICD codes")
38
+
39
+ class Config:
40
+ json_schema_extra = {
41
+ "example": {
42
+ "success": True,
43
+ "filename": "provider_notes.txt",
44
+ "extracted_text_length": 450,
45
+ "cpt_codes": ["99213", "93000"],
46
+ "cpt_explanation": "Office visit and EKG",
47
+ "icd_codes": ["I20.0", "R07.9"],
48
+ "icd_explanation": "Unstable angina and chest pain"
49
+ }
50
+ }
src/services/file_service.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import UploadFile, HTTPException
2
+ import os
3
+ from typing import Dict
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class FileService:
10
+ """Service to handle file uploads and text extraction"""
11
+
12
+ ALLOWED_EXTENSIONS = {'.txt'}
13
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
14
+
15
+ @staticmethod
16
+ def validate_file(file: UploadFile) -> None:
17
+ """
18
+ Validate uploaded file
19
+
20
+ Args:
21
+ file: Uploaded file object
22
+
23
+ Raises:
24
+ HTTPException: If file is invalid
25
+ """
26
+ # Check if file exists
27
+ if not file:
28
+ raise HTTPException(status_code=400, detail="No file provided")
29
+
30
+ # Check file extension
31
+ file_ext = os.path.splitext(file.filename)[1].lower()
32
+ if file_ext not in FileService.ALLOWED_EXTENSIONS:
33
+ raise HTTPException(
34
+ status_code=400,
35
+ detail=f"Invalid file type. Only {', '.join(FileService.ALLOWED_EXTENSIONS)} files are allowed"
36
+ )
37
+
38
+ @staticmethod
39
+ async def extract_text_from_file(file: UploadFile) -> Dict[str, any]:
40
+ """
41
+ Extract text content from uploaded file
42
+
43
+ Args:
44
+ file: Uploaded file object
45
+
46
+ Returns:
47
+ Dictionary containing extracted text and metadata
48
+ """
49
+ try:
50
+ # Validate file
51
+ FileService.validate_file(file)
52
+
53
+ # Read file content
54
+ content = await file.read()
55
+
56
+ # Check file size
57
+ file_size = len(content)
58
+ if file_size > FileService.MAX_FILE_SIZE:
59
+ raise HTTPException(
60
+ status_code=400,
61
+ detail=f"File too large. Maximum size is {FileService.MAX_FILE_SIZE / (1024*1024)} MB"
62
+ )
63
+
64
+ # Decode text
65
+ try:
66
+ text = content.decode('utf-8')
67
+ except UnicodeDecodeError:
68
+ try:
69
+ text = content.decode('latin-1')
70
+ except Exception as e:
71
+ raise HTTPException(
72
+ status_code=400,
73
+ detail="Unable to decode file. Please ensure it's a valid text file"
74
+ )
75
+
76
+ # Validate extracted text
77
+ if not text.strip():
78
+ raise HTTPException(
79
+ status_code=400,
80
+ detail="File is empty or contains no readable text"
81
+ )
82
+
83
+ if len(text.strip()) < 10:
84
+ raise HTTPException(
85
+ status_code=400,
86
+ detail="Extracted text is too short. Please provide more detailed provider notes"
87
+ )
88
+
89
+ logger.info(f"Successfully extracted {len(text)} characters from {file.filename}")
90
+
91
+ return {
92
+ "text": text,
93
+ "filename": file.filename,
94
+ "file_size": file_size,
95
+ "text_length": len(text)
96
+ }
97
+
98
+ except HTTPException:
99
+ raise
100
+ except Exception as e:
101
+ logger.error(f"Error extracting text from file: {str(e)}")
102
+ raise HTTPException(
103
+ status_code=500,
104
+ detail=f"Error processing file: {str(e)}"
105
+ )
106
+
107
+
108
+ # Singleton instance
109
+ file_service = FileService()
tests/test_api.py CHANGED
@@ -1,11 +1,13 @@
1
  from fastapi.testclient import TestClient
2
  from src.main import app
 
3
 
4
  client = TestClient(app)
5
 
6
  def test_coding_endpoint():
 
7
  provider_notes = {
8
- "notes": "Patient has a fever and cough."
9
  }
10
 
11
  response = client.post("/api/coding", json=provider_notes)
@@ -13,9 +15,56 @@ def test_coding_endpoint():
13
  assert response.status_code == 200
14
  data = response.json()
15
 
16
- assert "CPT" in data
17
- assert "ICD" in data
18
- assert isinstance(data["CPT"], list)
19
- assert isinstance(data["ICD"], list)
20
- assert "explanation" in data["CPT"][0]
21
- assert "explanation" in data["ICD"][0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi.testclient import TestClient
2
  from src.main import app
3
+ from io import BytesIO
4
 
5
  client = TestClient(app)
6
 
7
  def test_coding_endpoint():
8
+ """Test existing text input endpoint"""
9
  provider_notes = {
10
+ "provider_notes": "Patient has a fever and cough."
11
  }
12
 
13
  response = client.post("/api/coding", json=provider_notes)
 
15
  assert response.status_code == 200
16
  data = response.json()
17
 
18
+ assert "cpt_codes" in data
19
+ assert "icd_codes" in data
20
+ assert isinstance(data["cpt_codes"], list)
21
+ assert isinstance(data["icd_codes"], list)
22
+
23
+
24
+ def test_file_upload_endpoint():
25
+ """Test new file upload endpoint"""
26
+ # Create a sample TXT file
27
+ file_content = b"Patient John Doe presents with acute bronchitis. Cough for 5 days, productive with yellow sputum. Lung exam reveals diffuse wheezing."
28
+
29
+ files = {
30
+ "file": ("provider_notes.txt", BytesIO(file_content), "text/plain")
31
+ }
32
+
33
+ response = client.post("/api/upload-file", files=files)
34
+
35
+ assert response.status_code == 200
36
+ data = response.json()
37
+
38
+ assert data["success"] is True
39
+ assert data["filename"] == "provider_notes.txt"
40
+ assert data["extracted_text_length"] > 0
41
+ assert "cpt_codes" in data
42
+ assert "icd_codes" in data
43
+ assert isinstance(data["cpt_codes"], list)
44
+ assert isinstance(data["icd_codes"], list)
45
+
46
+
47
+ def test_file_upload_invalid_extension():
48
+ """Test file upload with invalid file type"""
49
+ file_content = b"Some content"
50
+
51
+ files = {
52
+ "file": ("document.pdf", BytesIO(file_content), "application/pdf")
53
+ }
54
+
55
+ response = client.post("/api/upload-file", files=files)
56
+
57
+ assert response.status_code == 400
58
+
59
+
60
+ def test_file_upload_empty_file():
61
+ """Test file upload with empty file"""
62
+ file_content = b""
63
+
64
+ files = {
65
+ "file": ("empty.txt", BytesIO(file_content), "text/plain")
66
+ }
67
+
68
+ response = client.post("/api/upload-file", files=files)
69
+
70
+ assert response.status_code == 400