#!/usr/bin/env python3 """mentors.py — Business Mentors (personas cố vấn) cho dashboard. CHỈ-ĐỌC. list -> danh sách mentor (persona có skill learn/business-mentor) + số khung/mô hình. detail -> 1 mentor: đặc điểm (giọng) + 'cung cấp gì' (frameworks + mental_models) + cách dùng. Đọc personas/*.yaml + content//. """ import os import re import sys import json try: import yaml except Exception: yaml = None def _bundle_root(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = _bundle_root() PERSONAS = os.path.join(ROOT, "personas") CONTENT = os.path.join(ROOT, "content") SKILLS = os.path.join(ROOT, "skills") # nhãn tiếng Việt dự phòng khi SKILL.md không có sẵn trong bundle (tier thấp) SKILL_LABELS = { "learn/business-mentor": ("Cố vấn kinh doanh 1-1", "Dạy khung & tư vấn theo tình huống thật của doanh nghiệp bạn."), "learn/ai-mentor": ("AI Mentor", "Dạy dùng AI theo giáo trình, mỗi buổi ra 1 đầu ra thật."), "operate/startup-kit": ("Dựng bộ khung khởi nghiệp", "Điền BMC, Value Prop… từng bước, lưu vào hồ sơ công ty."), "operate/decision-council": ("Hội đồng phản biện AI", "Soi một quyết định đa lăng kính (tài chính/khách/vận hành/rủi ro) trước khi chốt."), "operate/kpi": ("Số sức khoẻ tài chính", "LTV, CAC, hoàn vốn (payback) & runway — vốn còn trụ bao lâu."), "operate/roi-scorecard": ("Ưu tiên use-case AI", "Xếp hạng ý tưởng AI theo Tác động × Khả thi."), "operate/ai-decisions": ("Quyết định AI: Build–Buy & Rủi ro", "Tự xây/Mua/Hợp tác + phân loại rủi ro use-case AI và rào chắn."), "operate/pricing": ("Giá & biên lợi nhuận", "Bảng biên LN từng sản phẩm + kịch bản tăng giá."), "operate/customer-health": ("Sức khoẻ khách hàng", "Xếp khách Khoẻ/Rủi ro/Nguy cơ rời bỏ + việc giữ chân."), "operate/marketing-plan": ("Kế hoạch marketing", "Chiến lược AARRR + ý tưởng + lộ trình 90 ngày."), "operate/copywriting": ("Viết nội dung bán hàng", "Landing/quảng cáo/email theo nguyên tắc + tâm lý."), "operate/social-content": ("Nội dung mạng xã hội", "Ý tưởng bài + lịch đăng theo nền tảng."), "operate/customer-research": ("Nghiên cứu khách hàng", "Voice of Customer: hiểu khách thật sự cần gì."), "operate/campaign-launch": ("Ra mắt & chiến dịch", "Nhịp ra mắt + giới thiệu + mồi thu data + PR."), "operate/ai-seo": ("Để AI tìm thấy bạn (AEO)", "Tối ưu để ChatGPT/Perplexity/Google AI Overview trích dẫn."), "operate/seo": ("SEO Google", "Audit + on-page + dữ liệu có cấu trúc (schema) để được tìm thấy."), "operate/company-assessment": ("Đánh giá tình hình công ty", "Đọc hồ sơ đã điền → điểm mạnh, lỗ hổng, việc nên ưu tiên tiếp."), "operate/canvas-image": ("Vẽ ảnh canvas", "Render canvas gửi qua Telegram."), } def _load_yaml(p): try: if yaml: return yaml.safe_load(open(p, encoding="utf-8")) or {} except Exception: pass return {} def _is_mentor(pz): return "learn/business-mentor" in (pz.get("skills") or []) def _curriculum(pz): return (pz.get("content") or {}).get("curriculum") def _count_md(base, sub): d = os.path.join(base, sub) return len([f for f in os.listdir(d) if f.endswith(".md")]) if os.path.isdir(d) else 0 def list_mentors(): out = [] if os.path.isdir(PERSONAS): for fn in sorted(os.listdir(PERSONAS)): if not fn.endswith(".yaml") or fn.startswith("_"): continue pz = _load_yaml(os.path.join(PERSONAS, fn)) if not pz or not _is_mentor(pz): continue cur = _curriculum(pz) base = os.path.join(CONTENT, cur) if cur else "" fw = _count_md(base, "frameworks") if base else 0 mm = _count_md(base, "mental_models") if base else 0 out.append({"id": pz.get("id"), "name": pz.get("display_name"), "summary": pz.get("summary", ""), "when": pz.get("when", ""), "mode": pz.get("mode", ""), "status": pz.get("status", ""), "tiers": pz.get("tiers") or [], "frameworks": fw, "mental_models": mm, "general": (pz.get("role") == "orchestrator"), # role tường minh: cố vấn TỔNG điều phối "has_content": bool(cur)}) out.sort(key=lambda m: (not m["general"], m["name"] or "")) # cố vấn tổng đứng trước return {"ok": True, "mentors": out} def _md_meta(path): """H1 title + dòng mô tả đầu (**Là gì:** / **Mục tiêu:**).""" title, desc = "", "" try: for ln in open(path, encoding="utf-8"): s = ln.strip() if not title and s.startswith("# "): title = s[2:].strip() elif title and not desc and re.match(r"\*\*(Là gì|Mục tiêu|Là)", s): desc = re.sub(r"\*\*[^*]+:?\*\*", "", s).strip(" :") break except Exception: pass return title, desc def _list_md(base, sub, limit=40): out = [] d = os.path.join(base, sub) if base else "" if d and os.path.isdir(d): for f in sorted(os.listdir(d)): if f.endswith(".md"): t, ds = _md_meta(os.path.join(d, f)) out.append({"id": f[:-3], "title": t or f[:-3], "desc": ds}) return out[:limit] def _voice(base): """Rút 'Tông & nhịp' (traits) + vài câu chữ ký từ voice.md.""" traits, sig = [], [] p = os.path.join(base, "voice.md") if base else "" if not (p and os.path.isfile(p)): return traits, sig sec = None for ln in open(p, encoding="utf-8"): s = ln.rstrip() if s.startswith("## "): h = s[3:].lower() sec = "tone" if ("tông" in h or "nhịp" in h) else ("sig" if "chữ ký" in h else None) continue if sec == "tone" and s.strip().startswith("- "): t = re.sub(r"[*`]", "", s.strip()[2:]).strip() if t: traits.append(t) elif sec == "sig": m = re.match(r"^\d+\.\s*(.+)$", s.strip()) if m: t = re.sub(r"\*\([^)]*\)\*", "", m.group(1)).strip().strip('"').strip() if t: sig.append(t) return traits[:7], sig[:8] def _skill_meta(sid): """skill id → {id, name, desc}. Đọc SKILL.md nếu có; nếu không, dùng nhãn dự phòng.""" sid2 = re.sub(r"[^a-z0-9/_-]", "", str(sid or "")) p = os.path.join(SKILLS, *sid2.split("/")) + os.sep + "SKILL.md" skmd_name, skmd_desc = "", "" try: t = open(p, encoding="utf-8").read() if t.startswith("---") and yaml: y = yaml.safe_load(t.split("---", 2)[1]) or {} skmd_name = (y.get("name") or "").strip() skmd_desc = (y.get("description") or "").strip() except Exception: pass lbl = SKILL_LABELS.get(sid2) name = (lbl[0] if lbl else "") or skmd_name or sid2.split("/")[-1] # ưu tiên nhãn VN dễ đọc desc = skmd_desc or (lbl[1] if lbl else "") # ưu tiên mô tả thật từ SKILL.md if desc: desc = re.split(r"(?<=[.!?。])\s", desc)[0].strip() if len(desc) > 140: desc = desc[:140].rstrip() + "…" return {"id": sid, "name": name, "desc": desc} def _lessons(base): """Curriculum dạng bài học (vd ai-bos): đọc 00_meta.yaml → [{id,title,stage,status}].""" meta = os.path.join(base, "00_meta.yaml") if base else "" if not (yaml and meta and os.path.isfile(meta)): return [] try: m = yaml.safe_load(open(meta, encoding="utf-8")) or {} except Exception: return [] out = [] for L in (m.get("lessons") or []): title = "" if L.get("path"): fp = os.path.join(CONTENT, L["path"]) try: # ưu tiên frontmatter title của bài học t = open(fp, encoding="utf-8").read() if t.startswith("---") and yaml: title = ((yaml.safe_load(t.split("---", 2)[1]) or {}).get("title") or "").strip() except Exception: pass if not title: title = _md_meta(fp)[0] out.append({"id": L.get("id"), "title": title or L.get("id", ""), "stage": L.get("stage", ""), "status": L.get("status", "")}) return out def _kb_topics(rel): """Kho tri thức nền persona dựa vào (vd content/knowledge_base/) → [{id,title,desc}]. Đọc frontmatter title (dạng 'Tên: mô tả'). Theo tier: instance chỉ có phần KB được ship.""" rel = (rel or "").strip().strip("/") if not rel: return [] d = os.path.join(ROOT, *rel.split("/")) out = [] if os.path.isdir(d): for f in sorted(os.listdir(d)): if not f.endswith(".md") or f.startswith("_"): continue title = "" try: t = open(os.path.join(d, f), encoding="utf-8").read() if t.startswith("---") and yaml: title = ((yaml.safe_load(t.split("---", 2)[1]) or {}).get("title") or "").strip() except Exception: pass if not title: title = _md_meta(os.path.join(d, f))[0] or f[:-3] name, _, desc = title.partition(":") out.append({"id": f[:-3], "title": name.strip(), "desc": desc.strip()}) return out def detail(mid): mid = re.sub(r"[^a-z0-9_]", "", (mid or "").lower()) p = os.path.join(PERSONAS, mid + ".yaml") if not mid or not os.path.isfile(p): return {"ok": False, "error": "not_found"} pz = _load_yaml(p) cur = _curriculum(pz) base = os.path.join(CONTENT, cur) if cur else "" traits, sig = _voice(base) fws, mms = _list_md(base, "frameworks"), _list_md(base, "mental_models") is_general = (pz.get("role") == "orchestrator") # role tường minh: cố vấn TỔNG specialists = [] if is_general: # liệt kê chuyên gia mentor tổng có thể gọi for m in list_mentors().get("mentors", []): if m["id"] != mid and not m.get("general"): specialists.append({"id": m["id"], "name": m["name"], "summary": m["summary"]}) return {"ok": True, "id": mid, "name": pz.get("display_name"), "summary": pz.get("summary", ""), "when": pz.get("when", ""), "mode": pz.get("mode", ""), "status": pz.get("status", ""), "tiers": pz.get("tiers") or [], "org_roles": (pz.get("access") or {}).get("org_roles") or [], "general": is_general, "specialists": specialists, "skills": [_skill_meta(s) for s in (pz.get("skills") or [])], "frameworks": fws, "mental_models": mms, "lessons": _lessons(base), "knowledge": _kb_topics((pz.get("content") or {}).get("knowledge_base", "")), "has_heuristics": bool(base and os.path.isfile(os.path.join(base, "heuristics.md"))), "traits": traits, "signatures": sig} def main(): a = sys.argv[1:] cmd = a[0] if a else "list" if cmd == "list": out = list_mentors() elif cmd == "detail": out = detail(a[1] if len(a) > 1 else "") else: out = {"ok": False, "error": "unknown cmd"} print(json.dumps(out, ensure_ascii=False)) return 0 if __name__ == "__main__": sys.exit(main())