Files
kamidso-back/kamidso/native_connectors/semantic/money.py
T
javadandCursor edbaa2acec Add native Didar connectors so chats can query CRM data reliably.
Connector calls, semantic rules, and DuckDB memory keep long runs stable, and stream retry stops chats from stalling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 19:26:47 +03:30

123 lines
4.0 KiB
Python

"""Currency-safe amount reporting. Raw cross-currency sums are never produced."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from kamidso.native_connectors.semantic.catalog import load_bundle
@dataclass(frozen=True)
class MoneySummary:
groups: List[Dict[str, Any]] = field(default_factory=list)
mixed: bool = False
combined_total: Optional[float] = None
currency_verified: bool = False
def _amount_fields() -> Dict[str, str]:
rules = load_bundle()["money"]
sources = rules.get("source_fields") if isinstance(rules.get("source_fields"), dict) else {}
policy = rules.get("amount_field_policy") if isinstance(rules.get("amount_field_policy"), dict) else {}
return {
"final": str(policy.get("default_record_amount") or "FinalAdjustmentPrice"),
"fallback": str(policy.get("fallback") or "Price"),
"currency": str(sources.get("currency_id") or "CurrencyId"),
}
def _as_amount(value: Any) -> Optional[float]:
if isinstance(value, bool) or value is None:
return None
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
text = value.strip().replace(",", "")
if not text:
return None
try:
return float(text)
except ValueError:
return None
return None
def _label(currency_id: str, currency_map: Dict[str, Any]) -> tuple:
mapped = currency_map.get(currency_id)
if isinstance(mapped, dict):
name = mapped.get("code") or mapped.get("name")
if name:
return str(name), True
if isinstance(mapped, str) and mapped.strip():
return mapped.strip(), True
shown = currency_id or "unknown"
return f"CurrencyId={shown}", False
def summarize_amounts(
records: List[Any],
calibration: Optional[Dict[str, Any]] = None,
) -> MoneySummary:
calibration = calibration or {}
fields = _amount_fields()
currency_map = calibration.get("currency_map")
if not isinstance(currency_map, dict):
currency_map = {}
buckets: Dict[str, float] = {}
order: List[str] = []
for record in records:
if not isinstance(record, dict):
continue
amount = _as_amount(record.get(fields["final"]))
if amount is None:
amount = _as_amount(record.get(fields["fallback"]))
if amount is None:
continue
currency = record.get(fields["currency"])
key = "" if currency is None else str(currency)
if key not in buckets:
order.append(key)
buckets[key] = 0.0
buckets[key] += amount
groups: List[Dict[str, Any]] = []
for key in order:
label, known = _label(key, currency_map)
groups.append(
{
"currency_id": key,
"label": label,
"amount": buckets[key],
"known": known,
}
)
mixed = len(groups) > 1
known_single = len(groups) == 1 and bool(groups[0]["known"])
verified = bool(calibration.get("dashboard_aggregate_currency_semantics_verified")) and known_single
combined = None if mixed or not groups else groups[0]["amount"]
return MoneySummary(
groups=groups,
mixed=mixed,
combined_total=combined,
currency_verified=verified,
)
def format_amount(value: float) -> str:
if float(value).is_integer():
return str(int(value))
return str(value)
def amount_lines(summary: MoneySummary) -> List[str]:
if not summary.groups:
return ["Amounts: none on the verified rows."]
if summary.mixed:
lines = ["Amounts by currency (not summed):"]
for group in summary.groups:
lines.append(f"- {group['label']}: {format_amount(group['amount'])}")
lines.append("These currency groups stay separate.")
return lines
group = summary.groups[0]
return [f"Amount: {group['label']} {format_amount(group['amount'])}"]