diff --git a/app/logic/vision_analysis.py b/app/logic/vision_analysis.py index 50b9118..9e511b0 100644 --- a/app/logic/vision_analysis.py +++ b/app/logic/vision_analysis.py @@ -696,6 +696,76 @@ def build_vision_candidate_fields(classification: dict[str, Any]) -> list[dict[s return fields + +def build_vision_field_suggestions( + candidate_fields: list[dict[str, Any]], + existing_fields: dict[str, Any] | None = None, +) -> list[dict[str, Any]]: + """ + Convert vision candidate fields into simple add/update/ignore suggestions. + + This intentionally stays conservative: + - high confidence merchant/time/item_count/money candidates are surfaced + - symbol/noise is ignored + - existing field comparison can be expanded later + """ + existing_fields = existing_fields or {} + suggestions: list[dict[str, Any]] = [] + + type_to_existing_key = { + "merchant_or_header": "merchant_raw", + "transaction_time": "transaction_time", + "item_count": "item_count", + "money_amounts": "amount_candidates", + } + + for field in candidate_fields or []: + candidate_type = field.get("candidate_type") + if candidate_type == "symbol_or_noise": + continue + + confidence = float(field.get("confidence") or 0) + ocr_confidence = field.get("ocr_confidence") + value = field.get("value") + + if not value: + continue + + min_conf = 0.40 + if candidate_type in {"merchant_or_header", "transaction_time"}: + min_conf = 0.60 + elif candidate_type == "money_amounts": + min_conf = 0.50 + + if confidence < min_conf: + continue + + existing_key = type_to_existing_key.get(candidate_type, candidate_type) + existing_value = existing_fields.get(existing_key) + + action = "add" + if existing_value: + action = "review_update" if str(existing_value).strip() != str(value).strip() else "already_present" + + suggestions.append( + { + "suggestion_type": candidate_type, + "target_field": existing_key, + "action": action, + "value": value, + "existing_value": existing_value, + "confidence": confidence, + "ocr_confidence": ocr_confidence, + "source": "vision_candidate_fields", + "source_region_index": field.get("source_region_index"), + "source_bbox": field.get("source_bbox"), + "source_crop_path": field.get("source_crop_path"), + "raw_text": field.get("raw_text"), + } + ) + + return suggestions + def build_vision_assisted_layout(source_layout: dict[str, Any] | None, vision_result: dict[str, Any]) -> dict[str, Any]: """ Convert vision analysis into normal layout_json. @@ -716,6 +786,7 @@ def build_vision_assisted_layout(source_layout: dict[str, Any] | None, vision_re region_score, ) candidate_fields = build_vision_candidate_fields(region_classification) + field_suggestions = build_vision_field_suggestions(candidate_fields) layout["vision_assisted"] = True layout["vision_assisted_status"] = normalized_vision.get("status", "unknown") @@ -725,6 +796,7 @@ def build_vision_assisted_layout(source_layout: dict[str, Any] | None, vision_re layout["vision_region_score"] = region_score layout["vision_region_classification"] = region_classification layout["vision_candidate_fields"] = candidate_fields + layout["vision_field_suggestions"] = field_suggestions layout["layout_sync_source"] = "vision_assisted" layout["layout_needs_review"] = True return layout