Skip to content

Commit 3b97baf

Browse files
author
Bob Strahan
committed
Add granular assessment bounding box conversion support with comprehensive testing
1 parent 6237673 commit 3b97baf

File tree

2 files changed

+301
-0
lines changed

2 files changed

+301
-0
lines changed

lib/idp_common_pkg/idp_common/assessment/granular_service.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,20 @@ def _process_assessment_task(
720720
"confidence_reason": f"Unable to parse assessment response for {attr_name} - default score assigned",
721721
}
722722

723+
# Process bounding boxes automatically if bbox data is present
724+
try:
725+
logger.debug(
726+
f"Checking for bounding box data in granular assessment task {task.task_id}"
727+
)
728+
assessment_data = self._extract_geometry_from_assessment(
729+
assessment_data
730+
)
731+
except Exception as e:
732+
logger.warning(
733+
f"Failed to extract geometry data for task {task.task_id}: {str(e)}"
734+
)
735+
# Continue with assessment even if geometry extraction fails
736+
723737
# Check for confidence threshold alerts
724738
confidence_alerts = []
725739
self._check_confidence_alerts_for_task(
@@ -987,6 +1001,149 @@ def _get_text_confidence_data(self, page) -> str:
9871001

9881002
return ""
9891003

1004+
def _convert_bbox_to_geometry(
1005+
self, bbox_coords: List[float], page_num: int
1006+
) -> Dict[str, Any]:
1007+
"""
1008+
Convert [x1,y1,x2,y2] coordinates to geometry format.
1009+
1010+
Args:
1011+
bbox_coords: List of 4 coordinates [x1, y1, x2, y2] in 0-1000 scale
1012+
page_num: Page number where the bounding box appears
1013+
1014+
Returns:
1015+
Dictionary in geometry format compatible with pattern-1 UI
1016+
"""
1017+
if len(bbox_coords) != 4:
1018+
raise ValueError(f"Expected 4 coordinates, got {len(bbox_coords)}")
1019+
1020+
x1, y1, x2, y2 = bbox_coords
1021+
1022+
# Ensure coordinates are in correct order
1023+
x1, x2 = min(x1, x2), max(x1, x2)
1024+
y1, y2 = min(y1, y2), max(y1, y2)
1025+
1026+
# Convert from normalized 0-1000 scale to 0-1
1027+
left = x1 / 1000.0
1028+
top = y1 / 1000.0
1029+
width = (x2 - x1) / 1000.0
1030+
height = (y2 - y1) / 1000.0
1031+
1032+
return {
1033+
"boundingBox": {"top": top, "left": left, "width": width, "height": height},
1034+
"page": page_num,
1035+
}
1036+
1037+
def _process_single_assessment_geometry(
1038+
self, attr_assessment: Dict[str, Any], attr_name: str = ""
1039+
) -> Dict[str, Any]:
1040+
"""
1041+
Process geometry data for a single assessment (with confidence key).
1042+
1043+
Args:
1044+
attr_assessment: Single assessment dictionary with confidence data
1045+
attr_name: Name of attribute for logging
1046+
1047+
Returns:
1048+
Enhanced assessment with geometry converted to proper format
1049+
"""
1050+
enhanced_attr = attr_assessment.copy()
1051+
1052+
# Check if this assessment includes bbox data
1053+
if "bbox" in attr_assessment or "page" in attr_assessment:
1054+
# Both bbox and page are required for valid geometry
1055+
if "bbox" in attr_assessment and "page" in attr_assessment:
1056+
try:
1057+
bbox_coords = attr_assessment["bbox"]
1058+
page_num = attr_assessment["page"]
1059+
1060+
# Validate bbox coordinates
1061+
if isinstance(bbox_coords, list) and len(bbox_coords) == 4:
1062+
# Convert to geometry format
1063+
geometry = self._convert_bbox_to_geometry(bbox_coords, page_num)
1064+
enhanced_attr["geometry"] = [geometry]
1065+
1066+
logger.debug(
1067+
f"Converted bounding box for {attr_name}: {bbox_coords} -> geometry format"
1068+
)
1069+
else:
1070+
logger.warning(
1071+
f"Invalid bounding box format for {attr_name}: {bbox_coords}"
1072+
)
1073+
1074+
except Exception as e:
1075+
logger.warning(
1076+
f"Failed to process bounding box for {attr_name}: {str(e)}"
1077+
)
1078+
else:
1079+
# If only one of bbox/page exists, log a warning about incomplete data
1080+
if "bbox" in attr_assessment and "page" not in attr_assessment:
1081+
logger.warning(
1082+
f"Found bbox without page for {attr_name} - removing incomplete bbox data"
1083+
)
1084+
elif "page" in attr_assessment and "bbox" not in attr_assessment:
1085+
logger.warning(
1086+
f"Found page without bbox for {attr_name} - removing incomplete page data"
1087+
)
1088+
1089+
# Always remove raw bbox/page data from output (whether processed or incomplete)
1090+
enhanced_attr.pop("bbox", None)
1091+
enhanced_attr.pop("page", None)
1092+
1093+
return enhanced_attr
1094+
1095+
def _extract_geometry_from_assessment(
1096+
self, assessment_data: Dict[str, Any]
1097+
) -> Dict[str, Any]:
1098+
"""
1099+
Extract geometry data from assessment response and convert to proper format.
1100+
Now supports recursive processing of nested group attributes.
1101+
1102+
Args:
1103+
assessment_data: Dictionary containing assessment results from LLM
1104+
1105+
Returns:
1106+
Enhanced assessment data with geometry information converted to proper format
1107+
"""
1108+
enhanced_assessment = {}
1109+
1110+
for attr_name, attr_assessment in assessment_data.items():
1111+
if isinstance(attr_assessment, dict):
1112+
# Check if this is a direct confidence assessment
1113+
if "confidence" in attr_assessment:
1114+
# This is a direct assessment - process its geometry
1115+
enhanced_assessment[attr_name] = (
1116+
self._process_single_assessment_geometry(
1117+
attr_assessment, attr_name
1118+
)
1119+
)
1120+
else:
1121+
# This is a group attribute (no direct confidence) - recursively process nested attributes
1122+
logger.debug(f"Processing group attribute: {attr_name}")
1123+
enhanced_assessment[attr_name] = (
1124+
self._extract_geometry_from_assessment(attr_assessment)
1125+
)
1126+
1127+
elif isinstance(attr_assessment, list):
1128+
# Handle list attributes - process each item recursively
1129+
enhanced_list = []
1130+
for i, item_assessment in enumerate(attr_assessment):
1131+
if isinstance(item_assessment, dict):
1132+
# Recursively process each list item
1133+
enhanced_item = self._extract_geometry_from_assessment(
1134+
item_assessment
1135+
)
1136+
enhanced_list.append(enhanced_item)
1137+
else:
1138+
# Non-dict items pass through unchanged
1139+
enhanced_list.append(item_assessment)
1140+
enhanced_assessment[attr_name] = enhanced_list
1141+
else:
1142+
# Other types pass through unchanged
1143+
enhanced_assessment[attr_name] = attr_assessment
1144+
1145+
return enhanced_assessment
1146+
9901147
def process_document_section(self, document: Document, section_id: str) -> Document:
9911148
"""
9921149
Process a single section from a Document object to assess extraction confidence using granular approach.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
4+
"""
5+
Test to verify that both regular and granular assessment services
6+
handle bounding box conversion correctly.
7+
"""
8+
9+
from idp_common.assessment.granular_service import GranularAssessmentService
10+
from idp_common.assessment.service import AssessmentService
11+
12+
13+
def test_both_services_convert_bbox_to_geometry():
14+
"""Test that both regular and granular services convert bbox to geometry."""
15+
16+
# Test data with bbox coordinates
17+
mock_assessment_data = {
18+
"YTDNetPay": {
19+
"confidence": 1.0,
20+
"confidence_reason": "Clear text with high OCR confidence",
21+
"bbox": [443, 333, 507, 345],
22+
"page": 1,
23+
},
24+
"CompanyAddress": {
25+
"State": {
26+
"confidence": 0.99,
27+
"confidence_reason": "Clear text",
28+
"bbox": [230, 116, 259, 126],
29+
"page": 1,
30+
},
31+
"ZipCode": {
32+
"confidence": 0.99,
33+
"confidence_reason": "Clear text",
34+
"bbox": [261, 116, 298, 126],
35+
"page": 1,
36+
},
37+
},
38+
}
39+
40+
print("=== Testing Bounding Box Conversion in Both Services ===")
41+
42+
# Test regular assessment service
43+
print("\n📝 Testing Regular AssessmentService")
44+
regular_service = AssessmentService()
45+
regular_result = regular_service._extract_geometry_from_assessment(
46+
mock_assessment_data
47+
)
48+
49+
# Check YTDNetPay conversion
50+
regular_ytd = regular_result["YTDNetPay"]
51+
regular_ytd_has_geometry = "geometry" in regular_ytd
52+
regular_ytd_has_bbox = "bbox" in regular_ytd
53+
54+
print(
55+
f"Regular Service - YTDNetPay: geometry={regular_ytd_has_geometry}, bbox={regular_ytd_has_bbox}"
56+
)
57+
58+
# Check CompanyAddress.State conversion
59+
regular_state = regular_result["CompanyAddress"]["State"]
60+
regular_state_has_geometry = "geometry" in regular_state
61+
regular_state_has_bbox = "bbox" in regular_state
62+
63+
print(
64+
f"Regular Service - CompanyAddress.State: geometry={regular_state_has_geometry}, bbox={regular_state_has_bbox}"
65+
)
66+
67+
# Test granular assessment service
68+
print("\n📝 Testing GranularAssessmentService")
69+
granular_service = GranularAssessmentService()
70+
granular_result = granular_service._extract_geometry_from_assessment(
71+
mock_assessment_data
72+
)
73+
74+
# Check YTDNetPay conversion
75+
granular_ytd = granular_result["YTDNetPay"]
76+
granular_ytd_has_geometry = "geometry" in granular_ytd
77+
granular_ytd_has_bbox = "bbox" in granular_ytd
78+
79+
print(
80+
f"Granular Service - YTDNetPay: geometry={granular_ytd_has_geometry}, bbox={granular_ytd_has_bbox}"
81+
)
82+
83+
# Check CompanyAddress.State conversion
84+
granular_state = granular_result["CompanyAddress"]["State"]
85+
granular_state_has_geometry = "geometry" in granular_state
86+
granular_state_has_bbox = "bbox" in granular_state
87+
88+
print(
89+
f"Granular Service - CompanyAddress.State: geometry={granular_state_has_geometry}, bbox={granular_state_has_bbox}"
90+
)
91+
92+
# Verify both services work identically
93+
print("\n🔍 Verification:")
94+
95+
# Both should convert bbox to geometry
96+
assert regular_ytd_has_geometry, (
97+
"Regular service should convert YTDNetPay bbox to geometry"
98+
)
99+
assert not regular_ytd_has_bbox, (
100+
"Regular service should remove YTDNetPay bbox after conversion"
101+
)
102+
assert granular_ytd_has_geometry, (
103+
"Granular service should convert YTDNetPay bbox to geometry"
104+
)
105+
assert not granular_ytd_has_bbox, (
106+
"Granular service should remove YTDNetPay bbox after conversion"
107+
)
108+
109+
# Both should handle nested attributes
110+
assert regular_state_has_geometry, (
111+
"Regular service should convert nested State bbox to geometry"
112+
)
113+
assert not regular_state_has_bbox, (
114+
"Regular service should remove nested State bbox after conversion"
115+
)
116+
assert granular_state_has_geometry, (
117+
"Granular service should convert nested State bbox to geometry"
118+
)
119+
assert not granular_state_has_bbox, (
120+
"Granular service should remove nested State bbox after conversion"
121+
)
122+
123+
# Check geometry values are equivalent
124+
regular_ytd_geometry = regular_ytd["geometry"][0]["boundingBox"]
125+
granular_ytd_geometry = granular_ytd["geometry"][0]["boundingBox"]
126+
127+
assert regular_ytd_geometry == granular_ytd_geometry, (
128+
"Both services should produce identical geometry"
129+
)
130+
131+
print("✅ Regular AssessmentService: Converts bbox → geometry correctly")
132+
print("✅ GranularAssessmentService: Converts bbox → geometry correctly")
133+
print("✅ Both services handle nested attributes (CompanyAddress.State)")
134+
print("✅ Both services produce identical geometry output")
135+
print("✅ Both services remove raw bbox data after conversion")
136+
137+
print("\n🎉 Both services now support automatic bounding box conversion!")
138+
print("Your deployed stack with granular assessment will now work correctly!")
139+
140+
return True
141+
142+
143+
if __name__ == "__main__":
144+
test_both_services_convert_bbox_to_geometry()

0 commit comments

Comments
 (0)