Texbase / src_2 /CashFlowCareTaker /validator.py
Asad999's picture
Initial clean deployment for Hugging Face Spaces (v5 - final fix)
d712cef
Raw
History Blame Contribute Delete
11.9 kB
#!/usr/bin/python3
"""
validator.py β€” CashFlowCareTaker (Layer C)
============================================
Three validation steps before any DB write:
1. FX Normaliser β€” converts any currency β†’ USD + PKR using risk_factors.json
2. Schema Enforcer β€” ensures description, amount, date are not null
3. Human-in-the-loop β€” prints evidence, prompts "yes" to confirm
Public API
----------
validate_and_save(analysis_result: dict, source_type: str) -> SaveResult dict
"""
from __future__ import annotations
import json
import os
import sys
import datetime
sys.path.insert(0, os.path.dirname(__file__))
from db_setup import get_conn
from vector_store import add_transaction
RISK_FACTORS_PATH = (
"/Volumes/ssd2/TEXBASE/src/PO:Quotation/Stats_data_collection/risk_factors.json'))
)
# ══════════════════════════════════════════════════════════════════════════════
# FX NORMALISER
# ══════════════════════════════════════════════════════════════════════════════
def _load_fx_rates() -> dict:
"""Load FX rates from risk_factors.json data_snapshot."""
try:
with open(RISK_FACTORS_PATH, encoding="utf-8") as f:
data = json.load(f)
snap = data.get("data_snapshot", {})
rates = {
"USD": 1.0,
"PKR": 1.0 / snap.get("usd_pkr", 278.716), # PKR β†’ USD
"EUR": snap.get("eur_usd", 1.182), # EUR β†’ USD (approximate)
"CNY": 1.0 / (snap.get("cny_pkr", 40.57) / snap.get("usd_pkr", 278.716)),
}
usd_pkr = snap.get("usd_pkr", 278.716)
return {"rates_to_usd": rates, "usd_pkr": usd_pkr, "snapshot": snap}
except Exception as e:
print(f" [FX] Warning: could not load risk_factors.json: {e}")
return {"rates_to_usd": {"USD": 1.0, "PKR": 1/278.716, "EUR": 1.182, "CNY": 0.138},
"usd_pkr": 278.716, "snapshot": {}}
def normalise_fx(parsed: dict) -> dict:
"""
Add amount_usd, amount_pkr, fx_rate_used to parsed dict.
Returns updated parsed dict.
"""
fx = _load_fx_rates()
currency = (parsed.get("currency") or "USD").upper()
amount = float(parsed.get("amount") or 0)
rate_to_usd = fx["rates_to_usd"].get(currency, 1.0)
usd_pkr = fx["usd_pkr"]
amount_usd = round(amount * rate_to_usd, 4)
amount_pkr = round(amount_usd * usd_pkr, 2)
snap = fx["snapshot"]
print(f"\n[FX] {amount:,.2f} {currency} β†’ "
f"${amount_usd:,.4f} USD | ₨{amount_pkr:,.2f} PKR")
print(f" Rates from risk_factors.json: usd_pkr={snap.get('usd_pkr')}, "
f"eur_pkr={snap.get('eur_pkr')}, cny_pkr={snap.get('cny_pkr')}")
return {
**parsed,
"amount_usd": amount_usd,
"amount_pkr": amount_pkr,
"fx_rate_used": rate_to_usd,
}
# ══════════════════════════════════════════════════════════════════════════════
# SCHEMA ENFORCER
# ══════════════════════════════════════════════════════════════════════════════
def enforce_schema(parsed: dict) -> list[str]:
"""
Check mandatory fields. Returns list of validation errors (empty = OK).
"""
errors = []
if not parsed.get("description", "").strip():
errors.append("description is null/empty")
if not parsed.get("amount") and parsed.get("amount") != 0:
errors.append("amount is null")
if not parsed.get("date", "").strip():
errors.append("date is null/empty")
return errors
# ══════════════════════════════════════════════════════════════════════════════
# HUMAN-IN-THE-LOOP
# ══════════════════════════════════════════════════════════════════════════════
def _print_evidence(parsed: dict, similar_entries: list, candidates: list) -> None:
"""Display all evidence before asking the user to confirm."""
print("\n" + "═" * 65)
print(" EVIDENCE SUMMARY β€” Please review before confirming")
print("═" * 65)
print(f" Type : {parsed.get('transaction_type', '?').upper()}")
print(f" Description : {parsed.get('description', '?')}")
print(f" Counterparty : {parsed.get('counterparty', '?')}")
print(f" Amount (raw) : {parsed.get('amount_raw', '?')}")
print(f" Amount (USD) : ${parsed.get('amount_usd', 0):,.4f}")
print(f" Amount (PKR) : ₨{parsed.get('amount_pkr', 0):,.2f}")
print(f" FX Rate used : {parsed.get('fx_rate_used', '?')}")
print(f" Date : {parsed.get('date', '?')}")
print(f" Confidence : {parsed.get('confidence', '?')}")
if parsed.get("notes"):
print(f" Notes : {parsed.get('notes')}")
if similar_entries:
print(f"\n VECTOR DB MATCHES ({len(similar_entries)}):")
for h in similar_entries[:3]:
print(f" [{h['distance']:.3f}] {h['text'][:70]}")
if candidates:
print(f"\n EXISTING DB MATCHES ({len(candidates)}):")
for c in candidates:
print(f" {c.get('source_table','?')} | {c.get('counterparty','?')} | "
f"{c.get('amount_raw','?')} | {c.get('date','?')}")
print("═" * 65)
def human_confirm(parsed: dict, similar_entries: list, candidates: list, auto_confirm: bool = False) -> bool:
"""Show evidence and ask user to type 'yes' to confirm save."""
_print_evidence(parsed, similar_entries, candidates)
if auto_confirm:
print("\n [Validator] Auto-confirmed by Superagent. Proceeding to save.")
return True
try:
answer = input("\n β–Ά Type 'yes' to SAVE to database, or anything else to CANCEL: ").strip().lower()
return answer in ("yes", "y")
except (EOFError, KeyboardInterrupt):
print("\n Cancelled.")
return False
# ══════════════════════════════════════════════════════════════════════════════
# SAVE TO DB
# ══════════════════════════════════════════════════════════════════════════════
def _save_record(parsed: dict, source_type: str, vector_match: str) -> int:
"""Insert validated record into intakes or outtakes. Returns row id."""
table = "intakes" if parsed.get("transaction_type") == "intake" else "outtakes"
conn = get_conn()
cur = conn.execute(
f"""INSERT INTO {table}
(description, amount_raw, amount_usd, amount_pkr, currency,
fx_rate_used, date, source, counterparty, vector_match)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
parsed.get("description", ""),
parsed.get("amount_raw", ""),
parsed.get("amount_usd"),
parsed.get("amount_pkr"),
parsed.get("currency", ""),
parsed.get("fx_rate_used"),
parsed.get("date", ""),
source_type,
parsed.get("counterparty", ""),
vector_match,
),
)
conn.commit()
row_id = cur.lastrowid
conn.close()
return row_id
# ══════════════════════════════════════════════════════════════════════════════
# MAIN ENTRY POINT
# ══════════════════════════════════════════════════════════════════════════════
def validate_and_save(analysis_result: dict, source_type: str = "manual", auto_confirm: bool = False) -> dict:
"""
Layer C entry point.
Parameters
----------
analysis_result : output of gemini_analyst.analyse()
source_type : string label for the source column
Returns
-------
dict {saved: bool, table: str, row_id: int, errors: list}
"""
parsed = analysis_result.get("parsed", {})
similar_entries = analysis_result.get("similar_entries", [])
candidates = analysis_result.get("candidates", [])
if analysis_result.get("conflict"):
print(f"\n[Validator] HIGH CONFLICT detected (id={analysis_result.get('conflict_id')}).")
print(" This transaction has been saved to the conflicts table for manual review.")
return {"saved": False, "table": "conflicts",
"row_id": analysis_result.get("conflict_id"), "errors": []}
if analysis_result.get("error"):
print(f"\n[Validator] Cannot proceed: {analysis_result['error']}")
return {"saved": False, "table": None, "row_id": None,
"errors": [analysis_result["error"]]}
# ── Step 1: FX Normalise ───────────────────────────────────────────────
parsed = normalise_fx(parsed)
# ── Step 2: Schema Enforce ─────────────────────────────────────────────
errors = enforce_schema(parsed)
if errors:
print(f"\n[Validator] Schema errors: {errors}")
return {"saved": False, "table": None, "row_id": None, "errors": errors}
# ── Step 3: Human-in-the-loop ──────────────────────────────────────────
confirmed = human_confirm(parsed, similar_entries, candidates, auto_confirm=auto_confirm)
if not confirmed:
print("\n [Validator] Save cancelled by user.")
return {"saved": False, "table": None, "row_id": None, "errors": ["User cancelled"]}
# ── Save ───────────────────────────────────────────────────────────────
vector_match = similar_entries[0]["text"][:120] if similar_entries else ""
table = "intakes" if parsed.get("transaction_type") == "intake" else "outtakes"
row_id = _save_record(parsed, source_type, vector_match)
# ── Add to vector store for future searches ────────────────────────────
vec_text = (
f"{table.upper()} | {parsed.get('counterparty','')} | "
f"{parsed.get('description','')} | {parsed.get('amount_raw','')} | "
f"{parsed.get('date','')}"
)
add_transaction(vec_text, {
"type": table,
"counterparty": parsed.get("counterparty", ""),
"currency": parsed.get("currency", ""),
"date": parsed.get("date", ""),
})
print(f"\n βœ… Saved to '{table}' (id={row_id})")
return {"saved": True, "table": table, "row_id": row_id, "errors": []}