Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from flask import Flask\nfrom .config import Config\nfrom .routes import main as main_blueprint\ndef create_app():\n app = Flask(__name__)\n app.config.from_object(Config)\n app.register_blueprint(main_blueprint)\n return app
4 changes: 3 additions & 1 deletion app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .text_extract import extract_text
from .text_extract import extract_text
from .pdf_extract import extract_pdf
from .structured_extract import extract_structured
180 changes: 180 additions & 0 deletions app/api/structured_extract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from typing import List, Optional
import time
import os

from app.domain import ocr, pdf_ocr, fileUpload, structured_extraction
from app.model.StructuredSchema import StructuredExtractionResult
from app.services import storage

router = APIRouter()

@router.post("/extract_structured", response_model=List[StructuredExtractionResult])
async def extract_structured(

Check failure on line 13 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 73 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEf&open=AZ2NOj6WJpYMQiJwULEf&pullRequest=97
input_files: Optional[List[UploadFile]] = File(None, description="List of files (Images or PDFs) to process (required if source='upload')"),

Check failure on line 14 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEg&open=AZ2NOj6WJpYMQiJwULEg&pullRequest=97
source: str = Form("upload", description="Source of the file: 'upload' or 'object_storage'"),

Check failure on line 15 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEh&open=AZ2NOj6WJpYMQiJwULEh&pullRequest=97
client_id: Optional[str] = Form(None, description="Client ID for object storage (required if source='object_storage')"),

Check failure on line 16 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEi&open=AZ2NOj6WJpYMQiJwULEi&pullRequest=97
object_keys: Optional[List[str]] = Form(None, description="List of object keys in the bucket (required if source='object_storage')"),

Check failure on line 17 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEj&open=AZ2NOj6WJpYMQiJwULEj&pullRequest=97
lang: str = Form("eng+por", description="Language code (eng, por, eng+por)."),

Check failure on line 18 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEk&open=AZ2NOj6WJpYMQiJwULEk&pullRequest=97
mode: str = Form("fast", description="OCR mode: 'fast' or 'accurate'."),

Check failure on line 19 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEl&open=AZ2NOj6WJpYMQiJwULEl&pullRequest=97
auto_detect: bool = Form(False, description="Enable Orientation and Script Detection (OSD)."),

Check failure on line 20 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEm&open=AZ2NOj6WJpYMQiJwULEm&pullRequest=97
model_provider: str = Form("local_tesseract", description="Provider for structured extraction (e.g., 'local_tesseract', 'openai', 'ollama', 'azure')"),

Check failure on line 21 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEn&open=AZ2NOj6WJpYMQiJwULEn&pullRequest=97
model_name: str = Form("", description="Model name to use (e.g., 'gpt-4o-mini', 'llama3'). Required if provider is not 'local_tesseract'"),

Check failure on line 22 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEo&open=AZ2NOj6WJpYMQiJwULEo&pullRequest=97
force_processing: bool = Form(False, description="Force PDF processing even if page count > 10.")

Check failure on line 23 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEp&open=AZ2NOj6WJpYMQiJwULEp&pullRequest=97
):
"""
Extract structured data from uploaded images or PDFs.
First performs OCR, then optionally passes the raw text to an LLM for structured JSON output.
"""

if mode not in ["fast", "accurate"]:
raise HTTPException(status_code=400, detail="Invalid mode. Use 'fast' or 'accurate'.")

Check failure on line 31 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEq&open=AZ2NOj6WJpYMQiJwULEq&pullRequest=97

if lang == "auto":
auto_detect = True
lang = "eng+por"

if model_provider != "local_tesseract" and not model_name:
raise HTTPException(status_code=400, detail="model_name is required when model_provider is not 'local_tesseract'.")

Check failure on line 38 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEr&open=AZ2NOj6WJpYMQiJwULEr&pullRequest=97

results = []

if source == "upload":
if not input_files:
raise HTTPException(status_code=400, detail="input_files is required when source is 'upload'.")

Check failure on line 44 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEs&open=AZ2NOj6WJpYMQiJwULEs&pullRequest=97

if client_id or object_keys:
raise HTTPException(status_code=400, detail="Ambiguous request: cannot provide both upload files and object storage parameters.")

Check failure on line 47 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEt&open=AZ2NOj6WJpYMQiJwULEt&pullRequest=97

for file in input_files:
start_time = time.time()
filename = file.filename

# Determine file type
is_pdf = filename.lower().endswith(".pdf")

# Save file
temp_file = fileUpload._save_file_to_server(file)

try:
# Perform OCR
if is_pdf:
# Basic PDF header check
try:
with open(temp_file, "rb") as f:

