sophhub 0.4.21 → 0.4.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/agents/parent-toddler/.config.json +7 -1
  2. package/agents/parent-toddler/HEARTBEAT.md +4 -4
  3. package/agents/parent-toddler/TOOLS.md +9 -0
  4. package/agents/parent-toddler/scripts/compact_sessions_over_threshold.py +368 -0
  5. package/package.json +1 -1
  6. package/skills/agent-install/src/SKILL.md +2 -0
  7. package/skills/agent-install/src/scripts/common.py +20 -0
  8. package/skills/agent-install/src/scripts/update_openclaw.py +5 -0
  9. package/skills/image-classify/skill.json +12 -5
  10. package/skills/image-classify/src/SKILL.md +1 -1
  11. package/skills/image-classify/src/references/config.json +1 -1
  12. package/skills/insurance-customer-policy/skill.json +12 -0
  13. package/skills/insurance-customer-policy/src/SKILL.md +121 -0
  14. package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
  15. package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
  16. package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
  17. package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
  18. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
  19. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
  20. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
  21. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
  22. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
  23. package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
  24. package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
  25. package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
  26. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
  27. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
  28. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
  29. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
  30. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
  31. package/skills/insurance-product-analysis/skill.json +12 -0
  32. package/skills/insurance-product-analysis/src/SKILL.md +99 -0
  33. package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
  34. package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
  35. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
  36. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
  37. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
  38. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
  39. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
  40. package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
  41. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
  42. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
  43. package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
  44. package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
  45. package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
  46. package/skills/insurance-sales-pipeline/skill.json +12 -0
  47. package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
  48. package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
  49. package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
  50. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
  51. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
  52. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
  53. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
  54. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
  55. package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
  56. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
  57. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
  58. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
  59. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
  60. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
  61. package/skills/insurance-schedule-renewal/skill.json +12 -0
  62. package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
  63. package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
  64. package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
  65. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
  66. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
  67. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
  68. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
  69. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
  70. package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
  71. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
  72. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
  73. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
  74. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
  75. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
  76. package/skills/insurance-shared-data/skill.json +20 -0
  77. package/skills/insurance-shared-data/src/SKILL.md +33 -0
  78. package/skills/insurance-shared-data/src/scripts/cloud_insurance_super_test.py +246 -0
