portrait_generator.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. from __future__ import annotations
  2. import math
  3. import re
  4. from typing import Any
  5. def _clamp(v: float, lo: float = 0, hi: float = 100) -> int:
  6. return int(max(lo, min(hi, v)))
  7. def _has(v: Any) -> bool:
  8. if v is None:
  9. return False
  10. if isinstance(v, str):
  11. return bool(v.strip())
  12. if isinstance(v, (list, dict)):
  13. return len(v) > 0
  14. return True
  15. def _keyword_tags(elements: dict[str, Any]) -> list[dict[str, Any]]:
  16. """
  17. 多级标签体系(MVP):一级维度 + 二级标签 + 具体值
  18. """
  19. cause = elements.get("case_cause")
  20. tags: list[dict[str, Any]] = []
  21. if cause:
  22. tags.append(
  23. {
  24. "level1": "法律维度",
  25. "level2": "争议焦点类型",
  26. "value": str(cause),
  27. }
  28. )
  29. if elements.get("overtime_desc"):
  30. tags.append({"level1": "事实维度", "level2": "关键事实", "value": "加班事实"})
  31. if elements.get("termination_reason"):
  32. tags.append({"level1": "事实维度", "level2": "解除情形", "value": "已识别解除原因"})
  33. return tags
  34. def _extract_keywords(text: str, top_k: int = 15) -> list[dict[str, Any]]:
  35. """
  36. 用 jieba 分词提取高频关键词,返回连贯的中文词语。
  37. """
  38. if not text:
  39. return []
  40. try:
  41. import jieba
  42. # 添加法律领域自定义词,避免复合词被切分
  43. for w in ["劳动合同", "经济补偿", "赔偿金", "拖欠工资", "违法解除",
  44. "加班费", "工伤认定", "社会保险", "住房公积金",
  45. "解除劳动合同", "双倍工资", "违法辞退", "劳动仲裁",
  46. "科技有限公司", "有限公司", "劳动争议", "仲裁委员会",
  47. "年终奖", "二倍工资"]:
  48. jieba.add_word(w)
  49. words = jieba.cut(text[:12000])
  50. toks = [w.strip() for w in words if len(w.strip()) >= 2]
  51. except ImportError:
  52. toks = re.findall(r"[一-鿿]{2,6}|[A-Za-z]{3,}", text[:12000])
  53. stop = {
  54. "申请人", "被申请人", "仲裁", "请求", "事实", "理由", "事项",
  55. "劳动", "争议", "委员会", "申请", "本案", "仲裁庭",
  56. "经审理", "查明", "如下", "予以", "认定",
  57. "支持", "驳回", "维持", "的", "了", "在", "和",
  58. "是", "不", "与", "及", "年", "月", "日", "元",
  59. "支付", "公司", "人民", "法院", "原告", "被告",
  60. }
  61. freq: dict[str, int] = {}
  62. for t in toks:
  63. if t in stop or len(t) <= 1:
  64. continue
  65. freq[t] = freq.get(t, 0) + 1
  66. items = sorted(freq.items(), key=lambda x: x[1], reverse=True)[:top_k]
  67. return [{"name": k, "value": v} for k, v in items]
  68. def generate_portrait(elements: dict[str, Any], raw_text: str = "", evidence_count: int = 0) -> dict[str, Any]:
  69. laws = elements.get("law_refs") or []
  70. claims = elements.get("claims") or {}
  71. claim_items = claims.get("items") or []
  72. # 法律维度:争议焦点明确度 / 法条引用准确性(用“有无+数量”粗略替代) / 证据法律效力(用 evidence_count 近似)
  73. dispute_focus = 60 + (20 if _has(elements.get("case_cause")) else 0) + (10 if len(claim_items) >= 2 else 0)
  74. law_ref_score = 40 + min(40, 10 * len(laws))
  75. evidence_legal_power = 30 + min(50, evidence_count * 10)
  76. legal_score = _clamp(dispute_focus * 0.4 + law_ref_score * 0.35 + evidence_legal_power * 0.25)
  77. # 事实维度:时间线完整性 / 事实清晰度 / 证据完备度
  78. timeline_complete = 30 + (35 if _has(elements.get("entry_date")) else 0) + (35 if _has(elements.get("leave_date")) else 0)
  79. fact_clear = 40 + (25 if _has(elements.get("termination_reason")) else 0) + (20 if _has(elements.get("overtime_desc")) else 0)
  80. evidence_complete = 20 + min(60, evidence_count * 12)
  81. fact_score = _clamp(timeline_complete * 0.35 + fact_clear * 0.4 + evidence_complete * 0.25)
  82. # 风险维度:诉求支持可能性 / 矛盾激化程度
  83. amount_total = claims.get("amount_total")
  84. month_salary = elements.get("month_salary") or 0
  85. support_prob = 60
  86. if amount_total and month_salary:
  87. ratio = amount_total / max(month_salary, 1)
  88. if ratio > 12:
  89. support_prob -= 20
  90. elif ratio > 6:
  91. support_prob -= 10
  92. else:
  93. support_prob += 5
  94. if elements.get("case_cause") == "违法解除劳动合同" and not _has(elements.get("termination_reason")):
  95. support_prob -= 10
  96. support_prob = max(5, min(95, support_prob))
  97. escalation = 40 + min(40, len(claim_items) * 8) + (10 if (amount_total or 0) > 50000 else 0)
  98. risk_score = _clamp((100 - support_prob) * 0.6 + escalation * 0.4)
  99. # KPI:风险等级(给前端标签)
  100. risk_level = "低"
  101. if risk_score >= 70:
  102. risk_level = "高"
  103. elif risk_score >= 45:
  104. risk_level = "中"
  105. portrait = {
  106. "scores": {
  107. "legal": legal_score,
  108. "fact": fact_score,
  109. "risk": risk_score,
  110. },
  111. "legal_dimension": {
  112. "score": legal_score,
  113. "subscores": {
  114. "争议焦点明确度": _clamp(dispute_focus),
  115. "法律条款引用充分度": _clamp(law_ref_score),
  116. "证据法律效力(近似)": _clamp(evidence_legal_power),
  117. },
  118. },
  119. "fact_dimension": {
  120. "score": fact_score,
  121. "subscores": {
  122. "时间线完整性": _clamp(timeline_complete),
  123. "事实描述清晰度": _clamp(fact_clear),
  124. "证据完备度(近似)": _clamp(evidence_complete),
  125. },
  126. },
  127. "risk_dimension": {
  128. "score": risk_score,
  129. "subscores": {
  130. "诉求支持可能性(规则近似)": _clamp(support_prob),
  131. "矛盾激化程度(规则近似)": _clamp(escalation),
  132. },
  133. },
  134. "tags": _keyword_tags(elements),
  135. "keywords": _extract_keywords(raw_text),
  136. "risk_level": risk_level,
  137. }
  138. return portrait