Check failure on line 64 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use an asynchronous file API instead of synchronous open() in this async function.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULE3&open=AZ2NOj6WJpYMQiJwULE3&pullRequest=97
header = f.read(5)
if header != b"%PDF-":
raise HTTPException(status_code=400, detail="Downloaded file is not a valid PDF.")

Check failure on line 67 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEu&open=AZ2NOj6WJpYMQiJwULEu&pullRequest=97
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error validating file: {e}")

Check failure on line 71 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEv&open=AZ2NOj6WJpYMQiJwULEv&pullRequest=97

raw_text = await pdf_ocr.process_pdf(
file_path=temp_file,
lang=lang,
mode=mode,
auto=auto_detect,
force_processing=force_processing
)
else:
fileUpload.validate_image_file(file)
raw_text = await ocr.read_image(
img_path=temp_file,
lang=lang,
mode=mode,
auto=auto_detect
)

# Extract Structured Data
structured_data = structured_extraction.extract_structured_data(
text=raw_text,
provider=model_provider,
model_name=model_name,
lang_hint=lang
)

finally:
if os.path.exists(temp_file):
os.remove(temp_file)

time_taken = str(round((time.time() - start_time), 2))

results.append(StructuredExtractionResult(
file_name=filename or "unknown",
provider_used=model_provider,
model_used=model_name if model_provider != "local_tesseract" else None,
data=structured_data,
time_taken=time_taken
))

elif source == "object_storage":
if input_files:
raise HTTPException(status_code=400, detail="Ambiguous request: cannot provide both upload files and object storage parameters.")

Check failure on line 113 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEw&open=AZ2NOj6WJpYMQiJwULEw&pullRequest=97

if not client_id or not object_keys:
raise HTTPException(status_code=400, detail="client_id and object_keys are required when source is 'object_storage'.")

Check failure on line 116 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEx&open=AZ2NOj6WJpYMQiJwULEx&pullRequest=97

for key in object_keys:
start_time = time.time()

is_pdf = key.lower().endswith(".pdf")
temp_file = None

try:
try:
temp_file = storage.download_file_from_storage(client_id, key)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

Check failure on line 128 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEy&open=AZ2NOj6WJpYMQiJwULEy&pullRequest=97
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error retrieving file {key} from storage.")

Check failure on line 130 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 500 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULEz&open=AZ2NOj6WJpYMQiJwULEz&pullRequest=97

if is_pdf:
try:
with open(temp_file, "rb") as f:

Check failure on line 134 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use an asynchronous file API instead of synchronous open() in this async function.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULE4&open=AZ2NOj6WJpYMQiJwULE4&pullRequest=97
header = f.read(5)
if header != b"%PDF-":
raise HTTPException(status_code=400, detail="Downloaded file is not a valid PDF.")

Check failure on line 137 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULE0&open=AZ2NOj6WJpYMQiJwULE0&pullRequest=97
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error validating file: {e}")

Check failure on line 141 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULE1&open=AZ2NOj6WJpYMQiJwULE1&pullRequest=97

raw_text = await pdf_ocr.process_pdf(
file_path=temp_file,
lang=lang,
mode=mode,
auto=auto_detect,
force_processing=force_processing
)
else:
raw_text = await ocr.read_image(
img_path=temp_file,
lang=lang,
mode=mode,
auto=auto_detect
)
Comment on lines +132 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate downloaded non-PDF files before sending them to OCR.

The upload path validates images via fileUpload.validate_image_file(...), but the object-storage path sends any non-.pdf blob straight to ocr.read_image(...). That means a bad key or unexpected file type bypasses the same guardrails as uploads.

🧰 Tools
🪛 Ruff (0.15.9)

[warning] 140-140: Do not catch blind exception: Exception

(BLE001)


