sophhub 0.4.21 → 0.4.22

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 (68) hide show
  1. package/package.json +1 -1
  2. package/skills/insurance-customer-policy/skill.json +12 -0
  3. package/skills/insurance-customer-policy/src/SKILL.md +121 -0
  4. package/skills/insurance-customer-policy/src/pyproject.toml +9 -0
  5. package/skills/insurance-customer-policy/src/scripts/cli.py +16 -0
  6. package/skills/insurance-customer-policy/src/scripts/cloud_insurance_full_test.py +785 -0
  7. package/skills/insurance-customer-policy/src/scripts/dashboard_all.py +205 -0
  8. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__init__.py +0 -0
  9. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/__main__.py +4 -0
  10. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/cli.py +816 -0
  11. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/db.py +181 -0
  12. package/skills/insurance-customer-policy/src/scripts/insurance_customer_cli/insurance_paths.py +184 -0
  13. package/skills/insurance-customer-policy/src/scripts/run_e2e_smoke.py +164 -0
  14. package/skills/insurance-customer-policy/src/scripts/test_cloud_zero_config.py +217 -0
  15. package/skills/insurance-customer-policy/src/scripts/test_dashboard_all.py +113 -0
  16. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__init__.py +0 -0
  17. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/__main__.py +4 -0
  18. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/cli.py +816 -0
  19. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/db.py +181 -0
  20. package/skills/insurance-customer-policy/src/src/insurance_customer_cli/insurance_paths.py +184 -0
  21. package/skills/insurance-product-analysis/skill.json +12 -0
  22. package/skills/insurance-product-analysis/src/SKILL.md +99 -0
  23. package/skills/insurance-product-analysis/src/pyproject.toml +9 -0
  24. package/skills/insurance-product-analysis/src/scripts/cli.py +16 -0
  25. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__init__.py +0 -0
  26. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/__main__.py +4 -0
  27. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/cli.py +545 -0
  28. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/db.py +180 -0
  29. package/skills/insurance-product-analysis/src/scripts/insurance_product_cli/orchestrator.py +163 -0
  30. package/skills/insurance-product-analysis/src/scripts/run_e2e_smoke.py +202 -0
  31. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__init__.py +0 -0
  32. package/skills/insurance-product-analysis/src/src/insurance_product_cli/__main__.py +4 -0
  33. package/skills/insurance-product-analysis/src/src/insurance_product_cli/cli.py +545 -0
  34. package/skills/insurance-product-analysis/src/src/insurance_product_cli/db.py +180 -0
  35. package/skills/insurance-product-analysis/src/src/insurance_product_cli/orchestrator.py +163 -0
  36. package/skills/insurance-sales-pipeline/skill.json +12 -0
  37. package/skills/insurance-sales-pipeline/src/SKILL.md +102 -0
  38. package/skills/insurance-sales-pipeline/src/pyproject.toml +9 -0
  39. package/skills/insurance-sales-pipeline/src/scripts/cli.py +16 -0
  40. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__init__.py +0 -0
  41. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/__main__.py +4 -0
  42. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/cli.py +496 -0
  43. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/db.py +180 -0
  44. package/skills/insurance-sales-pipeline/src/scripts/insurance_pipeline_cli/orchestrator.py +36 -0
  45. package/skills/insurance-sales-pipeline/src/scripts/run_e2e_smoke.py +208 -0
  46. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__init__.py +0 -0
  47. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/__main__.py +4 -0
  48. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/cli.py +496 -0
  49. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/db.py +180 -0
  50. package/skills/insurance-sales-pipeline/src/src/insurance_pipeline_cli/orchestrator.py +36 -0
  51. package/skills/insurance-schedule-renewal/skill.json +12 -0
  52. package/skills/insurance-schedule-renewal/src/SKILL.md +94 -0
  53. package/skills/insurance-schedule-renewal/src/pyproject.toml +9 -0
  54. package/skills/insurance-schedule-renewal/src/scripts/cli.py +16 -0
  55. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__init__.py +0 -0
  56. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/__main__.py +4 -0
  57. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/cli.py +429 -0
  58. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/db.py +180 -0
  59. package/skills/insurance-schedule-renewal/src/scripts/insurance_schedule_cli/orchestrator.py +94 -0
  60. package/skills/insurance-schedule-renewal/src/scripts/run_e2e_smoke.py +218 -0
  61. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__init__.py +0 -0
  62. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/__main__.py +4 -0
  63. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/cli.py +429 -0
  64. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/db.py +180 -0
  65. package/skills/insurance-schedule-renewal/src/src/insurance_schedule_cli/orchestrator.py +94 -0
  66. package/skills/insurance-shared-data/skill.json +20 -0
  67. package/skills/insurance-shared-data/src/SKILL.md +33 -0
  68. package/skills/insurance-shared-data/src/scripts/cloud_insurance_super_test.py +246 -0
