358 lines
14 KiB
Python
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())
|