[warning] 141-141: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/structured_extract.py` around lines 132 - 156, Non-PDF blobs sent to
ocr.read_image bypass image validation; call the existing image validator
(fileUpload.validate_image_file or the corresponding validate function used for
uploads) on temp_file in the else branch before calling ocr.read_image, and if
validation fails raise an HTTPException(400) with a clear message; ensure you
catch/propagate validation exceptions similar to the PDF branch so invalid files
are rejected prior to calling ocr.read_image (refer to temp_file, is_pdf,
pdf_ocr.process_pdf, ocr.read_image and fileUpload.validate_image_file to locate
the code).


# Extract Structured Data
structured_data = structured_extraction.extract_structured_data(
text=raw_text,
provider=model_provider,
model_name=model_name,
lang_hint=lang
)

time_taken = str(round((time.time() - start_time), 2))
results.append(StructuredExtractionResult(
file_name=key,
provider_used=model_provider,
model_used=model_name if model_provider != "local_tesseract" else None,
data=structured_data,
time_taken=time_taken
))
finally:
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)
else:
raise HTTPException(status_code=400, detail="Invalid source. Use 'upload' or 'object_storage'.")

Check failure on line 178 in app/api/structured_extract.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Document this HTTPException with status code 400 in the "responses" parameter.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj6WJpYMQiJwULE2&open=AZ2NOj6WJpYMQiJwULE2&pullRequest=97

return results
1 change: 0 additions & 1 deletion app/config.py

This file was deleted.

77 changes: 77 additions & 0 deletions app/domain/structured_extraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import litellm
import instructor
from litellm import completion
from app.model.StructuredSchema import StructuredDocumentData, DocumentEntity
from app.domain import ner
import logging

log = logging.getLogger("uvicorn")

def extract_structured_data(
text: str,
provider: str = "local_tesseract",
model_name: str = "",
lang_hint: str = "eng+por"
) -> StructuredDocumentData:
"""
Extracts structured data from raw text.
If provider is 'local_tesseract', returns a fallback structure using heuristics and spaCy.
Otherwise, uses litellm + instructor to ask the specified model for the StructuredDocumentData.
"""

if provider == "local_tesseract" or not model_name:
# Fallback to local heuristic extraction
log.info("Using local fallback for structured extraction (no LLM)")

# Extract entities using existing spaCy NER
spacy_entities = ner.extract_entities(text, lang_hint=lang_hint)
entities = [
DocumentEntity(text=e["text"], label=e["label"])
for e in spacy_entities
]

return StructuredDocumentData(
document_type="unknown",
key_values=[],
tables=[],
entities=entities,
text_summary=f"Fallback extraction. Extracted {len(text)} characters of text."
)

# Use litellm with instructor to get structured JSON
log.info(f"Using LLM for structured extraction: provider={provider}, model={model_name}")

Check warning on line 42 in app/domain/structured_extraction.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this code to not log user-controlled data.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj8rJpYMQiJwULE5&open=AZ2NOj8rJpYMQiJwULE5&pullRequest=97

Check notice

Code scanning / SonarCloud

Logging should not be vulnerable to injection attacks Low

Change this code to not log user-controlled data. See more on SonarQube Cloud

# Patch litellm completion with instructor
client = instructor.from_litellm(completion)

# Prefix the model with the provider if litellm requires it
# litellm format is usually "provider/model_name"
full_model_name = f"{provider}/{model_name}" if "/" not in model_name and provider != "openai" else model_name

prompt = (
"You are an expert data extraction assistant. I will provide you with the raw OCR text of a document. "
"Your task is to analyze the text and extract structured information, including the type of the document, "
"any key-value pairs (like invoice numbers, dates, totals, names), any tables you can identify, "
"and a brief summary. Also extract important named entities.\n\n"
"Here is the raw document text:\n"
"------------------\n"
f"{text}\n"
"------------------\n"
)

try:
# instructor will handle forcing the StructuredDocumentData schema
structured_data = client.chat.completions.create(
model=full_model_name,
messages=[
{"role": "system", "content": "You are a precise data extraction AI."},
{"role": "user", "content": prompt}
],
response_model=StructuredDocumentData,
max_tokens=4096,
)
Comment on lines +45 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that the FastAPI handler is async, calls the helper without `await`,
# and that the helper performs a synchronous Instructor/LiteLLM completion call.
set -euo pipefail

echo "== async endpoint calling structured_extraction.extract_structured_data =="
rg -n -C2 'async def extract_structured|structured_extraction\.extract_structured_data\(' app/api/structured_extract.py

echo
echo "== sync helper using litellm completion + blocking create(...) call =="
rg -n -C2 'def extract_structured_data|from_litellm|client\.chat\.completions\.create' app/domain/structured_extraction.py

Repository: gomesrocha/api_ocr

Length of output: 1561


🏁 Script executed:

# Check if there's any threadpool wrapping around the extract_structured_data calls
rg -n -B5 -A5 'structured_extraction\.extract_structured_data' app/api/structured_extract.py | head -60

Repository: gomesrocha/api_ocr

Length of output: 937


🏁 Script executed:

# Check imports in the structured_extract.py to see if asyncio utilities are imported
head -20 app/api/structured_extract.py

Repository: gomesrocha/api_ocr

Length of output: 1260


Don't make a blocking LLM call from the async request path.

extract_structured_data() is a synchronous function that's called directly from async def extract_structured(...) without wrapping in a threadpool executor. The function performs a blocking I/O call with client.chat.completions.create(...) (line 64), which blocks the event loop. Under load, this serializes requests and stalls the entire async handler. Wrap the call with asyncio.to_thread() or starlette.concurrency.run_in_threadpool(), or convert the function to async with an async HTTP client.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/domain/structured_extraction.py` around lines 45 - 72, The blocking call
to client.chat.completions.create inside extract_structured_data() (which is
invoked from async extract_structured(...)) must be moved off the event loop;
wrap the synchronous call to client.chat.completions.create(...,
response_model=StructuredDocumentData, ...) in a thread executor (e.g. use
asyncio.to_thread(...) or starlette.concurrency.run_in_threadpool(...)) or
alternatively convert extract_structured_data() to async with a non-blocking
client; ensure you return the StructuredDocumentData result from the threaded
call and preserve the same parameters (model=full_model_name, messages,
max_tokens) when invoking the client.