@@ -0,0 +1,545 @@
1
+ """Insurance product analysis CLI."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sqlite3
7
+ import sys
8
+ from datetime import datetime, timedelta
9
+ from typing import Any
10
+
11
+ from insurance_product_cli import db, orchestrator
12
+
13
+ try:
14
+ sys.stdout.reconfigure(encoding="utf-8")
15
+ sys.stderr.reconfigure(encoding="utf-8")
16
+ except (AttributeError, OSError):
17
+ pass
18
+
19
+ CACHE_TTL_DAYS = 7
20
+
21
+
22
+ def print_json(obj: Any) -> None:
23
+ print(json.dumps(obj, ensure_ascii=False, indent=2))
24
+
25
+
26
+ # ---------- product CRUD ----------
27
+
28
+ def cmd_product_add(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
29
+ highlights = []
30
+ if args.highlights:
31
+ if "|" in args.highlights:
32
+ highlights = [s.strip() for s in args.highlights.split("|") if s.strip()]
33
+ else:
34
+ try:
35
+ highlights = json.loads(args.highlights)
36
+ except json.JSONDecodeError:
37
+ highlights = [args.highlights]
38
+ cov_min, cov_max = _parse_range(args.coverage_range)
39
+ pre_min, pre_max = _parse_range(args.premium_range)
40
+ age_min, age_max = _parse_age_range(args.age_range)
41
+ cur = conn.cursor()
42
+ try:
43
+ cur.execute(
44
+ """INSERT INTO products
45
+ (name, company, product_type, coverage_min, coverage_max,
46
+ premium_min, premium_max, age_min, age_max,
47
+ waiting_period, deductible, renewal_terms,
48
+ highlights_json, target_audience, file_path, raw_text)
49
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
50
+ (
51
+ args.name,
52
+ args.company,
53
+ args.product_type,
54
+ cov_min,
55
+ cov_max,
56
+ pre_min,
57
+ pre_max,
58
+ age_min,
59
+ age_max,
60
+ args.waiting_period or "",
61
+ args.deductible or "",
62
+ args.renewal_terms or "",
63
+ json.dumps(highlights, ensure_ascii=False),
64
+ args.target_audience or "",
65
+ args.file or "",
66
+ args.raw_text or "",
67
+ ),
68
+ )
69
+ except sqlite3.IntegrityError:
70
+ print_json({"ok": False, "error": "duplicate_product"})
71
+ return 1
72
+ conn.commit()
73
+ print_json({"ok": True, "id": cur.lastrowid, "name": args.name})
74
+ return 0
75
+
76
+
77
+ def _parse_range(s: str | None) -> tuple[float | None, float | None]:
78
+ if not s:
79
+ return None, None
80
+ s2 = s.replace("万", "*10000").replace("元", "").replace(" ", "")
81
+ if "-" not in s2:
82
+ return None, None
83
+ a, b = s2.split("-", 1)
84
+ try:
85
+ return float(eval(a)), float(eval(b)) # noqa: S307 - controlled input
86
+ except (ValueError, SyntaxError, NameError):
87
+ return None, None
88
+
89
+
90
+ def _parse_age_range(s: str | None) -> tuple[int | None, int | None]:
91
+ if not s:
92
+ return None, None
93
+ s2 = s.replace("岁", "").replace(" ", "")
94
+ if "-" not in s2:
95
+ return None, None
96
+ a, b = s2.split("-", 1)
97
+ try:
98
+ return int(a), int(b)
99
+ except ValueError:
100
+ return None, None
101
+
102
+
103
+ def _row_product(row: sqlite3.Row) -> dict:
104
+ d = dict(row)
105
+ if "highlights_json" in d and isinstance(d["highlights_json"], str):
106
+ try:
107
+ d["highlights"] = json.loads(d["highlights_json"])
108
+ except json.JSONDecodeError:
109
+ d["highlights"] = []
110
+ return d
111
+
112
+
113
+ def cmd_product_query(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
114
+ cur = conn.cursor()
115
+ rows: list[sqlite3.Row]
116
+ if args.name:
117
+ rows = list(cur.execute("SELECT * FROM products WHERE name=? AND status='active'", (args.name,)))
118
+ elif args.product_type:
119
+ rows = list(cur.execute("SELECT * FROM products WHERE product_type=? AND status='active' ORDER BY name", (args.product_type,)))
120
+ elif args.company:
121
+ rows = list(cur.execute("SELECT * FROM products WHERE company=? AND status='active' ORDER BY name", (args.company,)))
122
+ else:
123
+ rows = list(cur.execute("SELECT * FROM products WHERE status='active' ORDER BY name"))
124
+ print_json({"ok": True, "products": [_row_product(r) for r in rows], "count": len(rows)})
125
+ return 0
126
+
127
+
128
+ def cmd_product_update(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
129
+ try:
130
+ payload = json.loads(args.set)
131
+ except json.JSONDecodeError as e:
132
+ print_json({"ok": False, "error": "bad_json", "hint": str(e)})
133
+ return 1
134
+ row = conn.execute("SELECT id FROM products WHERE name=? AND status='active'", (args.name,)).fetchone()
135
+ if not row:
136
+ print_json({"ok": False, "error": "product_not_found"})
137
+ return 1
138
+ mapping = {
139
+ "name": "name",
140
+ "company": "company",
141
+ "product_type": "product_type",
142
+ "coverage_min": "coverage_min",
143
+ "coverage_max": "coverage_max",
144
+ "premium_min": "premium_min",
145
+ "premium_max": "premium_max",
146
+ "age_min": "age_min",
147
+ "age_max": "age_max",
148
+ "waiting_period": "waiting_period",
149
+ "deductible": "deductible",
150
+ "renewal_terms": "renewal_terms",
151
+ "target_audience": "target_audience",
152
+ "file_path": "file_path",
153
+ "raw_text": "raw_text",
154
+ }
155
+ sets, vals = [], []
156
+ for k, v in payload.items():
157
+ col = mapping.get(k)
158
+ if col is not None:
159
+ sets.append(f"{col}=?")
160
+ vals.append(v)
161
+ elif k == "highlights":
162
+ sets.append("highlights_json=?")
163
+ vals.append(json.dumps(v, ensure_ascii=False))
164
+ if sets:
165
+ sets.append("updated_at=datetime('now','localtime')")
166
+ vals.append(row["id"])
167
+ conn.execute(f"UPDATE products SET {', '.join(sets)} WHERE id=?", vals)
168
+ conn.commit()
169
+ # invalidate cache
170
+ conn.execute("DELETE FROM product_analysis_cache WHERE product_id=?", (row["id"],))
171
+ conn.commit()
172
+ print_json({"ok": True, "id": row["id"]})
173
+ return 0
174
+
175
+
176
+ def cmd_product_delete(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
177
+ row = conn.execute("SELECT id FROM products WHERE name=? AND status='active'", (args.name,)).fetchone()
178
+ if not row:
179
+ print_json({"ok": False, "error": "product_not_found"})
180
+ return 1
181
+ if not args.yes:
182
+ print_json({"ok": False, "error": "need_confirm", "hint": "请加 --yes 确认删除"})
183
+ return 1
184
+ conn.execute(
185
+ "UPDATE products SET status='deleted', updated_at=datetime('now','localtime') WHERE id=?",
186
+ (row["id"],),
187
+ )
188
+ conn.commit()
189
+ print_json({"ok": True})
190
+ return 0
191
+
192
+
193
+ # ---------- product-analyze ----------
194
+
195
+ def cmd_product_analyze(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
196
+ row = conn.execute(
197
+ "SELECT * FROM products WHERE name=? AND status='active'", (args.product,)
198
+ ).fetchone()
199
+ if not row:
200
+ print_json({"ok": False, "error": "product_not_found"})
201
+ return 1
202
+ pid = row["id"]
203
+ cache = conn.execute(
204
+ "SELECT * FROM product_analysis_cache WHERE product_id=?", (pid,)
205
+ ).fetchone()
206
+ if cache and not args.refresh:
207
+ try:
208
+ analyzed = datetime.strptime(cache["analyzed_at"][:19], "%Y-%m-%d %H:%M:%S")
209
+ fresh = (datetime.now() - analyzed) < timedelta(days=CACHE_TTL_DAYS)
210
+ except (ValueError, TypeError):
211
+ fresh = False
212
+ if fresh:
213
+ result = {
214
+ "key_terms": json.loads(cache["key_terms_json"] or "[]"),
215
+ "selling_points": json.loads(cache["selling_points_json"] or "[]"),
216
+ "fit_audience": json.loads(cache["fit_audience_json"] or "[]"),
217
+ "pitch_script": cache["pitch_script"] or "",
218
+ "analyzed_at": cache["analyzed_at"],
219
+ "from_cache": True,
220
+ }
221
+ payload: dict[str, Any] = {"ok": True, "product": _row_product(row), "result": result}
222
+ if args.match_customers:
223
+ payload["customer_matches"] = _match_customers_for_product(_row_product(row))
224
+ print_json(payload)
225
+ return 0
226
+ # No fresh cache → return needs_llm hook (script does not call LLM directly)
227
+ print_json(
228
+ {
229
+ "ok": False,
230
+ "error": "needs_llm",
231
+ "hint": "解读未命中缓存,请由上层 agent 用 LLM 填空后调 analysis-write 写回。",
232
+ "product": _row_product(row),
233
+ "raw_text": row["raw_text"] or "",
234
+ "prompt_template": _analyze_prompt_template(_row_product(row)),
235
+ }
236
+ )
237
+ return 1
238
+
239
+
240
+ def _analyze_prompt_template(product: dict) -> str:
241
+ return (
242
+ "请把下列保险产品资料解读为 4 段:\n"
243
+ "1) 条款要点(保什么/不保什么/等待期/免赔额/续保条件)\n"
244
+ "2) 核心卖点(3-5 个,相对同类产品的优势)\n"
245
+ "3) 适用人群(年龄/收入/家庭结构/已有保单维度)\n"
246
+ "4) 话术建议(怎么向客户开口,避免理赔承诺/夸大)\n\n"
247
+ f"产品名:{product.get('name')}\n"
248
+ f"公司:{product.get('company')}\n"
249
+ f"类型:{product.get('product_type')}\n"
250
+ f"亮点:{product.get('highlights')}\n"
251
+ f"原始资料:{product.get('raw_text', '')}\n"
252
+ "请返回 JSON:{key_terms:[...], selling_points:[...], fit_audience:[...], pitch_script:'...'}"
253
+ )
254
+
255
+
256
+ def cmd_analysis_write(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
257
+ row = conn.execute("SELECT id FROM products WHERE name=? AND status='active'", (args.product,)).fetchone()
258
+ if not row:
259
+ print_json({"ok": False, "error": "product_not_found"})
260
+ return 1
261
+ pid = row["id"]
262
+ try:
263
+ if args.result_file:
264
+ with open(args.result_file, encoding="utf-8") as f:
265
+ result = json.load(f)
266
+ else:
267
+ result = json.loads(args.result)
268
+ except (json.JSONDecodeError, OSError) as e:
269
+ print_json({"ok": False, "error": "bad_json", "hint": str(e)})
270
+ return 1
271
+ conn.execute(
272
+ """INSERT OR REPLACE INTO product_analysis_cache
273
+ (product_id, key_terms_json, selling_points_json, fit_audience_json, pitch_script, analyzed_at)
274
+ VALUES (?,?,?,?,?, datetime('now','localtime'))""",
275
+ (
276
+ pid,
277
+ json.dumps(result.get("key_terms") or [], ensure_ascii=False),
278
+ json.dumps(result.get("selling_points") or [], ensure_ascii=False),
279
+ json.dumps(result.get("fit_audience") or [], ensure_ascii=False),
280
+ result.get("pitch_script") or "",
281
+ ),
282
+ )
283
+ conn.commit()
284
+ print_json({"ok": True, "product_id": pid})
285
+ return 0
286
+
287
+
288
+ # ---------- gap-analysis (forward to customer-policy) ----------
289
+
290
+ def cmd_gap_analysis(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
291
+ data = orchestrator.gap_analysis(
292
+ customer_id=args.customer_id,
293
+ gap_type=args.gap_type,
294
+ all_customers=bool(args.all),
295
+ )
296
+ print_json(data)
297
+ return 0 if data.get("ok") else 1
298
+
299
+
300
+ # ---------- customer-match ----------
301
+
302
+ def _match_customers_for_product(product: dict) -> list[dict]:
303
+ seg = orchestrator.customer_segment()
304
+ if not seg.get("ok"):
305
+ return []
306
+ candidates: list[dict] = []
307
+ seen: set[str] = set()
308
+ for bucket in ("hi_value", "active", "new", "dormant"):
309
+ for c in seg.get(bucket) or []:
310
+ cid = c.get("customer_id")
311
+ if not cid or cid in seen:
312
+ continue
313
+ seen.add(cid)
314
+ cust_data = orchestrator.customer_query(customer_id=cid)
315
+ cust = (cust_data.get("customers") or [{}])[0]
316
+ age = cust.get("age")
317
+ income = cust.get("income")
318
+ if product.get("age_min") is not None and age is not None and age < product["age_min"]:
319
+ continue
320
+ if product.get("age_max") is not None and age is not None and age > product["age_max"]:
321
+ continue
322
+ gap = orchestrator.gap_analysis(customer_id=cid)
323
+ gaps = (gap.get("result") or {}).get("gaps") or []
324
+ score = 0
325
+ if product.get("product_type") in gaps:
326
+ score += 100
327
+ if bucket == "hi_value":
328
+ score += 50
329
+ if bucket == "active":
330
+ score += 20
331
+ if bucket == "new":
332
+ score += 10
333
+ if income and product.get("premium_max") and income > 0:
334
+ if product["premium_max"] / income < 0.2:
335
+ score += 5
336
+ candidates.append({
337
+ "customer_id": cid,
338
+ "name": c.get("name"),
339
+ "age": age,
340
+ "income": income,
341
+ "bucket": bucket,
342
+ "matches_gap": product.get("product_type") in gaps,
343
+ "score": score,
344
+ })
345
+ candidates.sort(key=lambda r: r["score"], reverse=True)
346
+ return candidates
347
+
348
+
349
+ def _match_products_for_customer(conn: sqlite3.Connection, customer_id: str) -> list[dict]:
350
+ cust = orchestrator.customer_query(customer_id=customer_id)
351
+ rows = cust.get("customers") or []
352
+ if not rows:
353
+ return []
354
+ cust_row = rows[0]
355
+ age = cust_row.get("age")
356
+ income = cust_row.get("income")
357
+ gap = orchestrator.gap_analysis(customer_id=customer_id)
358
+ gaps = (gap.get("result") or {}).get("gaps") or []
359
+ cur = conn.cursor()
360
+ products = list(cur.execute("SELECT * FROM products WHERE status='active'"))
361
+ matches: list[dict] = []
362
+ for p in products:
363
+ prod = _row_product(p)
364
+ if prod.get("age_min") is not None and age is not None and age < prod["age_min"]:
365
+ continue
366
+ if prod.get("age_max") is not None and age is not None and age > prod["age_max"]:
367
+ continue
368
+ score = 0
369
+ if prod.get("product_type") in gaps:
370
+ score += 100
371
+ if income and prod.get("premium_max") and prod["premium_max"] / income < 0.2:
372
+ score += 10
373
+ matches.append({
374
+ "product_id": prod["id"],
375
+ "name": prod["name"],
376
+ "company": prod["company"],
377
+ "product_type": prod["product_type"],
378
+ "matches_gap": prod.get("product_type") in gaps,
379
+ "score": score,
380
+ })
381
+ matches.sort(key=lambda r: r["score"], reverse=True)
382
+ return matches
383
+
384
+
385
+ def cmd_customer_match(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
386
+ if args.product:
387
+ row = conn.execute(
388
+ "SELECT * FROM products WHERE name=? AND status='active'", (args.product,)
389
+ ).fetchone()
390
+ if not row:
391
+ print_json({"ok": False, "error": "product_not_found"})
392
+ return 1
393
+ product = _row_product(row)
394
+ print_json({"ok": True, "product": product["name"], "matches": _match_customers_for_product(product)})
395
+ return 0
396
+ if args.customer_id:
397
+ print_json(
398
+ {
399
+ "ok": True,
400
+ "customer_id": args.customer_id,
401
+ "matches": _match_products_for_customer(conn, args.customer_id),
402
+ }
403
+ )
404
+ return 0
405
+ print_json({"ok": False, "error": "need_product_or_customer"})
406
+ return 1
407
+
408
+
409
+ # ---------- product-compare ----------
410
+
411
+ COMPARE_FIELDS = [
412
+ ("company", "公司"),
413
+ ("product_type", "产品类型"),
414
+ ("coverage_min", "保额下限"),
415
+ ("coverage_max", "保额上限"),
416
+ ("premium_min", "保费下限"),
417
+ ("premium_max", "保费上限"),
418
+ ("age_min", "投保年龄下限"),
419
+ ("age_max", "投保年龄上限"),
420
+ ("waiting_period", "等待期"),
421
+ ("deductible", "免赔额"),
422
+ ("renewal_terms", "续保条件"),
423
+ ]
424
+
425
+
426
+ def cmd_product_compare(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
427
+ cur = conn.cursor()
428
+ a = cur.execute("SELECT * FROM products WHERE name=? AND status='active'", (args.product_a,)).fetchone()
429
+ b = cur.execute("SELECT * FROM products WHERE name=? AND status='active'", (args.product_b,)).fetchone()
430
+ if not a or not b:
431
+ print_json({"ok": False, "error": "product_not_found",
432
+ "missing": [n for n, r in [(args.product_a, a), (args.product_b, b)] if not r]})
433
+ return 1
434
+ pa = _row_product(a)
435
+ pb = _row_product(b)
436
+ rows = []
437
+ for field, label in COMPARE_FIELDS:
438
+ rows.append({"field": label, "a": pa.get(field), "b": pb.get(field)})
439
+ rows.append({"field": "亮点", "a": pa.get("highlights"), "b": pb.get("highlights")})
440
+ print_json({"ok": True, "a": pa["name"], "b": pb["name"], "compare": rows})
441
+ return 0
442
+
443
+
444
+ # ---------- dashboard ----------
445
+
446
+ def cmd_dashboard(conn: sqlite3.Connection, args: argparse.Namespace) -> int:
447
+ n = int(conn.execute("SELECT COUNT(*) AS n FROM products WHERE status='active'").fetchone()["n"])
448
+ types = [
449
+ dict(r)
450
+ for r in conn.execute(
451
+ "SELECT product_type, COUNT(*) AS n FROM products WHERE status='active' GROUP BY product_type ORDER BY n DESC"
452
+ )
453
+ ]
454
+ print_json({"ok": True, "skill": "insurance-product-analysis", "products_total": n, "types": types})
455
+ return 0
456
+
457
+
458
+ # ---------- argparse ----------
459
+
460
+ def build_parser() -> argparse.ArgumentParser:
461
+ p = argparse.ArgumentParser(prog="insurance_product_cli")
462
+ sub = p.add_subparsers(dest="cmd", required=True)
463
+
464
+ a = sub.add_parser("product-add")
465
+ a.add_argument("--name", required=True)
466
+ a.add_argument("--company", required=True)
467
+ a.add_argument("--product-type", "--type", dest="product_type", required=True)
468
+ a.add_argument("--coverage-range")
469
+ a.add_argument("--premium-range")
470
+ a.add_argument("--age-range")
471
+ a.add_argument("--waiting-period")
472
+ a.add_argument("--deductible")
473
+ a.add_argument("--renewal-terms")
474
+ a.add_argument("--highlights")
475
+ a.add_argument("--target-audience")
476
+ a.add_argument("--file")
477
+ a.add_argument("--raw-text")
478
+ a.set_defaults(func=cmd_product_add)
479
+
480
+ a = sub.add_parser("product-query")
481
+ a.add_argument("--name")
482
+ a.add_argument("--product-type", "--type", dest="product_type")
483
+ a.add_argument("--company")
484
+ a.set_defaults(func=cmd_product_query)
485
+
486
+ a = sub.add_parser("product-update")
487
+ a.add_argument("--name", required=True)
488
+ a.add_argument("--set", required=True)
489
+ a.set_defaults(func=cmd_product_update)
490
+
491
+ a = sub.add_parser("product-delete")
492
+ a.add_argument("--name", required=True)
493
+ a.add_argument("--yes", action="store_true")
494
+ a.set_defaults(func=cmd_product_delete)
495
+
496
+ a = sub.add_parser("product-analyze")
497
+ a.add_argument("--product", required=True)
498
+ a.add_argument("--match-customers", action="store_true")
499
+ a.add_argument("--refresh", action="store_true")
500
+ a.set_defaults(func=cmd_product_analyze)
501
+
502
+ a = sub.add_parser("analysis-write")
503
+ a.add_argument("--product", required=True)
504
+ a.add_argument("--result", help="JSON 字符串")
505
+ a.add_argument("--result-file", help="包含分析结果 JSON 的文件路径")
506
+ a.set_defaults(func=cmd_analysis_write)
507
+
508
+ a = sub.add_parser("gap-analysis")
509
+ a.add_argument("--customer-id")
510
+ a.add_argument("--all", action="store_true")
511
+ a.add_argument("--gap-type")
512
+ a.set_defaults(func=cmd_gap_analysis)
513
+
514
+ a = sub.add_parser("customer-match")
515
+ a.add_argument("--product")
516
+ a.add_argument("--customer-id")
517
+ a.set_defaults(func=cmd_customer_match)
518
+
519
+ a = sub.add_parser("product-compare")
520
+ a.add_argument("--product-a", required=True)
521
+ a.add_argument("--product-b", required=True)
522
+ a.set_defaults(func=cmd_product_compare)
523
+
524
+ a = sub.add_parser("dashboard")
525
+ a.add_argument("--json", action="store_true")
526
+ a.set_defaults(func=cmd_dashboard)
527
+
528
+ return p
529
+
530
+
531
+ def main(argv: list[str] | None = None) -> int:
532
+ parser = build_parser()
533
+ args = parser.parse_args(argv)
534
+ conn = db.connect()
535
+ try:
536
+ return int(args.func(conn, args) or 0)
537
+ except sqlite3.Error as e:
538
+ print_json({"ok": False, "error": "sqlite_error", "hint": str(e)})
539
+ return 1
540
+ finally:
541
+ conn.close()
542
+
543
+
544
+ if __name__ == "__main__":
545
+ raise SystemExit(main())