| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795 |
- from __future__ import annotations
- import json
- import re
- from dataclasses import dataclass
- from datetime import date
- from pathlib import Path
- from typing import Any, Callable
- from app.hierarchy_extract import (
- DEFAULT_CAUSE_TYPE,
- build_elements_hierarchy_for_cause,
- load_hierarchy_templates,
- )
- _WS = r"[ \t\u3000]*"
- def _clean_text(text: str) -> str:
- if not text:
- return ""
- text = text.replace("\r\n", "\n").replace("\r", "\n")
- text = re.sub(r"[ \t\u3000]+", " ", text)
- return text
- def _first_group(pattern: str, text: str, flags: int = 0) -> str | None:
- m = re.search(pattern, text, flags)
- if not m:
- return None
- for i in range(1, (m.lastindex or 0) + 1):
- g = m.group(i)
- if g is not None and str(g).strip():
- return str(g).strip()
- return None
- def _all_matches(pattern: str, text: str, flags: int = 0, group: int = 0) -> list[str]:
- out: list[str] = []
- for m in re.finditer(pattern, text, flags):
- out.append((m.group(group) if group else m.group(0)).strip())
- return out
- _CN_NUM = {
- "零": 0,
- "〇": 0,
- "一": 1,
- "二": 2,
- "两": 2,
- "三": 3,
- "四": 4,
- "五": 5,
- "六": 6,
- "七": 7,
- "八": 8,
- "九": 9,
- }
- _CN_UNIT = {"十": 10, "百": 100, "千": 1000, "万": 10000, "亿": 100000000}
- def parse_cn_number(s: str) -> float | None:
- """
- 解析常见中文金额(如:八千元、二万三千、十二万)为数字。
- 只覆盖毕业设计 MVP 常见写法;不处理复杂小数表达。
- """
- if not s:
- return None
- s = s.strip()
- s = re.sub(r"[元整人民币¥\s]", "", s)
- if not s:
- return None
- # 纯中文数字/单位
- total = 0
- section = 0
- number = 0
- has_any = False
- for ch in s:
- if ch in _CN_NUM:
- number = _CN_NUM[ch]
- has_any = True
- elif ch in _CN_UNIT:
- unit = _CN_UNIT[ch]
- has_any = True
- if unit >= 10000:
- section = (section + (number or 0)) * unit
- total += section
- section = 0
- else:
- section += (number or 1) * unit
- number = 0
- else:
- return None
- return float(total + section + number) if has_any else None
- def parse_amount(text: str) -> float | None:
- """
- 支持:8000元、8,000元、¥8000、八千元 等
- """
- if not text:
- return None
- text = text.strip()
- # 数字 + 元
- m = re.search(r"(?:¥|人民币)?\s*([0-9]{1,3}(?:,[0-9]{3})*|[0-9]+)(?:\.[0-9]{1,2})?\s*元", text)
- if m:
- raw = m.group(1).replace(",", "")
- try:
- return float(raw)
- except ValueError:
- pass
- # 中文金额
- m2 = re.search(r"([零〇一二两三四五六七八九十百千万亿]+)\s*元", text)
- if m2:
- return parse_cn_number(m2.group(1))
- return None
- def parse_date_any(s: str) -> str | None:
- """
- 统一解析日期格式并输出 YYYY-MM-DD(字符串)。
- 支持:
- - 2020年1月1日
- - 2020.01.01
- - 2020-01-01
- - 2020/1/1
- """
- if not s:
- return None
- s = s.strip()
- m = re.search(r"([12][0-9]{3})\s*[年./-]\s*([01]?[0-9])\s*[月./-]\s*([0-3]?[0-9])\s*(?:日)?", s)
- if not m:
- return None
- y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
- try:
- _ = date(y, mo, d)
- except ValueError:
- return None
- return f"{y:04d}-{mo:02d}-{d:02d}"
- CAUSE_KEYWORDS: list[tuple[str, list[str]]] = [
- ("工资报酬", ["工资", "拖欠工资", "未支付工资", "工资差额", "薪资", "报酬"]),
- ("经济补偿", ["经济补偿", "补偿金", "N+1", "补偿"]),
- ("工伤待遇", ["工伤", "工伤待遇", "伤残", "一次性伤残", "医疗费", "停工留薪"]),
- ("违法解除劳动合同", ["违法解除", "非法解除", "无故解除", "违法辞退", "解除劳动合同赔偿"]),
- ("加班费", ["加班费", "加班工资", "延时加班", "休息日加班", "法定节假日加班", "加班"]),
- ("未订立书面劳动合同", ["未签订劳动合同", "未订立书面劳动合同", "双倍工资", "二倍工资"]),
- ("年休假", ["年休假", "未休年假", "带薪年休假"]),
- ("社会保险", ["社会保险", "社保", "养老保险", "医疗保险", "失业保险", "未缴纳社保"]),
- ]
- _SCHEMA_PATH = Path(__file__).resolve().parent.parent / "data" / "case_elements_schema.json"
- def load_case_elements_schema() -> dict[str, Any]:
- if not _SCHEMA_PATH.is_file():
- return {"groups": [], "field_labels": {}, "table_name": "", "version": ""}
- try:
- return json.loads(_SCHEMA_PATH.read_text(encoding="utf-8"))
- except Exception:
- return {"groups": [], "field_labels": {}, "table_name": "", "version": ""}
- def _table_rows_for_ids(flat: dict[str, Any], labels: dict[str, str], field_ids: list[str], covered: set[str]) -> list[dict[str, Any]]:
- rows: list[dict[str, Any]] = []
- for fid in field_ids or []:
- covered.add(fid)
- rows.append(
- {
- "field_id": fid,
- "field_label": labels.get(fid, fid),
- "value": flat.get(fid),
- }
- )
- return rows
- def _build_table_group_node(flat: dict[str, Any], labels: dict[str, str], g: dict[str, Any], covered: set[str]) -> dict[str, Any]:
- """递归:支持 sub_groups(对应要素分解模板的一层/二层/三层结构)。"""
- gid = g.get("id", "")
- node: dict[str, Any] = {
- "group_id": gid,
- "group_label": g.get("label", gid),
- "rows": _table_rows_for_ids(flat, labels, g.get("field_ids") or [], covered),
- "sub_groups": [],
- }
- for sg in g.get("sub_groups") or []:
- node["sub_groups"].append(_build_table_group_node(flat, labels, sg, covered))
- return node
- def _count_table_rows(nodes: list[dict[str, Any]]) -> int:
- n = 0
- for node in nodes:
- n += len(node.get("rows") or [])
- n += _count_table_rows(node.get("sub_groups") or [])
- return n
- def build_case_elements_table(flat: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
- """
- 按 case_elements_schema.json 生成分组表;支持 sub_groups 嵌套。
- schema 中每个 field_id 对应一行;抽取结果里存在但未声明的顶层字段追加「补充要素」。
- """
- labels = schema.get("field_labels") or {}
- covered: set[str] = set()
- groups_out: list[dict[str, Any]] = [_build_table_group_node(flat, labels, g, covered) for g in schema.get("groups") or []]
- skip_keys = {"case_elements_table"}
- extra_ids = sorted(k for k in flat.keys() if k not in skip_keys and k not in covered)
- if extra_ids:
- groups_out.append(
- {
- "group_id": "supplement",
- "group_label": "补充要素",
- "rows": [
- {
- "field_id": fid,
- "field_label": labels.get(fid, fid),
- "value": flat.get(fid),
- }
- for fid in extra_ids
- ],
- "sub_groups": [],
- }
- )
- return {
- "table_name": schema.get("table_name", "案件要素表"),
- "version": schema.get("version", ""),
- "groups": groups_out,
- "field_count": _count_table_rows(groups_out),
- }
- def detect_case_cause(text: str) -> str | None:
- t = text or ""
- for cause, keys in CAUSE_KEYWORDS:
- if any(k in t for k in keys):
- return cause
- return None
- def extract_law_refs(text: str) -> list[str]:
- t = text or ""
- # 《劳动合同法》第xx条 / 《xx法》 / 劳动合同法第xx条
- refs = set()
- for item in _all_matches(r"《[^》]{2,30}》\s*第\s*[0-9一二三四五六七八九十百千]+\s*条", t):
- refs.add(item)
- for item in _all_matches(r"《[^》]{2,30}》", t):
- refs.add(item)
- for item in _all_matches(r"(劳动合同法|劳动争议调解仲裁法|工伤保险条例)\s*第\s*[0-9一二三四五六七八九十百千]+\s*条", t):
- refs.add(item)
- return sorted(refs)
- def extract_case_number(text: str) -> str | None:
- t = text or ""
- v = _first_group(r"(?:案号|案件编号|仲裁案号)[::]\s*([^\n,,。;;]{4,40})", t)
- if v:
- return v
- m = re.search(r"劳人仲案字\s*\[[0-9]{4}\]\s*第\s*[0-9一二三四五六七八九十百千]+\s*号", t)
- if m:
- return m.group(0).strip()
- m2 = re.search(r"\([12][0-9]{3}\)\s*第\s*[0-9]+\s*号", t)
- return m2.group(0).strip() if m2 else None
- def extract_filing_date(text: str) -> str | None:
- t = text or ""
- raw = _first_group(r"(?:立案日期|立案时间)[::]\s*([^\n,,。;;]{4,24})", t)
- return parse_date_any(raw) if raw else None
- def extract_arbitration_org(text: str) -> str | None:
- t = text or ""
- direct = _first_group(r"(?:仲裁机构|仲裁委员会)[::]\s*([^\n,,。;;]{4,60})", t)
- if direct:
- return direct
- m = re.search(r"([\u4e00-\u9fff]{2,20}劳动(?:人事)?争议仲裁委员会)", t)
- return m.group(1).strip() if m else None
- def extract_case_title(text: str) -> str | None:
- t = text or ""
- v = _first_group(r"(?:案件名称|标题)[::]\s*([^\n]{2,80})", t)
- if v:
- return v.strip()
- return _first_group(r"案由[::]\s*([^\n,,。;;]{2,40})", t)
- def extract_applicant_name(text: str) -> str | None:
- t = text or ""
- return _first_group(r"(?:申请人|申请方|申请者)[::]" + _WS + r"([^\n,,。;;]{2,20})", t)
- def extract_applicant_type(text: str) -> str | None:
- t = text or ""
- if re.search(r"申请人[::].{0,30}(公司|有限|集团|厂|中心|委员会)", t):
- return "用人单位(或用工单位)"
- if re.search(r"申请人类型[::]\s*([^\n,,]{2,20})", t):
- return _first_group(r"申请人类型[::]\s*([^\n,,]{2,20})", t)
- name = extract_applicant_name(t) or ""
- if len(name) <= 4 and not any(x in name for x in ["公司", "有限", "厂"]):
- return "自然人"
- return None
- def extract_employment_type(text: str) -> str | None:
- t = text or ""
- for label, keys in [
- ("劳务派遣", ["劳务派遣", "派遣员工", "用工单位", "派遣单位"]),
- ("非全日制用工", ["非全日制"]),
- ("全日制用工", ["全日制", "标准工时"]),
- ("劳务关系", ["劳务关系", "劳务协议"]),
- ]:
- if any(k in t for k in keys):
- return label
- return None
- def extract_respondent_name(text: str) -> str | None:
- t = text or ""
- return _first_group(r"(?:被申请人|被申请方|答辩人)[::]" + _WS + r"([^\n,,。;;]{2,40})", t)
- def extract_employer_nature(text: str) -> str | None:
- t = text or ""
- # 优先从“单位性质”字段取
- direct = _first_group(r"(?:单位性质|用人单位性质)[::]" + _WS + r"(企业|事业单位|个人|个体工商户|民办非企业|机关)", t)
- if direct:
- if direct in ("个体工商户",):
- return "个人"
- return direct
- # 根据名称后缀猜测
- resp = extract_respondent_name(t) or ""
- if any(k in resp for k in ["有限公司", "有限责任公司", "股份有限公司", "公司", "厂", "集团", "企业"]):
- return "企业"
- if any(k in resp for k in ["医院", "学校", "研究院", "事业单位", "中心", "局", "委员会"]):
- return "事业单位"
- return None
- def extract_worker_position(text: str) -> str | None:
- t = text or ""
- return _first_group(r"(?:岗位|职位|工种)[::]" + _WS + r"([^\n,,。;;]{2,20})", t)
- def extract_entry_date(text: str) -> str | None:
- t = text or ""
- raw = _first_group(r"(?:入职时间|入职日期|到岗时间|参加工作时间)[::]?\s*([^\n,,。;;]{4,20})", t)
- if raw:
- return parse_date_any(raw)
- raw2 = _first_group(r"(?:于|在)\s*([12][0-9]{3}[年./-][01]?[0-9][月./-][0-3]?[0-9])\s*(?:入职|到岗|开始工作)", t)
- return parse_date_any(raw2) if raw2 else None
- def extract_leave_date(text: str) -> str | None:
- t = text or ""
- raw = _first_group(r"(?:离职时间|离职日期|解除时间|终止时间)[::]?\s*([^\n,,。;;]{4,20})", t)
- if raw:
- return parse_date_any(raw)
- raw2 = _first_group(r"(?:于|在)\s*([12][0-9]{3}[年./-][01]?[0-9][月./-][0-3]?[0-9])\s*(?:离职|解除劳动合同|解除)", t)
- return parse_date_any(raw2) if raw2 else None
- def extract_month_salary(text: str) -> float | None:
- t = text or ""
- raw = _first_group(r"(?:月工资|月薪|工资标准)[::]?\s*([^\n,,。;;]{2,30})", t)
- if raw:
- amt = parse_amount(raw + "元" if "元" not in raw else raw)
- if amt is not None:
- return amt
- # 兜底:出现“月工资8000元”这样的写法
- m = re.search(r"(月工资|月薪|工资标准)" + _WS + r"(?:为|是|:)?\s*(?:人民币|¥)?\s*([0-9]{1,3}(?:,[0-9]{3})*|[0-9]+)\s*元", t)
- if m:
- return float(m.group(2).replace(",", ""))
- return None
- def extract_overtime_desc(text: str) -> str | None:
- t = text or ""
- # 抓取包含“加班”的句子/条目(简化:按换行/句号切)
- parts = re.split(r"[\n。;;]+", t)
- hits = [p.strip() for p in parts if "加班" in p and len(p.strip()) >= 6]
- if not hits:
- return None
- return ";".join(hits[:3])
- def extract_dispute_focus(text: str) -> str | None:
- t = text or ""
- return _first_group(r"(?:争议焦点)[::]\s*([^\n]{2,120})", t)
- def extract_work_duration_text(text: str) -> str | None:
- t = text or ""
- m = re.search(r"(?:工作年限|用工期间|劳动关系存续)[:: ]?\s*([^\n。;;]{4,40})", t)
- if m:
- return m.group(1).strip()
- m2 = re.search(r"(?:自|从)[^\n]{0,30}(?:至|到)[^\n]{0,30}(?:止|共计)", t)
- return m2.group(0).strip()[:80] if m2 else None
- def extract_contract_type(text: str) -> str | None:
- t = text or ""
- if "无固定期限" in t:
- return "无固定期限劳动合同"
- if "固定期限" in t and "劳动合同" in t:
- return "固定期限劳动合同"
- if "未签订" in t and "劳动合同" in t:
- return "未订立书面劳动合同"
- if "劳动合同" in t and "期限" in t:
- return _first_group(r"(?:劳动合同|合同)(?:期限|类型)[::]\s*([^\n,,。;;]{2,30})", t)
- return None
- def extract_injury_related(text: str) -> str | None:
- t = text or ""
- if "工伤" not in t:
- return None
- parts = re.split(r"[\n。;;]+", t)
- hits = [p.strip() for p in parts if "工伤" in p and len(p.strip()) >= 6]
- return ";".join(hits[:2]) if hits else "涉及工伤"
- def extract_social_insurance_hint(text: str) -> str | None:
- t = text or ""
- if not any(k in t for k in ["社保", "社会保险", "五险", "养老保险", "医疗保险", "失业保险", "公积金"]):
- return None
- parts = re.split(r"[\n。;;]+", t)
- hits = [p.strip() for p in parts if any(k in p for k in ["社保", "社会保险", "五险", "未缴纳", "欠缴"]) and len(p.strip()) >= 6]
- return ";".join(hits[:2]) if hits else "涉及社会保险争议"
- def extract_evidence_materials(text: str) -> list[str] | None:
- t = text or ""
- block = _first_group(
- r"(?:证据(?:材料|清单)|附件|所附证据)[::]?\s*(.+?)(?:此致|仲裁请求|事实与理由|申请人[::]|$)",
- t,
- flags=re.S,
- )
- if not block:
- lines = _all_matches(r"^\s*[0-9一二三四五六七八九十]+[.、]\s*([^\n]{2,60})", t, flags=re.M)
- return lines[:15] if lines else None
- lines = [ln.strip(" \t-—") for ln in re.split(r"[\n;;]+", block) if ln.strip()]
- out = []
- for ln in lines:
- ln = re.sub(r"^\s*[((]?\s*[0-9一二三四五六七八九十]+\s*[))]?\s*[.、]?\s*", "", ln)
- if len(ln) >= 2:
- out.append(ln[:120])
- return out[:15] if out else None
- def infer_claim_types(text: str, claims: dict[str, Any], case_cause: str | None) -> list[str]:
- t = text or ""
- types: set[str] = set()
- if case_cause:
- types.add(case_cause)
- mapping = [
- ("工资", "工资报酬"),
- ("加班", "加班费"),
- ("经济补偿", "经济补偿"),
- ("赔偿金", "违法解除赔偿"),
- ("工伤", "工伤待遇"),
- ("年休假", "年休假"),
- ("双倍工资", "未订立书面劳动合同"),
- ("社保", "社会保险"),
- ("未签合同", "未订立书面劳动合同"),
- ]
- for kw, lab in mapping:
- if kw in t:
- types.add(lab)
- for it in (claims or {}).get("items") or []:
- s = str(it)
- for kw, lab in mapping:
- if kw in s:
- types.add(lab)
- return sorted(types)
- def extract_termination_reason(text: str) -> str | None:
- t = text or ""
- direct = _first_group(r"(?:解除原因|解除理由|辞退原因|解除劳动合同原因)[::]" + _WS + r"([^\n。;;]{2,60})", t)
- if direct:
- return direct
- # 兜底:包含“因……解除/辞退”
- m = re.search(r"(?:因|由于)\s*([^\n。;;]{2,40})\s*(?:被)?(?:辞退|解除劳动合同|解除)", t)
- if m:
- return m.group(1).strip()
- return None
- def extract_claims(text: str) -> dict[str, Any]:
- """
- 返回:
- - items: list[str]
- - amount_total: float | None
- """
- t = text or ""
- # 请求段落:仲裁请求/请求事项/请求如下
- block = _first_group(r"(?:仲裁请求|请求事项|请求如下)[::]?\s*(.+?)(?:事实与理由|事实理由|此致|证据目录|$)", t, flags=re.S)
- items: list[str] = []
- if block:
- lines = [ln.strip(" \t-—") for ln in re.split(r"[\n;;]+", block) if ln.strip()]
- # 若是 1. 2. 3. 形式
- norm: list[str] = []
- for ln in lines:
- ln = re.sub(r"^\s*[((]?\s*[0-9一二三四五六七八九十]+\s*[))]?\s*[.、]?\s*", "", ln)
- if ln:
- norm.append(ln)
- items = norm[:10]
- # 汇总金额:从 items 中找最大/累加(MVP:取最大或第一条)
- amounts = []
- for it in items:
- a = parse_amount(it)
- if a is not None:
- amounts.append(a)
- amount_total = sum(amounts) if amounts else None
- return {"items": items, "amount_total": amount_total}
- def _yes_no_from_text(t: str, positive: list[str], negative: list[str]) -> str | None:
- for p in positive:
- if p in t:
- return "是"
- for n in negative:
- if n in t:
- return "否"
- return None
- def _snippet_near_keywords(t: str, keywords: tuple[str, ...], width: int = 120) -> str | None:
- for kw in keywords:
- i = t.find(kw)
- if i >= 0:
- frag = t[max(0, i - 20) : i + width].replace("\n", " ").strip()
- return frag[:200] if frag else None
- return None
- def _amount_near_keywords(t: str, keywords: tuple[str, ...]) -> float | None:
- for kw in keywords:
- i = t.find(kw)
- if i >= 0:
- window = t[i : i + 100]
- if (a := parse_amount(window)) is not None:
- return a
- return None
- def classify_template_primary_cause(text: str) -> str | None:
- """对应《案件普遍的智能分解模板》顶层案由分支(由关键词路由,可多案由时取最先命中)。"""
- t = text or ""
- ordered = [
- ("生育保险待遇纠纷", ("生育津贴", "生育医疗", "产假工资", "生育保险待遇", "产检", "流产假")),
- ("赔偿金纠纷", ("违法解除劳动", "违法终止劳动", "违法辞退", "赔偿金", "2N", "二倍经济补偿")),
- ("经济补偿金纠纷", ("经济补偿金", "代通知金", "N+1", "解除合同经济补偿", "离职补偿")),
- ("追索劳动报酬", ("追索劳动报酬", "拖欠工资", "工资差额", "加班费", "高温津贴", "年休假工资", "未及时足额")),
- ("工伤保险待遇纠纷", ("工伤保险待遇", "劳动能力鉴定", "一次性伤残", "停工留薪", "工伤认定", "工伤")),
- ("劳动关系纠纷类", ("确认劳动关系", "未订立书面劳动合同", "未签订劳动合同", "二倍工资", "双倍工资")),
- ]
- for name, kws in ordered:
- if any(k in t for k in kws):
- return name
- return None
- def merge_dispute_template_fields(text: str, base: dict[str, Any]) -> dict[str, Any]:
- """
- 按「案件普遍的智能分解模板」补充扁平字段(规则/关键词,可与 NLP 模型替换)。
- base 为已跑完基础 extractors 的结果,用于拼装通用要素。
- """
- t = _clean_text(text)
- o: dict[str, Any] = {}
- o["tmpl_primary_cause"] = classify_template_primary_cause(t)
- ga: list[str] = []
- if base.get("applicant_name"):
- ga.append(f"申请人:{base['applicant_name']}")
- if base.get("applicant_type"):
- ga.append(f"类型:{base['applicant_type']}")
- if base.get("worker_position"):
- ga.append(f"岗位:{base['worker_position']}")
- ph = _first_group(r"(?:手机|联系电话|电话)[::]?\s*([0-9\-\s]{7,22})", t)
- if ph:
- ga.append(f"联系方式:{ph.strip()}")
- o["gen_applicant_info"] = ";".join(ga) if ga else None
- gr: list[str] = []
- if base.get("respondent_name"):
- gr.append(f"被申请人:{base['respondent_name']}")
- if base.get("employer_nature"):
- gr.append(f"单位性质:{base['employer_nature']}")
- o["gen_respondent_info"] = ";".join(gr) if gr else None
- facts = _first_group(
- r"(?:事实与理由|事实和理由)[::]?\s*(.+?)(?=仲裁请求|请求事项|此致|证据目录|$)",
- t,
- flags=re.S,
- )
- o["gen_facts_and_reasons"] = (facts.strip()[:4000] if facts else None) or _snippet_near_keywords(
- t, ("事实", "理由", "入职", "离职", "工资"), 200
- )
- # --- 1 劳动关系纠纷类 ---
- o["lr1_pay_cycle"] = _first_group(r"(?:工资发放|劳动报酬发放|支付周期)[::]?\s*([^\n。;]{2,40})", t) or _snippet_near_keywords(
- t, ("按月", "每月", "月薪", "计件"), 30
- )
- ms = base.get("month_salary")
- try:
- ms_f = float(ms) if ms is not None and ms != "" else None
- except (TypeError, ValueError):
- ms_f = None
- o["lr1_pay_amount"] = _amount_near_keywords(t, ("劳动报酬", "工资标准", "月工资", "月薪")) or ms_f
- o["lr1_pay_form"] = _snippet_near_keywords(t, ("银行转账", "现金", "微信", "支付宝", "打卡"))
- o["lr1_si_joined"] = _yes_no_from_text(
- t,
- ["已缴纳社保", "参加社会保险", "缴纳五险", "有社保"],
- ["未缴纳社保", "未参加社会保险", "未缴社保", "无社保"],
- ) or base.get("social_insurance_hint")
- o["lr1_si_benefit_amount"] = _amount_near_keywords(t, ("保险待遇", "社保待遇", "补缴"))
- o["lr1_contract_signed"] = _yes_no_from_text(
- t,
- ["签订劳动合同", "订立书面劳动合同", "有书面合同"],
- ["未签订劳动合同", "未订立书面劳动合同", "无书面合同"],
- )
- o["lr1_open_ended_contract"] = "是" if ("无固定期限" in t or "无固定期" in t) else None
- o["lr1_double_wage_no_contract"] = "是" if ("二倍工资" in t or "双倍工资" in t or "未签劳动合同" in t) else None
- o["lr1_relation_duration"] = base.get("work_duration_text") or _snippet_near_keywords(
- t, ("劳动关系", "用工期间", "入职", "离职"), 80
- )
- # --- 2 工伤保险待遇纠纷 ---
- o["wi_lr_contract_signed"] = o.get("lr1_contract_signed")
- o["wi_lr_relation_duration"] = base.get("work_duration_text") or o.get("lr1_relation_duration")
- o["wi_benefit_pay_time"] = _first_group(r"(?:待遇发放|支付时间|发放时间)[::]?\s*([^\n。;]{2,40})", t)
- o["wi_benefit_amount_total"] = _amount_near_keywords(t, ("工伤保险待遇", "工伤待遇", "一次性伤残", "补助金"))
- o["wi_benefit_disability"] = _amount_near_keywords(t, ("伤残津贴", "伤残补助"))
- o["wi_benefit_prosthetic"] = _amount_near_keywords(t, ("假肢", "辅助器具"))
- o["wi_benefit_medical_allowance"] = _amount_near_keywords(t, ("医疗补助金", "医疗补助"))
- o["wi_benefit_travel"] = _amount_near_keywords(t, ("交通费", "食宿费", "交通食宿"))
- o["wi_benefit_rehab"] = _amount_near_keywords(t, ("康复费", "医疗费"))
- o["wi_benefit_nursing"] = _amount_near_keywords(t, ("护理费",))
- o["wi_benefit_meal"] = _amount_near_keywords(t, ("住院伙食", "伙食补助"))
- o["wi_benefit_pay_form"] = o.get("lr1_pay_form")
- o["wi_si_benefit_amt"] = o.get("lr1_si_benefit_amount")
- o["wi_si_joined"] = o.get("lr1_si_joined")
- o["wi_recognize_ec"] = _yes_no_from_text(
- t,
- ["认可经济补偿", "同意支付经济补偿", "支付经济补偿金"],
- ["不认可经济补偿", "无需支付经济补偿", "不同意经济补偿"],
- )
- # --- 3 追索劳动报酬 ---
- o["sr_pay_cycle"] = o.get("lr1_pay_cycle")
- o["sr_claim_amount"] = _amount_near_keywords(t, ("主张金额", "请求金额", "仲裁请求")) or (base.get("claims") or {}).get(
- "amount_total"
- )
- o["sr_claim_deducted_pay"] = _amount_near_keywords(t, ("克扣", "拖欠工资", "欠付工资"))
- o["sr_claim_overtime_pay"] = _amount_near_keywords(t, ("加班费", "加班工资")) or (
- parse_amount(base.get("overtime_desc") or "") if base.get("overtime_desc") else None
- )
- o["sr_claim_living_allowance"] = _amount_near_keywords(t, ("生活费", "待岗"))
- o["sr_high_temp_allowance"] = _amount_near_keywords(t, ("高温津贴", "防暑降温"))
- o["sr_actual_pay_standard"] = _snippet_near_keywords(t, ("实发工资", "实际支付", "实发"), 40)
- o["sr_agreed_pay_standard"] = _snippet_near_keywords(t, ("约定工资", "合同约定工资", "月薪约定"), 40) or (
- str(base["month_salary"]) if base.get("month_salary") is not None else None
- )
- o["sr_annual_leave_pay"] = _amount_near_keywords(t, ("年休假工资", "未休年假", "带薪年休假"))
- o["sr_unpaid_period"] = _first_group(r"(?:欠付期间|未支付期间|拖欠期间)[::]?\s*([^\n。;]{2,60})", t) or _snippet_near_keywords(
- t, ("至", "期间"), 50
- )
- o["sr_overtime_amount"] = o.get("sr_claim_overtime_pay")
- # --- 4 经济补偿金纠纷 ---
- o["ec_avg_salary_12m"] = _first_group(
- r"(?:离职前\s*12\s*个月|十二个月).{0,12}(?:平均|月均)工资[::]?\s*([^\n,。;]{2,40})",
- t,
- ) or _amount_near_keywords(t, ("平均工资", "月均工资", "月平均工资"))
- o["ec_claim_amount"] = o.get("sr_claim_amount")
- o["ec_double_wage_part"] = _amount_near_keywords(t, ("二倍工资", "双倍工资", "未签劳动合同"))
- o["ec_illegal_term_part"] = _amount_near_keywords(t, ("违法解除", "违法终止", "违法辞退"))
- o["ec_illegal_probation_part"] = _amount_near_keywords(t, ("违法约定试用期", "试用期赔偿"))
- o["ec_extra_compensation_part"] = _amount_near_keywords(t, ("额外补偿", "加付"))
- o["ec_notice_pay"] = _amount_near_keywords(t, ("代通知金", "提前三十日"))
- o["ec_additional_damages"] = _amount_near_keywords(t, ("加付赔偿金", "赔偿金50%"))
- o["ec_contract_duration"] = base.get("work_duration_text")
- o["ec_leave_reason"] = base.get("termination_reason")
- o["ec_leave_date"] = base.get("leave_date")
- # --- 5 赔偿金纠纷 ---
- o["dm_claim_amount"] = o.get("sr_claim_amount")
- o["dm_illegal_dismissal_damages"] = _amount_near_keywords(t, ("违法解除劳动", "违法辞退", "赔偿金", "2倍"))
- o["dm_contract_exists"] = o.get("lr1_contract_signed")
- o["dm_terminate_reason"] = base.get("termination_reason")
- o["dm_contract_continue"] = _yes_no_from_text(t, ["继续履行劳动合同", "恢复劳动关系"], ["不继续履行", "解除劳动关系"])
- # --- 6 生育保险待遇纠纷 ---
- o["mi_claim_amount"] = o.get("sr_claim_amount")
- o["mi_maternity_medical"] = _amount_near_keywords(t, ("生育医疗费", "生育医疗费用", "产检费"))
- o["mi_maternity_allowance_salary"] = _amount_near_keywords(t, ("生育津贴", "产假工资"))
- o["mi_additional_damages"] = o.get("ec_additional_damages")
- o["mi_travel_accommodation"] = o.get("wi_benefit_travel")
- o["mi_contract_continue"] = o.get("dm_contract_continue")
- o["mi_terminate_reason"] = base.get("termination_reason")
- return o
- def refresh_derived_element_fields(out: dict[str, Any], text: str) -> None:
- """
- 在 merge_dispute_template_fields 或 LLM 补丁写入 out 之后,重算 primary_cause_type、elements_hierarchy、case_elements_table。
- """
- t = _clean_text(text)
- templates = load_hierarchy_templates()
- cause = out.get("tmpl_primary_cause") or classify_template_primary_cause(t) or DEFAULT_CAUSE_TYPE
- if templates and cause not in templates:
- cause = DEFAULT_CAUSE_TYPE
- out["primary_cause_type"] = cause
- out["tmpl_primary_cause"] = cause
- if templates and cause in templates:
- hier = build_elements_hierarchy_for_cause(cause, out)
- if hier is not None:
- out["elements_hierarchy"] = hier
- schema = load_case_elements_schema()
- out["case_elements_table"] = build_case_elements_table(out, schema)
- @dataclass
- class RuleBasedLaborExtractor:
- """
- MVP:基于规则/正则的劳动仲裁要素抽取器(后续可替换为 BERT 微调模型)。
- 要素分组与中文标签由 backend/data/case_elements_schema.json 定义,可替换为您自己的案件要素表。
- """
- extractors: list[tuple[str, Callable[[str], Any]]] | None = None
- _schema: dict[str, Any] | None = None
- def __post_init__(self) -> None:
- self._schema = load_case_elements_schema()
- if self.extractors is None:
- self.extractors = [
- ("case_number", extract_case_number),
- ("filing_date", extract_filing_date),
- ("arbitration_org", extract_arbitration_org),
- ("case_title", extract_case_title),
- ("applicant_name", extract_applicant_name),
- ("applicant_type", extract_applicant_type),
- ("respondent_name", extract_respondent_name),
- ("employer_nature", extract_employer_nature),
- ("worker_position", extract_worker_position),
- ("employment_type", extract_employment_type),
- ("case_cause", detect_case_cause),
- ("dispute_focus", extract_dispute_focus),
- ("entry_date", extract_entry_date),
- ("leave_date", extract_leave_date),
- ("work_duration_text", extract_work_duration_text),
- ("month_salary", extract_month_salary),
- ("overtime_desc", extract_overtime_desc),
- ("termination_reason", extract_termination_reason),
- ("contract_type", extract_contract_type),
- ("injury_related", extract_injury_related),
- ("social_insurance_hint", extract_social_insurance_hint),
- ("claims", extract_claims),
- ("law_refs", extract_law_refs),
- ("evidence_materials", extract_evidence_materials),
- ]
- def extract(self, text: str) -> dict[str, Any]:
- text = _clean_text(text)
- out: dict[str, Any] = {}
- for key, fn in self.extractors or []:
- try:
- out[key] = fn(text)
- except Exception:
- out[key] = None
- try:
- out["claim_types"] = infer_claim_types(text, out.get("claims") or {}, out.get("case_cause"))
- except Exception:
- out["claim_types"] = []
- try:
- out.update(merge_dispute_template_fields(text, out))
- except Exception:
- pass
- refresh_derived_element_fields(out, text)
- return out
|