#!/usr/bin/env python3 """advisor_tools.py — deterministic decision calculators cho ban cố vấn (Starter Plus+). Cố vấn AI mạnh hơn khi tính được, không chỉ nói. Engine này lo phần TÍNH (tất định, an toàn cho model yếu); model chỉ thu thập input + diễn giải kết quả. Stdlib only. python3 lib/advisor_tools.py prioritize [--file f.json] -> ma trận ƯU TIÊN use-case AI (impact×feasibility + tiêu chí phụ → xếp hạng + góc phần tư + độ nhạy). Không --file: đọc company_kb/business/ai_usecases.json (chia sẻ với dashboard). python3 lib/advisor_tools.py save-usecases --file f.json -> lưu danh sách use-case (admin) python3 lib/advisor_tools.py decision --file f.json -> ma trận quyết định đa tiêu chí (option × tiêu chí có trọng số → xếp hạng + sát nút + độ nhạy) — cho decision-council. python3 lib/advisor_tools.py buildbuy --file f.json -> Build / Buy / Partner cho 1 năng lực AI python3 lib/advisor_tools.py ai-risk --file f.json -> phân loại rủi ro 1 use-case AI + rào chắn python3 lib/advisor_tools.py unit-economics --file f.json -> LTV / CAC / payback / LTV:CAC python3 lib/advisor_tools.py runway --file f.json -> vốn còn trụ bao lâu (burn → số tháng) python3 lib/advisor_tools.py pricing --file f.json -> biên LN theo sản phẩm + kịch bản giá python3 lib/advisor_tools.py health [--file f.json] -> sức khoẻ khách hàng (RFM+hài lòng → tier+rủi ro rời bỏ) python3 lib/advisor_tools.py save-customers --file f.json -> lưu danh sách khách (admin) → dashboard đọc python3 lib/advisor_tools.py sample -> input mẫu (để học schema) Mọi kết quả đều TÁI TẠO ĐƯỢC từ input; KHÔNG ý kiến — phần phán đoán để cho cố vấn (model). """ import os import re import sys import json PLATFORM_PREFIX = {"telegram": "tg", "slack": "sl", "discord": "dc", "whatsapp": "wa"} ADMIN_ROLES = {"ceo", "admin", "owner"} HIGH = 6.0 # ngưỡng cao/thấp trên thang 1–10 (chia góc phần tư) PERTURB = 0.30 # ±30% trọng số khi thử độ nhạy try: import activity as _activity except Exception: _activity = None def _bundle_root(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = _bundle_root() BUSINESS = os.path.join(ROOT, "company_kb", "business") USECASES = os.path.join(BUSINESS, "ai_usecases.json") REGISTRY = os.path.join(ROOT, "users", "_index.json") # ---------- identity / admin (same convention as startup_kit.py) ---------- def resolve_user(): platform = (os.environ.get("HERMES_SESSION_PLATFORM") or "telegram").lower() uid = os.environ.get("HERMES_SESSION_USER_ID") or os.environ.get("HERMES_SESSION_CHAT_ID") if not uid: return None prefix = PLATFORM_PREFIX.get(platform) or (re.sub(r"[^a-z0-9]", "", platform)[:2] or "xx") safe = re.sub(r"[^A-Za-z0-9_-]", "", str(uid)) return ("%s_%s" % (prefix, safe)) if safe else None def _is_admin(): uid = resolve_user() if not uid: return False try: reg = json.load(open(REGISTRY, encoding="utf-8")) role = ((reg.get("users") or {}).get(uid) or {}).get("org_role", "") return role.lower() in ADMIN_ROLES except Exception: return False def _num(v, lo=0.0, hi=10.0, default=0.0): try: return max(lo, min(hi, float(v))) except Exception: return default def _f(v): """Float hoặc None (số tiền/tỉ lệ; không clamp).""" try: return float(v) except Exception: return None # ---------- scoring core (dùng chung prioritize + decision) ---------- def _norm_weights(crit): """crit: {key: weight}. Chuẩn hoá tổng = 100. Rỗng → {} (caller xử lý).""" if not crit: return {} items = {k: max(0.0, float(v)) for k, v in crit.items()} tot = sum(items.values()) or 1.0 return {k: round(v * 100.0 / tot, 2) for k, v in items.items()} def _weighted(scores, weights): """Σ(score*weight)/Σweight trên thang 0–10. scores: {key: 1..10}.""" num = sum(_num(scores.get(k)) * w for k, w in weights.items()) den = sum(weights.values()) or 1.0 return round(num / den, 2) def _rank(items, weights): """items: [{name, scores}] → sorted desc theo điểm trọng số.""" out = [] for it in items: sc = it.get("scores") or {k: it.get(k) for k in weights} out.append({"name": str(it.get("name", "?")), "scores": {k: _num(sc.get(k)) for k in weights}, "score": _weighted(sc, weights), "note": str(it.get("note", ""))}) out.sort(key=lambda x: x["score"], reverse=True) for i, o in enumerate(out, 1): o["rank"] = i return out def _close_calls(ranked, pct=0.10): """Cặp option cách nhau ≤ pct*thang (1.0 điểm) → coi như sát nút (cần thêm tin).""" out = [] for i in range(len(ranked) - 1): if abs(ranked[i]["score"] - ranked[i + 1]["score"]) <= pct * 10: out.append([ranked[i]["name"], ranked[i + 1]["name"]]) return out def _sensitivity(items, weights): """Người thắng có đổi khi mỗi trọng số ±30% (phân bổ lại phần dư)? → stable/fragile.""" base = _rank(items, weights) if len(base) < 2: return {"stable": True, "flips": [], "note": "Chỉ 1 lựa chọn — không có gì để so."} winner = base[0]["name"] flips = [] for k in weights: for sign in (1 + PERTURB, 1 - PERTURB): w = dict(weights) w[k] = max(0.0, weights[k] * sign) others = [o for o in weights if o != k] delta = weights[k] - w[k] base_others = sum(weights[o] for o in others) or 1.0 for o in others: # phân bổ phần dư theo tỉ trọng w[o] = max(0.0, weights[o] + delta * (weights[o] / base_others)) top = _rank(items, _norm_weights(w))[0]["name"] if top != winner and k not in [f["criterion"] for f in flips]: flips.append({"criterion": k, "new_winner": top}) stable = not flips note = ("Kết quả VỮNG — đổi trọng số ±30%% vẫn cùng người dẫn (%s)." % winner) if stable else ( "Kết quả NHẠY — '%s' chỉ dẫn khi trọng số như hiện tại; đổi %s là lật. Cần thêm dữ liệu trước khi chốt." % (winner, ", ".join(f["criterion"] for f in flips))) return {"stable": stable, "winner": winner, "flips": flips, "note": note} # ---------- prioritize: ma trận ưu tiên use-case AI ---------- QUAD = { # (impact_high, feasibility_high) -> (key, vi) (True, True): ("quick_win", "Làm ngay (Quick Win)"), (True, False): ("big_bet", "Đặt cược lớn (đáng nhưng khó)"), (False, True): ("fill_in", "Làm khi rảnh (Fill-in)"), (False, False): ("avoid", "Tránh / hoãn"), } def prioritize(data): ucs = data.get("usecases") or data.get("items") or [] if not ucs: return {"ok": False, "error": "no_usecases", "hint": "Cần danh sách use-case; xem `advisor_tools.py sample prioritize`."} crit = data.get("criteria") or {} if not crit: # mặc định 2 trục kinh điển crit = {"impact": 50, "feasibility": 50} weights = _norm_weights(crit) ranked = _rank(ucs, weights) quads = {k: [] for k, _ in QUAD.values()} qlabel = {k: vi for k, vi in QUAD.values()} for r in ranked: imp = r["scores"].get("impact", r["score"]) fea = r["scores"].get("feasibility", r["score"]) key, vi = QUAD[(imp >= HIGH, fea >= HIGH)] r["quadrant"], r["quadrant_vi"] = key, vi quads[key].append(r["name"]) sens = _sensitivity(ucs, weights) return {"ok": True, "kind": "prioritize", "title": data.get("title", "Ưu tiên use-case AI"), "criteria": [{"key": k, "weight": w} for k, w in weights.items()], "ranked": ranked, "quadrants": quads, "quadrant_labels": qlabel, "close_calls": _close_calls(ranked), "sensitivity": sens, "confidence": ("Vững — có thể chốt thứ tự ưu tiên." if sens["stable"] else "Sát nút/nhạy — kiểm chứng impact/feasibility 1–2 use-case đầu trước khi chốt.")} def decision(data): opts = data.get("options") or data.get("items") or [] if not opts: return {"ok": False, "error": "no_options", "hint": "Cần >=2 phương án; xem `advisor_tools.py sample decision`."} crit = data.get("criteria") or {} if not crit: return {"ok": False, "error": "no_criteria", "hint": "Cần tiêu chí có trọng số (vd {upside:40,risk:30,...})."} weights = _norm_weights(crit) ranked = _rank(opts, weights) sens = _sensitivity(opts, weights) return {"ok": True, "kind": "decision", "decision": data.get("decision", data.get("title", "Quyết định")), "criteria": [{"key": k, "weight": w} for k, w in weights.items()], "ranked": ranked, "close_calls": _close_calls(ranked), "sensitivity": sens, "confidence": ("Vững — phương án dẫn rõ ràng." if sens["stable"] and not _close_calls(ranked) else "Sát nút/nhạy — đừng để ma trận tự quyết; phản biện thêm rồi mới chốt.")} # ---------- buildbuy: Build vs Buy vs Partner cho 1 năng lực AI ---------- def buildbuy(data): if not (data.get("options") or data.get("items")): return {"ok": False, "error": "no_options", "hint": "Cần options (Tự xây/Mua/Hợp tác); xem `sample buildbuy`."} if not data.get("criteria"): data["criteria"] = {"chi_phi_tong": 30, "toc_do_trien_khai": 20, "chu_quyen_du_lieu": 25, "kiem_soat_tuy_bien": 15, "bao_tri_dai_han": 10} r = decision(data) if r.get("ok"): r["kind"] = "buildbuy" top = r["ranked"][0] r["recommendation"] = "Nghiêng về **%s** — %s" % (top["name"], r["confidence"]) return r # ---------- ai-risk: phân loại rủi ro 1 use-case AI ---------- RISK_FACTORS = ["data_sensitivity", "autonomy", "impact_scope", "reversibility", "compliance"] RISK_VI = {"data_sensitivity": "Độ nhạy dữ liệu", "autonomy": "Mức AI tự quyết", "impact_scope": "Phạm vi ảnh hưởng", "reversibility": "Khó đảo ngược", "compliance": "Ràng buộc pháp lý"} GUARDS = { "low": ["Tự động hoá được, vẫn ghi log để truy lại", "Người duyệt mẫu định kỳ"], "medium": ["Người trong vòng lặp ở bước quan trọng", "Ghi log + rà soát hằng tuần", "Có nút tắt / khôi phục nhanh"], "high": ["BẮT BUỘC người duyệt trước khi AI tác động ra ngoài", "Giới hạn phạm vi + kiểm thử trước khi mở rộng", "Theo dõi & cảnh báo khi lệch", "Kế hoạch khôi phục rõ ràng"], "critical": ["KHÔNG để AI tự quyết — người quyết cuối, luôn", "Đánh giá tác động + pháp lý TRƯỚC khi chạy", "Thử nghiệm hẹp, giám sát chặt", "Lưu vết đầy đủ để truy soát & giải trình"], } def ai_risk(data): if not any(k in data for k in RISK_FACTORS): return {"ok": False, "error": "no_factors", "hint": "Chấm 1–5 mỗi yếu tố (5=rủi ro cao); xem `sample ai-risk`."} sc = {k: int(_num(data.get(k, 3), 1, 5, 3)) for k in RISK_FACTORS} avg = sum(sc.values()) / len(sc) key, tier = ("low", "Thấp") if avg <= 2 else ("medium", "Trung bình") if avg <= 3 else \ ("high", "Cao") if avg <= 4 else ("critical", "Nghiêm trọng") drivers = [RISK_VI[k] for k in sorted(sc, key=lambda x: sc[x], reverse=True)[:2] if sc[k] >= 3] return {"ok": True, "kind": "ai-risk", "name": data.get("name", "use-case"), "factors": {RISK_VI[k]: v for k, v in sc.items()}, "score": round(avg, 2), "tier": tier, "tier_key": key, "drivers": drivers, "guardrails": GUARDS[key], "note": "Rủi ro cao KHÔNG có nghĩa 'đừng làm' — nghĩa là làm CÓ rào chắn. AI cố vấn, người quyết."} # ---------- unit economics: LTV / CAC / payback ---------- def unit_economics(data): arpa = _f(data.get("arpa")) # doanh thu/khách/tháng (VND) churn = _f(data.get("monthly_churn")) # % rời bỏ/tháng gm = _f(data.get("gross_margin")) # biên lợi nhuận gộp % cac = _f(data.get("cac")) # chi phí có 1 khách if not arpa or not churn or churn <= 0: return {"ok": False, "error": "need_arpa_and_churn", "hint": "Cần arpa (doanh thu/khách/tháng) và monthly_churn (% >0)."} gm_frac = (gm / 100.0) if gm else 1.0 life = round(1.0 / (churn / 100.0), 1) # số tháng khách ở lại ltv = round(arpa * gm_frac * life) ratio = round(ltv / cac, 2) if cac else None payback = round(cac / (arpa * gm_frac), 1) if (cac and arpa * gm_frac > 0) else None flags = [] if ratio is not None: flags.append("LTV:CAC = %s — %s" % (ratio, "khoẻ (≥3)" if ratio >= 3 else "tạm (1–3), cần cải thiện" if ratio >= 1 else "ĐANG LỖ trên mỗi khách (<1)")) if payback is not None: flags.append("Hoàn vốn CAC ~%s tháng — %s" % (payback, "tốt (<12)" if payback < 12 else "hơi dài (>12)")) flags.append("Khách ở lại trung bình ~%s tháng (churn %s%%/tháng)" % (life, churn)) return {"ok": True, "kind": "unit-economics", "ltv": ltv, "cac": cac, "ltv_cac_ratio": ratio, "payback_months": payback, "lifetime_months": life, "gross_margin_pct": gm, "flags": flags, "note": "Số chỉ tốt khi đầu vào thật. Đây là ước tính từ con số user cung cấp — kiểm chứng trước khi quyết."} # ---------- runway: vốn còn trụ được bao lâu ---------- def runway(data): cash = _f(data.get("cash")) # tiền mặt còn (VND) burn = _f(data.get("monthly_burn")) # đốt ròng/tháng if burn is None: rev, exp = _f(data.get("monthly_revenue")) or 0.0, _f(data.get("monthly_expense")) or 0.0 burn = exp - rev if cash is None: return {"ok": False, "error": "need_cash", "hint": "Cần cash (tiền mặt còn) + monthly_burn (hoặc revenue/expense)."} if burn <= 0: return {"ok": True, "kind": "runway", "cash": cash, "monthly_burn": burn, "runway_months": None, "verdict": "Dòng tiền dương (không đốt vốn) — tập trung tái đầu tư có kỷ luật."} months = round(cash / burn, 1) verdict = ("🚨 NGUY HIỂM (<6 tháng) — ưu tiên dòng tiền/gọi vốn ngay." if months < 6 else "⚠️ Cảnh báo (6–12 tháng) — siết chi & tăng thu, chuẩn bị phương án vốn." if months < 12 else "Ổn (12–18 tháng) — vẫn theo dõi sát." if months < 18 else "Thoải mái (>18 tháng) — đầu tư tăng trưởng có kỷ luật.") return {"ok": True, "kind": "runway", "cash": cash, "monthly_burn": burn, "runway_months": months, "verdict": verdict, "note": "Giả định burn không đổi. Kiểm lại khi chi/thu thay đổi."} # ---------- pricing: biên lợi nhuận theo sản phẩm + kịch bản giá ---------- def pricing(data): items = data.get("items") or data.get("products") or [] if not items: return {"ok": False, "error": "no_items", "hint": "Cần items [{name, cost, price}]; xem `sample pricing`."} target = _f(data.get("target_margin")) target = target if (target and 0 < target < 100) else 40.0 # biên mục tiêu mặc định 40% rows = [] for it in items: cost = _f(it.get("cost")) or 0.0 price = _f(it.get("price")) or 0.0 if price <= 0: continue margin_pct = round((price - cost) / price * 100, 1) price_target = round(cost / (1 - target / 100.0)) if cost else price # giá để đạt biên mục tiêu scen = [{"label": "Giá hiện tại", "price": round(price), "margin_pct": margin_pct}] for bump in (10, 20): # kịch bản tăng giá p = price * (1 + bump / 100.0) scen.append({"label": "+%d%%" % bump, "price": round(p), "margin_pct": round((p - cost) / p * 100, 1)}) scen.append({"label": "Đạt biên %g%%" % target, "price": price_target, "margin_pct": target if cost else margin_pct}) rows.append({"name": str(it.get("name", "?")), "cost": round(cost), "price": round(price), "margin_amount": round(price - cost), "margin_pct": margin_pct, "below_target": margin_pct < target, "price_for_target": price_target, "scenarios": scen}) if not rows: return {"ok": False, "error": "no_valid_items", "hint": "Mỗi item cần price > 0."} below = [r["name"] for r in rows if r["below_target"]] avg = round(sum(r["margin_pct"] for r in rows) / len(rows), 1) return {"ok": True, "kind": "pricing", "target_margin": target, "avg_margin": avg, "items": rows, "below_target": below, "note": ("%d sản phẩm dưới biên mục tiêu %g%%: %s — cân tăng giá hoặc giảm giá vốn." % (len(below), target, ", ".join(below))) if below else "Mọi sản phẩm đạt/over biên mục tiêu %g%%." % target} # ---------- customer-health: chấm sức khoẻ khách hàng (RFM + hài lòng) ---------- HEALTH_SIGNALS = ["recency", "frequency", "monetary", "satisfaction"] HSIG_VI = {"recency": "Gần đây (lần mua/liên hệ cuối)", "frequency": "Tần suất mua", "monetary": "Giá trị chi tiêu", "satisfaction": "Mức hài lòng"} HEALTH_W = {"recency": 30, "frequency": 20, "monetary": 20, "satisfaction": 30} HTIER = [(75, "healthy", "Khoẻ"), (55, "ok", "Bình thường"), (40, "at_risk", "Rủi ro"), (0, "churning", "Nguy cơ rời bỏ")] HACT = {"churning": "Liên hệ NGAY — hỏi thẳng vấn đề, cân ưu đãi giữ chân.", "at_risk": "Chủ động hỏi thăm + xử lý điểm yếu sớm.", "ok": "Giữ đều; tìm cơ hội bán thêm/bán kèm.", "healthy": "Khách tốt — chăm sóc, xin giới thiệu/đánh giá."} def health(data): custs = data.get("customers") or data.get("items") or [] if not custs: return {"ok": False, "error": "no_customers", "hint": "Cần customers [{name, recency, frequency, monetary, satisfaction}] (1–5); xem `sample health`."} w = _norm_weights(data.get("weights") or HEALTH_W) rows = [] for c in custs: sc = {k: int(_num(c.get(k, 3), 1, 5, 3)) for k in HEALTH_SIGNALS} score = round(sum(sc[k] * w.get(k, 0) for k in sc) / (sum(w.values()) or 1) * 20) # 1–5 → 0–100 key, tier = next((k, vi) for thr, k, vi in HTIER if score >= thr) driver = HSIG_VI[min(sc, key=lambda k: sc[k])] rows.append({"name": str(c.get("name", "?")), "signals": {HSIG_VI[k]: v for k, v in sc.items()}, "score": score, "tier": tier, "tier_key": key, "driver": driver, "action": HACT[key]}) rows.sort(key=lambda r: r["score"]) # tệ nhất lên đầu (cần xử lý trước) by_tier = {} for r in rows: by_tier[r["tier_key"]] = by_tier.get(r["tier_key"], 0) + 1 at_risk = [r["name"] for r in rows if r["tier_key"] in ("at_risk", "churning")] avg = round(sum(r["score"] for r in rows) / len(rows)) return {"ok": True, "kind": "customer-health", "customers": rows, "avg_score": avg, "by_tier": by_tier, "at_risk": at_risk, "note": ("%d khách đang rủi ro/nguy cơ rời bỏ: %s — ưu tiên giữ chân (rẻ hơn tìm khách mới)." % (len(at_risk), ", ".join(at_risk))) if at_risk else "Danh mục khách ổn — duy trì chăm sóc."} def _read_customers(): try: return json.load(open(os.path.join(BUSINESS, "customers_health.json"), encoding="utf-8")) except Exception: return {} def save_customers(data): if not _is_admin(): return {"ok": False, "error": "not_admin", "hint": "Chỉ CEO/admin lưu danh sách khách công ty."} if not (data.get("customers") or data.get("items")): return {"ok": False, "error": "no_customers"} os.makedirs(BUSINESS, exist_ok=True) tmp = os.path.join(BUSINESS, "customers_health.json") + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) os.replace(tmp, os.path.join(BUSINESS, "customers_health.json")) if _activity: try: _activity.log("customer-health", "customers_saved", meta={"count": len(data.get("customers") or [])}) except Exception: pass return {"ok": True, "count": len(data.get("customers") or data.get("items") or [])} def _read_usecases(): try: return json.load(open(USECASES, encoding="utf-8")) except Exception: return {} def save_usecases(data): if not _is_admin(): return {"ok": False, "error": "not_admin", "hint": "Chỉ CEO/admin lưu danh sách use-case công ty."} if not (data.get("usecases") or data.get("items")): return {"ok": False, "error": "no_usecases"} os.makedirs(BUSINESS, exist_ok=True) tmp = USECASES + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) os.replace(tmp, USECASES) if _activity: try: _activity.log("roi-scorecard", "usecases_saved", meta={"count": len(data.get("usecases") or [])}) except Exception: pass return {"ok": True, "saved": USECASES, "count": len(data.get("usecases") or data.get("items") or [])} SAMPLE = { "prioritize": {"title": "Ưu tiên use-case AI — ", "criteria": {"impact": 45, "feasibility": 35, "strategic_fit": 20}, "usecases": [ {"name": "Chatbot CSKH 24/7", "impact": 8, "feasibility": 7, "strategic_fit": 7, "note": "Giảm tải hỗ trợ"}, {"name": "Tự động hoá báo cáo bán hàng", "impact": 6, "feasibility": 8, "strategic_fit": 5}, {"name": "Mô hình dự báo nhu cầu", "impact": 9, "feasibility": 4, "strategic_fit": 8}]}, "decision": {"decision": "Tự xây hay mua giải pháp AI CSKH?", "criteria": {"chi_phi_3_nam": 30, "toc_do_trien_khai": 25, "chu_quyen_du_lieu": 25, "linh_hoat": 20}, "options": [ {"name": "Tự xây (self-host)", "scores": {"chi_phi_3_nam": 7, "toc_do_trien_khai": 4, "chu_quyen_du_lieu": 9, "linh_hoat": 9}}, {"name": "Mua SaaS", "scores": {"chi_phi_3_nam": 5, "toc_do_trien_khai": 9, "chu_quyen_du_lieu": 4, "linh_hoat": 5}}]}, "buildbuy": {"decision": "Tự xây / Mua / Hợp tác cho chatbot CSKH AI?", "criteria": {"chi_phi_tong": 30, "toc_do_trien_khai": 20, "chu_quyen_du_lieu": 25, "kiem_soat_tuy_bien": 15, "bao_tri_dai_han": 10}, "options": [ {"name": "Tự xây (self-host)", "scores": {"chi_phi_tong": 6, "toc_do_trien_khai": 4, "chu_quyen_du_lieu": 9, "kiem_soat_tuy_bien": 9, "bao_tri_dai_han": 5}}, {"name": "Mua SaaS", "scores": {"chi_phi_tong": 5, "toc_do_trien_khai": 9, "chu_quyen_du_lieu": 4, "kiem_soat_tuy_bien": 5, "bao_tri_dai_han": 8}}, {"name": "Hợp tác đối tác triển khai", "scores": {"chi_phi_tong": 6, "toc_do_trien_khai": 7, "chu_quyen_du_lieu": 7, "kiem_soat_tuy_bien": 7, "bao_tri_dai_han": 7}}]}, "ai-risk": {"name": "Chatbot tự trả lời khiếu nại khách (không người duyệt)", "data_sensitivity": 4, "autonomy": 5, "impact_scope": 4, "reversibility": 3, "compliance": 3}, "unit-economics": {"arpa": 500000, "monthly_churn": 5, "gross_margin": 70, "cac": 2000000}, "runway": {"cash": 800000000, "monthly_burn": 120000000}, "pricing": {"target_margin": 40, "items": [ # mẫu TRUNG LẬP NGÀNH — khách thay bằng sản phẩm của họ {"name": "Sản phẩm A (bán chạy)", "cost": 60000, "price": 150000}, {"name": "Sản phẩm B (cao cấp)", "cost": 200000, "price": 300000}, {"name": "Dịch vụ kèm theo", "cost": 100000, "price": 500000}]}, "health": {"customers": [ # mỗi tín hiệu 1–5 (5=tốt nhất); recency 5 = vừa mua/liên hệ {"name": "Khách hàng A", "recency": 5, "frequency": 4, "monetary": 4, "satisfaction": 5}, {"name": "Khách hàng B", "recency": 2, "frequency": 2, "monetary": 3, "satisfaction": 2}, {"name": "Khách hàng C", "recency": 3, "frequency": 5, "monetary": 5, "satisfaction": 4}]}, } def _load_arg(): """Đọc input JSON từ --file hoặc stdin; rỗng → đọc kho company use-cases.""" a = sys.argv if "--file" in a: return json.load(open(a[a.index("--file") + 1], encoding="utf-8")) if not sys.stdin.isatty(): raw = sys.stdin.read().strip() if raw: return json.loads(raw) return None def main(): cmd = sys.argv[1] if len(sys.argv) > 1 else "prioritize" try: if cmd == "sample": which = sys.argv[2] if len(sys.argv) > 2 else "prioritize" out = SAMPLE.get(which, SAMPLE["prioritize"]) elif cmd == "prioritize": out = prioritize(_load_arg() or _read_usecases()) elif cmd == "decision": data = _load_arg() out = decision(data) if data else {"ok": False, "error": "no_input", "hint": "Cần --file hoặc stdin."} elif cmd == "buildbuy": data = _load_arg() out = buildbuy(data) if data else {"ok": False, "error": "no_input", "hint": "Cần --file hoặc stdin."} elif cmd == "ai-risk": data = _load_arg() out = ai_risk(data) if data else {"ok": False, "error": "no_input", "hint": "Cần --file hoặc stdin."} elif cmd in ("unit-economics", "unit_economics"): data = _load_arg() out = unit_economics(data) if data else {"ok": False, "error": "no_input", "hint": "Cần --file hoặc stdin."} elif cmd == "runway": data = _load_arg() out = runway(data) if data else {"ok": False, "error": "no_input", "hint": "Cần --file hoặc stdin."} elif cmd == "pricing": data = _load_arg() out = pricing(data) if data else {"ok": False, "error": "no_input", "hint": "Cần --file hoặc stdin."} elif cmd in ("health", "customer-health"): out = health(_load_arg() or _read_customers()) # không input → đọc kho đã lưu (dashboard) elif cmd == "save-customers": data = _load_arg() out = save_customers(data) if data else {"ok": False, "error": "no_input"} elif cmd == "save-usecases": data = _load_arg() out = save_usecases(data) if data else {"ok": False, "error": "no_input"} else: out = {"ok": False, "error": "unknown_cmd", "cmd": cmd} except Exception as e: out = {"ok": False, "error": str(e)} print(json.dumps(out, ensure_ascii=False)) if __name__ == "__main__": main()