@@ -0,0 +1,180 @@
1
+ """Shared SQLite schema for insurance skills."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sqlite3
6
+ from pathlib import Path
7
+
8
+
9
+ def default_db_path() -> Path:
10
+ base = Path.home() / ".config" / "insurance"
11
+ base.mkdir(parents=True, exist_ok=True)
12
+ return base / "insurance.sqlite3"
13
+
14
+
15
+ def get_db_path() -> Path:
16
+ for key in ("INSURANCE_DB_PATH", "INSURANCE_PRODUCT_DB_PATH"):
17
+ p = os.environ.get(key)
18
+ if p:
19
+ path = Path(p)
20
+ path.parent.mkdir(parents=True, exist_ok=True)
21
+ return path
22
+ return default_db_path()
23
+
24
+
25
+ def connect() -> sqlite3.Connection:
26
+ conn = sqlite3.connect(str(get_db_path()))
27
+ conn.row_factory = sqlite3.Row
28
+ conn.execute("PRAGMA foreign_keys=ON")
29
+ init_schema(conn)
30
+ return conn
31
+
32
+
33
+ def init_schema(conn: sqlite3.Connection) -> None:
34
+ conn.executescript(
35
+ """
36
+ CREATE TABLE IF NOT EXISTS customers (
37
+ id TEXT PRIMARY KEY,
38
+ name TEXT NOT NULL,
39
+ phone TEXT,
40
+ age INTEGER,
41
+ gender TEXT,
42
+ income REAL,
43
+ household_json TEXT NOT NULL DEFAULT '{}',
44
+ occupation TEXT,
45
+ birthday TEXT,
46
+ tags_json TEXT NOT NULL DEFAULT '[]',
47
+ fields_json TEXT NOT NULL DEFAULT '{}',
48
+ status TEXT NOT NULL DEFAULT 'active',
49
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
50
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
51
+ );
52
+ CREATE INDEX IF NOT EXISTS idx_customers_phone ON customers(phone);
53
+ CREATE INDEX IF NOT EXISTS idx_customers_birthday ON customers(birthday);
54
+
55
+ CREATE TABLE IF NOT EXISTS policies (
56
+ id TEXT PRIMARY KEY,
57
+ customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
58
+ company TEXT NOT NULL,
59
+ product_name TEXT NOT NULL,
60
+ product_type TEXT NOT NULL,
61
+ coverage REAL NOT NULL DEFAULT 0,
62
+ premium REAL NOT NULL DEFAULT 0,
63
+ pay_period TEXT,
64
+ effective_date TEXT NOT NULL,
65
+ expire_date TEXT,
66
+ next_pay_date TEXT,
67
+ insured_name TEXT,
68
+ beneficiary TEXT,
69
+ notes TEXT,
70
+ status TEXT NOT NULL DEFAULT 'active',
71
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
72
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
73
+ );
74
+ CREATE INDEX IF NOT EXISTS idx_policies_customer ON policies(customer_id, status);
75
+ CREATE INDEX IF NOT EXISTS idx_policies_expire ON policies(expire_date, status);
76
+ CREATE INDEX IF NOT EXISTS idx_policies_next_pay ON policies(next_pay_date, status);
77
+ CREATE INDEX IF NOT EXISTS idx_policies_type ON policies(product_type, status);
78
+
79
+ CREATE TABLE IF NOT EXISTS followups (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ customer_id TEXT NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
82
+ content TEXT NOT NULL,
83
+ next_step TEXT,
84
+ next_date TEXT,
85
+ channel TEXT,
86
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
87
+ );
88
+ CREATE INDEX IF NOT EXISTS idx_followups_customer ON followups(customer_id, created_at DESC);
89
+
90
+ CREATE TABLE IF NOT EXISTS customer_fields (
91
+ name TEXT PRIMARY KEY,
92
+ type TEXT NOT NULL DEFAULT 'text'
93
+ );
94
+
95
+ CREATE TABLE IF NOT EXISTS products (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ name TEXT NOT NULL UNIQUE,
98
+ company TEXT NOT NULL,
99
+ product_type TEXT NOT NULL,
100
+ coverage_min REAL,
101
+ coverage_max REAL,
102
+ premium_min REAL,
103
+ premium_max REAL,
104
+ age_min INTEGER,
105
+ age_max INTEGER,
106
+ waiting_period TEXT,
107
+ deductible TEXT,
108
+ renewal_terms TEXT,
109
+ highlights_json TEXT NOT NULL DEFAULT '[]',
110
+ target_audience TEXT,
111
+ file_path TEXT,
112
+ raw_text TEXT,
113
+ status TEXT NOT NULL DEFAULT 'active',
114
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
115
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
116
+ );
117
+ CREATE INDEX IF NOT EXISTS idx_products_type ON products(product_type, status);
118
+ CREATE INDEX IF NOT EXISTS idx_products_company ON products(company, status);
119
+
120
+ CREATE TABLE IF NOT EXISTS product_analysis_cache (
121
+ product_id INTEGER PRIMARY KEY REFERENCES products(id) ON DELETE CASCADE,
122
+ key_terms_json TEXT,
123
+ selling_points_json TEXT,
124
+ fit_audience_json TEXT,
125
+ pitch_script TEXT,
126
+ analyzed_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
127
+ );
128
+
129
+ CREATE TABLE IF NOT EXISTS deals (
130
+ id TEXT PRIMARY KEY,
131
+ customer_id TEXT NOT NULL,
132
+ customer_name TEXT,
133
+ product_name TEXT,
134
+ product_type TEXT,
135
+ stage TEXT NOT NULL,
136
+ estimated_premium REAL NOT NULL DEFAULT 0,
137
+ actual_premium REAL,
138
+ expected_close_date TEXT,
139
+ closed_at TEXT,
140
+ lost_reason TEXT,
141
+ lost_note TEXT,
142
+ note TEXT,
143
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
144
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
145
+ );
146
+ CREATE INDEX IF NOT EXISTS idx_deals_stage ON deals(stage, expected_close_date);
147
+ CREATE INDEX IF NOT EXISTS idx_deals_customer ON deals(customer_id);
148
+
149
+ CREATE TABLE IF NOT EXISTS deal_stage_log (
150
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
151
+ deal_id TEXT NOT NULL REFERENCES deals(id) ON DELETE CASCADE,
152
+ from_stage TEXT,
153
+ to_stage TEXT NOT NULL,
154
+ note TEXT,
155
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
156
+ );
157
+ CREATE INDEX IF NOT EXISTS idx_stage_log_deal ON deal_stage_log(deal_id, created_at);
158
+
159
+ CREATE TABLE IF NOT EXISTS monthly_targets (
160
+ month TEXT PRIMARY KEY,
161
+ target_premium REAL NOT NULL,
162
+ note TEXT,
163
+ created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
164
+ updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
165
+ );
166
+
167
+ CREATE TABLE IF NOT EXISTS sent_reminders (
168
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
169
+ reminder_type TEXT NOT NULL,
170
+ target_id TEXT NOT NULL,
171
+ bucket TEXT NOT NULL,
172
+ sent_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
173
+ status TEXT NOT NULL DEFAULT 'sent',
174
+ note TEXT,
175
+ UNIQUE (reminder_type, target_id, bucket)
176
+ );
177
+ CREATE INDEX IF NOT EXISTS idx_sent_target ON sent_reminders(target_id, reminder_type);
178
+ """
179
+ )
180
+ conn.commit()
@@ -0,0 +1,163 @@
1
+ """Shared-db read helpers for insurance-product-analysis."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sqlite3
6
+ from datetime import datetime, timedelta
7
+
8
+ from insurance_product_cli import db
9
+
10
+
11
+ def gap_analysis(customer_id: str | None = None, gap_type: str | None = None,
12
+ all_customers: bool = False) -> dict:
13
+ conn = db.connect()
14
+ try:
15
+ need_types = ["重疾险", "医疗险", "意外险", "寿险", "养老险", "教育金"]
16
+ if customer_id:
17
+ row = conn.execute(
18
+ "SELECT id, name FROM customers WHERE id=? AND status='active'",
19
+ (customer_id,),
20
+ ).fetchone()
21
+ if not row:
22
+ return {"ok": False, "error": "customer_not_found"}
23
+ results = [row]
24
+ elif all_customers or gap_type:
25
+ results = list(
26
+ conn.execute("SELECT id, name FROM customers WHERE status='active' ORDER BY id")
27
+ )
28
+ else:
29
+ return {"ok": False, "error": "need_customer_id_or_all"}
30
+
31
+ payload = []
32
+ for r in results:
33
+ owned = {
34
+ row["product_type"]
35
+ for row in conn.execute(
36
+ "SELECT DISTINCT product_type FROM policies WHERE customer_id=? AND status='active'",
37
+ (r["id"],),
38
+ )
39
+ if row["product_type"]
40
+ }
41
+ gaps = [t for t in need_types if t not in owned]
42
+ item = {"customer_id": r["id"], "name": r["name"], "owned_types": sorted(owned), "gaps": gaps}
43
+ if customer_id:
44
+ return {"ok": True, "result": item}
45
+ payload.append(item)
46
+ if gap_type:
47
+ payload = [x for x in payload if gap_type in x.get("gaps", [])]
48
+ return {"ok": True, "results": payload, "count": len(payload)}
49
+ finally:
50
+ conn.close()
51
+
52
+
53
+ def customer_query(customer_id: str | None = None, keyword: str | None = None) -> dict:
54
+ conn = db.connect()
55
+ try:
56
+ cur = conn.cursor()
57
+ if customer_id:
58
+ rows = list(cur.execute("SELECT * FROM customers WHERE id=? AND status='active'", (customer_id,)))
59
+ elif keyword:
60
+ kw = f"%{keyword}%"
61
+ rows = list(
62
+ cur.execute(
63
+ "SELECT * FROM customers WHERE status='active' AND (id LIKE ? OR name LIKE ? OR phone LIKE ?) "
64
+ "ORDER BY created_at DESC",
65
+ (kw, kw, kw),
66
+ )
67
+ )
68
+ else:
69
+ rows = list(cur.execute("SELECT * FROM customers WHERE status='active' ORDER BY created_at DESC"))
70
+ customers: list[dict] = []
71
+ for row in rows:
72
+ d = dict(row)
73
+ d["policies_count"] = int(
74
+ conn.execute(
75
+ "SELECT COUNT(*) AS n FROM policies WHERE customer_id=? AND status='active'",
76
+ (row["id"],),
77
+ ).fetchone()["n"]
78
+ )
79
+ last_fu = conn.execute(
80
+ "SELECT MAX(created_at) AS ts FROM followups WHERE customer_id=?",
81
+ (row["id"],),
82
+ ).fetchone()["ts"]
83
+ d["last_followup_at"] = last_fu
84
+ for k in ("household_json", "tags_json", "fields_json"):
85
+ try:
86
+ d[k[:-5]] = json.loads(d.get(k) or "{}")
87
+ except json.JSONDecodeError:
88
+ d[k[:-5]] = {}
89
+ customers.append(d)
90
+ return {"ok": True, "customers": customers, "count": len(customers)}
91
+ finally:
92
+ conn.close()
93
+
94
+
95
+ def policy_query(customer_id: str | None = None, product_type: str | None = None) -> dict:
96
+ conn = db.connect()
97
+ try:
98
+ cur = conn.cursor()
99
+ if customer_id:
100
+ rows = list(
101
+ cur.execute(
102
+ "SELECT * FROM policies WHERE customer_id=? AND status='active' ORDER BY effective_date DESC",
103
+ (customer_id,),
104
+ )
105
+ )
106
+ elif product_type:
107
+ rows = list(
108
+ cur.execute(
109
+ "SELECT * FROM policies WHERE product_type=? AND status='active' ORDER BY effective_date DESC",
110
+ (product_type,),
111
+ )
112
+ )
113
+ else:
114
+ rows = list(cur.execute("SELECT * FROM policies WHERE status='active' ORDER BY effective_date DESC"))
115
+ return {"ok": True, "policies": [dict(r) for r in rows], "count": len(rows)}
116
+ finally:
117
+ conn.close()
118
+
119
+
120
+ def customer_segment() -> dict:
121
+ conn = db.connect()
122
+ try:
123
+ now = datetime.now()
124
+ d30 = (now - timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
125
+ d60 = (now - timedelta(days=60)).strftime("%Y-%m-%d %H:%M:%S")
126
+ rows = list(conn.execute("SELECT * FROM customers WHERE status='active' ORDER BY created_at DESC"))
127
+ hi_value: list[dict] = []
128
+ active: list[dict] = []
129
+ new: list[dict] = []
130
+ dormant: list[dict] = []
131
+ for r in rows:
132
+ cid = r["id"]
133
+ premium = float(
134
+ conn.execute(
135
+ "SELECT COALESCE(SUM(premium),0) AS s FROM policies WHERE customer_id=? AND status='active'",
136
+ (cid,),
137
+ ).fetchone()["s"]
138
+ )
139
+ last_followup = conn.execute(
140
+ "SELECT MAX(created_at) AS ts FROM followups WHERE customer_id=?",
141
+ (cid,),
142
+ ).fetchone()["ts"]
143
+ slot = {"customer_id": cid, "name": r["name"]}
144
+ if premium > 50000:
145
+ hi_value.append(slot)
146
+ if r["created_at"] and str(r["created_at"]) >= d30:
147
+ new.append(slot)
148
+ if last_followup:
149
+ if str(last_followup) >= d30:
150
+ active.append(slot)
151
+ elif str(last_followup) < d60:
152
+ dormant.append(slot)
153
+ elif r["created_at"] and str(r["created_at"]) < d60:
154
+ dormant.append(slot)
155
+ return {
156
+ "ok": True,
157
+ "hi_value": hi_value,
158
+ "active": active,
159
+ "new": new,
160
+ "dormant": dormant,
161
+ }
162
+ finally:
163
+ conn.close()
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """E2E smoke for insurance-product-analysis over shared insurance DB."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ HERE = Path(__file__).resolve().parent
13
+
14
+
15
+ def _discover_skills_root() -> Path:
16
+ env = os.environ.get("INSURANCE_SKILLS_ROOT") or os.environ.get("SKILLS_ROOT")
17
+ if env:
18
+ p = Path(env).expanduser().resolve()
19
+ if p.is_dir():
20
+ return p
21
+ candidates = [HERE.parents[2], HERE.parents[1], Path("/app/skills"), Path.cwd() / "skills"]
22
+ for c in candidates:
23
+ try:
24
+ if (c / "insurance-customer-policy").is_dir():
25
+ return c.resolve()
26
+ except OSError:
27
+ continue
28
+ raise SystemExit("[FAIL] cannot discover skills root for smoke test")
29
+
30
+
31
+ def _resolve_customer_pkg(skills_root: Path) -> Path:
32
+ candidates = [
33
+ skills_root / "insurance-customer-policy" / "scripts",
34
+ skills_root / "insurance-customer-policy" / "src" / "scripts",
35
+ skills_root / "insurance-customer-policy" / "src" / "src",
36
+ ]
37
+ for p in candidates:
38
+ try:
39
+ if (p / "insurance_customer_cli" / "__init__.py").is_file():
40
+ return p
41
+ except OSError:
42
+ continue
43
+ raise SystemExit("[FAIL] cannot locate insurance_customer_cli package for smoke test")
44
+
45
+
46
+ SKILLS_ROOT = _discover_skills_root()
47
+ CUSTOMER_PKG = _resolve_customer_pkg(SKILLS_ROOT)
48
+
49
+
50
+ def _resolve_cli(skill_folder: str) -> Path:
51
+ candidates = [
52
+ SKILLS_ROOT / skill_folder / "scripts" / "cli.py",
53
+ SKILLS_ROOT / skill_folder / "src" / "scripts" / "cli.py",
54
+ ]
55
+ for p in candidates:
56
+ if p.is_file():
57
+ return p
58
+ raise SystemExit(f"[FAIL] cannot locate cli.py for {skill_folder}")
59
+
60
+
61
+ PRODUCT_CLI = _resolve_cli("insurance-product-analysis")
62
+ CUSTOMER_CLI = _resolve_cli("insurance-customer-policy")
63
+
64
+
65
+ def _run_json(cmd: list[str], env: dict[str, str], label: str) -> dict:
66
+ r = subprocess.run(cmd, env=env, capture_output=True, text=True, encoding="utf-8", timeout=120)
67
+ if r.returncode not in (0, 1):
68
+ raise SystemExit(f"{label} rc={r.returncode}\nCMD: {' '.join(cmd)}\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}")
69
+ out = (r.stdout or "").strip()
70
+ if not out:
71
+ raise SystemExit(f"{label} empty stdout\nCMD: {' '.join(cmd)}\nrc={r.returncode}\nSTDERR:\n{r.stderr}")
72
+ try:
73
+ return json.loads(out)
74
+ except json.JSONDecodeError as e:
75
+ raise SystemExit(f"{label} bad json: {e}\nCMD: {' '.join(cmd)}\nSTDOUT:\n{out}\nSTDERR:\n{r.stderr}")
76
+
77
+
78
+ def run_product(*args: str) -> dict:
79
+ cmd = [sys.executable, str(PRODUCT_CLI), *args]
80
+ env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
81
+ return _run_json(cmd, env, "product")
82
+
83
+
84
+ def run_customer(*args: str) -> dict:
85
+ cmd = [sys.executable, str(CUSTOMER_CLI), *args]
86
+ env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
87
+ return _run_json(cmd, env, "customer")
88
+
89
+
90
+ def assert_ok(d: dict, *args: str) -> dict:
91
+ if not d.get("ok"):
92
+ raise SystemExit(f"expected ok=True got {d} args={args}")
93
+ return d
94
+
95
+
96
+ def assert_err(d: dict, code: str, *args: str) -> None:
97
+ if d.get("ok") or d.get("error") != code:
98
+ raise SystemExit(f"expected error={code} got {d} args={args}")
99
+
100
+
101
+ def main() -> int:
102
+ tmp = tempfile.mkdtemp(prefix="ins_prod_smoke_")
103
+ shared = str(Path(tmp) / "insurance.sqlite3")
104
+ os.environ["INSURANCE_DB_PATH"] = shared
105
+ os.environ["INSURANCE_PRODUCT_DB_PATH"] = shared
106
+ os.environ["INSURANCE_CUSTOMER_DB_PATH"] = shared
107
+
108
+ print("[setup] seed customer-policy data")
109
+ assert_ok(run_customer("customer-add", "--name", "张先生", "--phone", "13800138000",
110
+ "--age", "35", "--income", "300000",
111
+ "--household", '{"spouse":true,"children":2}'))
112
+ assert_ok(run_customer("customer-add", "--name", "李女士", "--phone", "13900139000",
113
+ "--age", "28", "--income", "150000"))
114
+ assert_ok(run_customer("policy-add", "--customer-id", "C001",
115
+ "--company", "A保险", "--product", "百万医疗",
116
+ "--type", "医疗险", "--coverage", "1000000", "--premium", "500",
117
+ "--effective-date", "2024-01-01", "--pay-period", "1年"))
118
+ assert_ok(run_customer("followup-add", "--customer-id", "C001", "--content", "刚刚跟进"))
119
+
120
+ print("[1] product-add")
121
+ p1 = assert_ok(run_product("product-add", "--name", "XX重疾险2024",
122
+ "--company", "A保险公司", "--type", "重疾险",
123
+ "--coverage-range", "10万-100万",
124
+ "--premium-range", "3000-15000",
125
+ "--age-range", "0-55岁",
126
+ "--highlights", "覆盖120种重疾|轻症豁免|保额递增",
127
+ "--raw-text", "保120种重疾,等待期90天..."))
128
+ assert p1["id"]
129
+ assert_ok(run_product("product-add", "--name", "YY重疾险2024",
130
+ "--company", "B保险公司", "--type", "重疾险",
131
+ "--coverage-range", "30万-200万",
132
+ "--premium-range", "5000-20000",
133
+ "--age-range", "0-55岁",
134
+ "--highlights", "保额翻倍|覆盖180种重疾"))
135
+
136
+ print("[2] product-add: duplicate")
137
+ assert_err(run_product("product-add", "--name", "XX重疾险2024",
138
+ "--company", "A", "--type", "重疾险"), "duplicate_product")
139
+
140
+ print("[3] product-query")
141
+ d = assert_ok(run_product("product-query", "--type", "重疾险"))
142
+ assert d["count"] == 2
143
+
144
+ print("[4] product-update + cache invalidation")
145
+ assert_ok(run_product("product-update", "--name", "XX重疾险2024",
146
+ "--set", '{"highlights":["新亮点A","新亮点B"]}'))
147
+ d = assert_ok(run_product("product-query", "--name", "XX重疾险2024"))
148
+ assert "新亮点A" in d["products"][0]["highlights"]
149
+
150
+ print("[5] product-analyze (no cache → needs_llm)")
151
+ d = run_product("product-analyze", "--product", "XX重疾险2024")
152
+ assert d.get("error") == "needs_llm", d
153
+ assert "prompt_template" in d
154
+
155
+ print("[6] analysis-write + product-analyze (cache hit)")
156
+ assert_ok(run_product("analysis-write", "--product", "XX重疾险2024",
157
+ "--result", json.dumps({
158
+ "key_terms": ["保120种重疾", "等待期90天"],
159
+ "selling_points": ["保额递增", "轻症豁免", "费率低"],
160
+ "fit_audience": ["25-45岁", "年收入20w+"],
161
+ "pitch_script": "您已有医疗险..."
162
+ }, ensure_ascii=False)))
163
+ d = assert_ok(run_product("product-analyze", "--product", "XX重疾险2024"))
164
+ assert d["result"]["from_cache"] is True
165
+ assert "保额递增" in d["result"]["selling_points"]
166
+
167
+ print("[7] gap-analysis (forwarded)")
168
+ d = assert_ok(run_product("gap-analysis", "--customer-id", "C001"))
169
+ assert "重疾险" in d["result"]["gaps"]
170
+ d = assert_ok(run_product("gap-analysis", "--all", "--gap-type", "重疾险"))
171
+ assert d["count"] >= 1
172
+
173
+ print("[8] customer-match (product → customers)")
174
+ d = assert_ok(run_product("customer-match", "--product", "XX重疾险2024"))
175
+ assert any(m["matches_gap"] for m in d["matches"])
176
+ top = d["matches"][0]
177
+ assert top["score"] >= 100, top
178
+
179
+ print("[9] customer-match (customer → products)")
180
+ d = assert_ok(run_product("customer-match", "--customer-id", "C001"))
181
+ assert d["matches"], d
182
+ assert d["matches"][0]["matches_gap"] is True
183
+
184
+ print("[10] product-compare")
185
+ d = assert_ok(run_product("product-compare", "--product-a", "XX重疾险2024",
186
+ "--product-b", "YY重疾险2024"))
187
+ assert any(r["field"] == "公司" for r in d["compare"])
188
+
189
+ print("[11] dashboard")
190
+ d = assert_ok(run_product("dashboard", "--json"))
191
+ assert d["products_total"] == 2
192
+
193
+ print("[12] product-delete")
194
+ assert_err(run_product("product-delete", "--name", "YY重疾险2024"), "need_confirm")
195
+ assert_ok(run_product("product-delete", "--name", "YY重疾险2024", "--yes"))
196
+
197
+ print("ALL OK")
198
+ return 0
199
+
200
+
201
+ if __name__ == "__main__":
202
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from insurance_product_cli.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())