return structured_data
except Exception as e:
log.error(f"Error during LLM structured extraction: {e}")
# Return fallback on error
return extract_structured_data(text, "local_tesseract", "", lang_hint)
Comment on lines +74 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Surface remote-provider failures instead of silently downgrading.

This catch-all fallback returns local data after any LLM error, but the API layer still reports the requested provider_used / model_used. That makes a degraded local result look like a successful remote extraction. Please either propagate an error or return enough metadata for the caller to see that fallback happened.

🧰 Tools
🪛 Ruff (0.15.9)

[warning] 74-74: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/domain/structured_extraction.py` around lines 74 - 77, The current except
block in structured_extraction.py that catches Exception as e and returns
extract_structured_data(text, "local_tesseract", "", lang_hint) silently hides
remote LLM failures; update the error handling in the except of the LLM
extraction path (the block surrounding extract_structured_data call) to either
re-raise the original exception or return a structured fallback that includes
explicit metadata (e.g., provider_used="local_tesseract", model_used="" plus an
error field or fallback_flag=true and original_error message) so callers can
detect the downgrade; ensure this change is applied around the function/method
performing LLM structured extraction and that any return type changes are
propagated to callers or documented.

25 changes: 8 additions & 17 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn
from app.api import text_extract, pdf_extract, structured_extract

app = FastAPI()
app = FastAPI(title="API_OCR")

class Item(BaseModel):
name: str
description: str | None = None
price: float
app.include_router(text_extract.router)
app.include_router(pdf_extract.router)
app.include_router(structured_extract.router)

@app.get('/')
def read_root():
return {'message': 'Welcome to the FastAPI app!'}

@app.post('/items/')
def create_item(item: Item):
return item

if __name__ == '__main__':
uvicorn.run('main:app', host='127.0.0.1', port=8000, reload=True)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)

Check failure on line 12 in app/main.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid binding the application to all network interfaces.

See more on https://sonarcloud.io/project/issues?id=gomesrocha_api_ocr&issues=AZ2NOj88JpYMQiJwULE6&open=AZ2NOj88JpYMQiJwULE6&pullRequest=97

Check failure

Code scanning / SonarCloud

Web servers should not bind to all network interfaces High

Avoid binding the application to all network interfaces. See more on SonarQube Cloud
32 changes: 32 additions & 0 deletions app/model/StructuredSchema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any

class KeyValue(BaseModel):
key: str = Field(description="The field name or key")
value: str | float | int | bool | None = Field(description="The extracted value")

class TableRow(BaseModel):
cells: List[str] = Field(description="The cells in this row")

class Table(BaseModel):
title: Optional[str] = Field(None, description="Optional title or caption of the table")
columns: List[str] = Field(description="Column headers")
rows: List[TableRow] = Field(description="Rows of the table")

class DocumentEntity(BaseModel):
text: str = Field(description="The text of the entity")
label: str = Field(description="The type of the entity (e.g., PER, ORG, LOC, DATE, MONEY)")

class StructuredDocumentData(BaseModel):
document_type: str = Field(description="Inferred type of the document (e.g., 'invoice', 'scientific_paper', 'receipt', 'contract', 'unknown')")
key_values: List[KeyValue] = Field(default_factory=list, description="Extracted key-value pairs from the document")
tables: List[Table] = Field(default_factory=list, description="Extracted tables from the document")
entities: List[DocumentEntity] = Field(default_factory=list, description="Named entities found in the document")
text_summary: Optional[str] = Field(None, description="A brief summary of the document's content")

class StructuredExtractionResult(BaseModel):
file_name: str
provider_used: str
model_used: Optional[str] = None
data: StructuredDocumentData
time_taken: str
1 change: 0 additions & 1 deletion app/routes.py

This file was deleted.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dependencies = [
"fastapi>=0.129.0",
"filetype>=1.2.0",
"httpx>=0.28.1",
"instructor>=1.15.1",
"litellm>=1.83.0",
"pdf2image>=1.17.0",
"pillow>=12.1.1",
"pip>=26.0.1",
Expand Down
1 change: 0 additions & 1 deletion run.py

This file was deleted.

26 changes: 0 additions & 26 deletions src/main.py

This file was deleted.

Loading
Loading