AI-Pricing-Agent/scripts/e2e_pipeline_validate.py

358 lines
14 KiB
Python

"""End-to-end pricing pipeline validation (COSMOS + Apify + suggested price + gate).
Runs against live .env credentials (conda env Talha):
conda run -n Talha python scripts/e2e_pipeline_validate.py
conda run -n Talha python scripts/e2e_pipeline_validate.py UBMICROFIBERGUSSETPILLOWWHITEQUEEN 24.99
Writes:
data/e2e_pipeline_results.json
E2E_PIPELINE_REPORT.md (overwritten with this run's findings)
"""
from __future__ import annotations
import json
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT))
from config.settings import Settings, get_rules, get_settings # noqa: E402
from langgraph.types import Command # noqa: E402
from pricing_agent.analyze import analyze_price, suggested_price # noqa: E402
from pricing_agent.graph import build_graph # noqa: E402
from pricing_agent.providers import get_provider # noqa: E402
from pricing_agent.schemas import Decision, FeeQuote, SkuInput # noqa: E402
from pricing_agent.tools.gate import evaluate_gate # noqa: E402
from pricing_agent.tools.margin_engine import stack_from_quote # noqa: E402
def _checks() -> list[dict]:
return []
def main() -> int:
sku = sys.argv[1] if len(sys.argv) > 1 else "UBMICROFIBERGUSSETPILLOWWHITEQUEEN"
price = float(sys.argv[2]) if len(sys.argv) > 2 else 24.99
# Reload settings so fresh .env APIFY_* are picked up (lru_cache may be warm).
get_settings.cache_clear()
settings = Settings()
rules = get_rules()
checks: list[dict] = []
report: dict = {
"sku": sku,
"candidate_price": price,
"apify_token_set": bool(settings.apify_token),
"apify_seller_id": settings.apify_seller_id,
"margin_target": rules.margin_target,
"margin_floor": rules.margin_floor,
"checks": checks,
}
print(f"E2E SKU={sku} @ ${price:.2f}")
print(f"APIFY_TOKEN set={bool(settings.apify_token)} "
f"SELLER_ID={settings.apify_seller_id or '(none)'}")
print("---")
# ── 1. Provider enrich + fees (COSMOS) ─────────────────────────────
t0 = time.time()
provider = get_provider(settings)
sku_in = SkuInput(
sku=sku,
asin=None,
product_name="",
marketplace="US",
category="",
landed_cost=0.0,
target_price=price,
)
try:
enriched = provider.enrich(sku_in)
stack = provider.fee_stack(enriched)
checks.append({
"name": "cosmos_enrich_and_fees",
"pass": bool(enriched.asin) and stack.selling_price == price,
"elapsed_s": round(time.time() - t0, 1),
"asin": enriched.asin,
"landed_cost": enriched.landed_cost,
"fba_fee": stack.fba_fee,
"referral_pct": stack.referral_pct,
"cm_pct": round(stack.contribution_margin_pct, 4),
"break_even": stack.break_even_price,
"map_floor": stack.map_floor,
"detail": f"ASIN={enriched.asin} CM={stack.contribution_margin_pct*100:.1f}%",
})
except Exception as e: # noqa: BLE001
checks.append({
"name": "cosmos_enrich_and_fees",
"pass": False,
"error": str(e),
"elapsed_s": round(time.time() - t0, 1),
})
print("FAIL: COSMOS enrich/fees:", e)
_write(report)
return 1
print(f"[1] COSMOS OK asin={enriched.asin} CM={stack.contribution_margin_pct*100:.1f}% "
f"BE=${stack.break_even_price:.2f} MAP=${stack.map_floor:.2f}")
# ── 2. Suggested price math + re-check margin ─────────────────────
t0 = time.time()
sug = suggested_price(stack, rules.margin_target)
sug_ok = False
sug_cm = None
sug_detail = ""
try:
if sug is None:
sug_detail = "suggested_price returned None"
else:
# Re-stack at suggestion with scaled referral/returns (same model as unit test).
requote = FeeQuote(
selling_price=sug,
landed_cost=stack.landed_cost,
fba_fee=stack.fba_fee,
referral_amt=stack.referral_pct * sug,
returns_reserve=(stack.returns_reserve / stack.selling_price) * sug
if stack.selling_price else 0.0,
other_fees=stack.storage_alloc,
source="e2e-reprice",
)
restack = stack_from_quote(requote, rules)
sug_cm = restack.contribution_margin_pct
charm_ok = round(sug - int(sug), 2) == 0.99
clears = sug_cm + 1e-9 >= rules.margin_target
sug_ok = charm_ok and clears
sug_detail = (
f"suggested=${sug:.2f} charm={charm_ok} "
f"CM_at_sug={sug_cm*100:.1f}% >= target {rules.margin_target*100:.0f}% → {clears}"
)
# Also ask COSMOS takehome at the suggestion when possible.
try:
live = provider.fee_stack(enriched.model_copy(update={"target_price": sug}))
sug_detail += f" | COSMOS_CM_at_sug={live.contribution_margin_pct*100:.1f}%"
except Exception as e: # noqa: BLE001
sug_detail += f" | COSMOS_reprice_skip={e}"
except Exception as e: # noqa: BLE001
sug_detail = str(e)
checks.append({
"name": "suggested_price_clears_target",
"pass": sug_ok,
"elapsed_s": round(time.time() - t0, 1),
"suggested_price": sug,
"cm_at_suggested": sug_cm,
"detail": sug_detail,
})
print(f"[2] SUGGESTED {sug_detail}{'PASS' if sug_ok else 'FAIL'}")
# ── 3. Apify competitive (live Buy Box) ───────────────────────────
t0 = time.time()
try:
comp = provider.competitive(enriched)
apify_ok = (
bool(settings.apify_token)
and comp.buy_box_status.value != "UNKNOWN"
or (comp.buy_box_price is not None)
)
# Soften: price present is enough to call Apify usable even if UNKNOWN status
usable = comp.buy_box_price is not None or comp.is_suppressed
checks.append({
"name": "apify_competitive",
"pass": usable,
"elapsed_s": round(time.time() - t0, 1),
"buy_box_status": comp.buy_box_status.value,
"buy_box_price": comp.buy_box_price,
"competitive_low": comp.competitive_low,
"competitive_median": comp.competitive_median,
"competitive_high": comp.competitive_high,
"is_suppressed": comp.is_suppressed,
"reason": comp.reason,
"detail": (
f"{comp.buy_box_status.value} bb=${comp.buy_box_price} "
f"band={comp.competitive_low}-{comp.competitive_high}"
),
})
print(f"[3] APIFY {comp.buy_box_status.value} bb=${comp.buy_box_price} "
f"band={comp.competitive_low}-{comp.competitive_median}-{comp.competitive_high} "
f"{'PASS' if usable else 'FAIL'}")
print(f" {comp.reason}")
except Exception as e: # noqa: BLE001
checks.append({
"name": "apify_competitive",
"pass": False,
"error": str(e),
"elapsed_s": round(time.time() - t0, 1),
})
print("[3] APIFY FAIL:", e)
_write(report)
return 1
# ── 4. Gate decision ──────────────────────────────────────────────
t0 = time.time()
decision, reasons = evaluate_gate(
gate_stack=stack, competitive=comp, rules=rules
)
checks.append({
"name": "evaluate_gate",
"pass": decision in (Decision.APPROVED, Decision.NEEDS_REVIEW, Decision.BLOCKED),
"elapsed_s": round(time.time() - t0, 1),
"decision": decision.value,
"reasons": reasons,
"detail": f"{decision.value}: {reasons[0] if reasons else ''}",
})
print(f"[4] GATE {decision.value}")
for r in reasons:
print(f"{r}")
# ── 5. Analyze path (suggested + verdict; COSMOS-heavy) ───────────
t0 = time.time()
try:
analysis = analyze_price(sku, price, settings, with_narrative=False)
a_ok = analysis.suggested_price is not None
# Suggested from analyze should match (or be very close to) provider-stack suggestion
delta = None
if sug is not None and analysis.suggested_price is not None:
delta = abs(analysis.suggested_price - sug)
a_ok = a_ok and delta < 0.02 # same charm price within a cent
checks.append({
"name": "analyze_suggested_consistent",
"pass": a_ok,
"elapsed_s": round(time.time() - t0, 1),
"analyze_suggested": analysis.suggested_price,
"provider_suggested": sug,
"delta": delta,
"rating": analysis.rating.value,
"margin_pct": analysis.margin_pct,
"take_home": analysis.take_home_per_unit,
"detail": (
f"analyze_sug=${analysis.suggested_price} provider_sug=${sug} "
f"delta={delta} rating={analysis.rating.value}"
),
})
print(f"[5] ANALYZE sug=${analysis.suggested_price} "
f"rating={analysis.rating.value} CM={analysis.margin_pct*100:.1f}% "
f"{'PASS' if a_ok else 'FAIL'}")
except Exception as e: # noqa: BLE001
checks.append({
"name": "analyze_suggested_consistent",
"pass": False,
"error": str(e),
"elapsed_s": round(time.time() - t0, 1),
})
print("[5] ANALYZE FAIL:", e)
# ── 6. Full LangGraph pipeline (enrich→…→Apify→gate→HITL auto) ───
t0 = time.time()
try:
graph = build_graph()
config = {"configurable": {"thread_id": f"e2e:{sku}:{int(time.time())}"}}
state = graph.invoke({"sku_input": enriched}, config=config)
interrupts = state.get("__interrupt__")
if interrupts:
payload = interrupts[0].value
# smart: approve only APPROVED
action = (
"approve"
if payload.get("decision") == Decision.APPROVED.value
else "reject"
)
state = graph.invoke(
Command(resume={"action": action, "price": None, "note": f"e2e-{action}"}),
config=config,
)
proposal = state.get("decision")
graph_ok = proposal is not None
checks.append({
"name": "langgraph_e2e",
"pass": graph_ok,
"elapsed_s": round(time.time() - t0, 1),
"decision": proposal.decision.value if proposal else None,
"recommended_price": proposal.recommended_price if proposal else None,
"buy_box": (
proposal.competitive.buy_box_status.value if proposal else None
),
"buy_box_price": (
proposal.competitive.buy_box_price if proposal else None
),
"cm_pct": (
proposal.fee_stack.contribution_margin_pct if proposal else None
),
"written": state.get("written"),
"detail": (
f"{proposal.decision.value} @ ${proposal.recommended_price:.2f} "
f"bb={proposal.competitive.buy_box_status.value}"
if proposal else "no proposal"
),
})
print(f"[6] GRAPH {checks[-1]['detail']} written={state.get('written')} "
f"{'PASS' if graph_ok else 'FAIL'}")
except Exception as e: # noqa: BLE001
checks.append({
"name": "langgraph_e2e",
"pass": False,
"error": str(e),
"elapsed_s": round(time.time() - t0, 1),
})
print("[6] GRAPH FAIL:", e)
report["checks"] = checks
report["all_passed"] = all(c.get("pass") for c in checks)
_write(report)
print("---")
print(f"ALL PASSED: {report['all_passed']}")
return 0 if report["all_passed"] else 1
def _write(report: dict) -> None:
out_json = ROOT / "data" / "e2e_pipeline_results.json"
out_json.parent.mkdir(parents=True, exist_ok=True)
out_json.write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
lines = [
"# E2E pricing pipeline validation report",
"",
f"| | |",
f"|---|---|",
f"| **SKU** | `{report.get('sku')}` |",
f"| **Candidate price** | ${report.get('candidate_price')} |",
f"| **Apify token set** | {report.get('apify_token_set')} |",
f"| **Seller id** | `{report.get('apify_seller_id') or ''}` |",
f"| **Margin target / floor** | "
f"{(report.get('margin_target') or 0)*100:.0f}% / "
f"{(report.get('margin_floor') or 0)*100:.0f}% |",
f"| **All passed** | **{report.get('all_passed')}** |",
"",
"## Checks",
"",
"| Check | Pass | Detail |",
"|---|---|---|",
]
for c in report.get("checks", []):
detail = (c.get("detail") or c.get("error") or "").replace("|", "/")
lines.append(
f"| `{c.get('name')}` | {'PASS' if c.get('pass') else 'FAIL'} | {detail} |"
)
lines += [
"",
"## Notes",
"",
"- Suggested price is derived from the fee stack (COSMOS costs/fees + margin target), "
"charm-rounded to `.99`, then re-checked that CM ≥ target.",
"- Apify fills Buy Box / competitive band after COSMOS resolves the ASIN.",
"- LangGraph path: enrich → margin → competitive → evaluate → human_review → write_back.",
"",
f"Raw JSON: [`data/e2e_pipeline_results.json`](data/e2e_pipeline_results.json)",
"",
]
(ROOT / "E2E_PIPELINE_REPORT.md").write_text("\n".join(lines), encoding="utf-8")
print(f"Wrote {out_json}")
print(f"Wrote {ROOT / 'E2E_PIPELINE_REPORT.md'}")
if __name__ == "__main__":
raise SystemExit(main())