release: v2.8.2 fix Ollama amount normalization
This commit is contained in:
parent
4bf592cf0c
commit
6218e743ef
13
MDfile/RELEASE_NOTES_v2.8.2.md
Normal file
13
MDfile/RELEASE_NOTES_v2.8.2.md
Normal file
@ -0,0 +1,13 @@
|
||||
# BMC Hub v2.8.2
|
||||
|
||||
## AI Template Builder
|
||||
|
||||
- Rettet fejl når Ollama returnerer fakturabeløb som tekst i stedet for numerisk JSON.
|
||||
- Understøtter nu både danske beløbsformater som `1.471,20 DKK` og engelske formater som `1471.20`.
|
||||
- Ugyldige eller tomme beløb håndteres uden at afbryde AI-analysen.
|
||||
- Tilføjet regressionstest for beløbsnormalisering.
|
||||
|
||||
## Verifikation
|
||||
|
||||
- Regressionstest: 2 bestået.
|
||||
- Python-syntaks og diff-kontrol bestået.
|
||||
@ -460,6 +460,18 @@ REGLER FOR title:
|
||||
# Parse JSON from response
|
||||
extraction = self._parse_json_response(raw_response)
|
||||
|
||||
# Models do not always respect the numeric JSON schema and may
|
||||
# return Danish/English amount strings. Normalize them before
|
||||
# applying sign rules so comparisons with zero are type-safe.
|
||||
for amount_key in ('total_amount', 'vat_amount'):
|
||||
extraction[amount_key] = self._normalize_amount_value(extraction.get(amount_key))
|
||||
if isinstance(extraction.get('lines'), list):
|
||||
for line in extraction['lines']:
|
||||
if not isinstance(line, dict):
|
||||
continue
|
||||
for amount_key in ('unit_price', 'line_total', 'vat_amount'):
|
||||
line[amount_key] = self._normalize_amount_value(line.get(amount_key))
|
||||
|
||||
# CRITICAL: Fix amount signs based on document_type
|
||||
# LLM sometimes returns negative amounts for invoices - fix this!
|
||||
document_type = extraction.get('document_type', 'invoice')
|
||||
@ -528,6 +540,39 @@ REGLER FOR title:
|
||||
"confidence": 0.0
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_amount_value(value):
|
||||
"""Convert common Danish/English amount representations to numbers."""
|
||||
if value is None or value == '':
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
|
||||
cleaned = re.sub(r'[^0-9,.-]', '', value.strip()).strip('.,')
|
||||
if not cleaned or cleaned in {'-', '.', ',', '-.', '-,'}:
|
||||
return None
|
||||
|
||||
if ',' in cleaned and '.' in cleaned:
|
||||
if cleaned.rfind(',') > cleaned.rfind('.'):
|
||||
cleaned = cleaned.replace('.', '').replace(',', '.')
|
||||
else:
|
||||
cleaned = cleaned.replace(',', '')
|
||||
elif ',' in cleaned:
|
||||
cleaned = cleaned.replace('.', '').replace(',', '.')
|
||||
elif cleaned.count('.') > 1:
|
||||
cleaned = cleaned.replace('.', '')
|
||||
elif '.' in cleaned and len(cleaned.rsplit('.', 1)[1]) == 3:
|
||||
cleaned = cleaned.replace('.', '')
|
||||
|
||||
try:
|
||||
return float(cleaned)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _parse_json_response(self, response: str) -> Dict:
|
||||
"""Parse JSON from LLM response with aggressive fallback strategies"""
|
||||
logger.info(f"🔍 Response length: {len(response)}, preview: {response[:200]}")
|
||||
|
||||
19
tests/test_ollama_amount_normalization.py
Normal file
19
tests/test_ollama_amount_normalization.py
Normal file
@ -0,0 +1,19 @@
|
||||
from app.services.ollama_service import OllamaService
|
||||
|
||||
|
||||
def test_normalizes_numeric_and_danish_amount_values():
|
||||
normalize = OllamaService._normalize_amount_value
|
||||
|
||||
assert normalize(1471.2) == 1471.2
|
||||
assert normalize("1471.20") == 1471.2
|
||||
assert normalize("1.471,20 DKK") == 1471.2
|
||||
assert normalize("-1 471,20 kr.") == -1471.2
|
||||
|
||||
|
||||
def test_rejects_non_amount_values_without_type_errors():
|
||||
normalize = OllamaService._normalize_amount_value
|
||||
|
||||
assert normalize(None) is None
|
||||
assert normalize("") is None
|
||||
assert normalize("ukendt") is None
|
||||
assert normalize({"value": "10,00"}) is None
|
||||
Loading…
Reference in New Issue
Block a user