角色业务范围收紧工单与模板;报价日期不当费用;未选舱不迁已报价;另开一票可清违禁 hold
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
本文件职责:把销售原话收成多项 added_services,供确认、工单和 TMS 的 addedServices 使用。
|
||||
12 个服务词只要完整出现就记;名单外的必须写在「增值服务」后面才记,并保留原词。
|
||||
服务词后面紧跟的「+数字」(如 TC +1.5、托盘+1)并进同一项,写成 TC+1.5、托盘+1。
|
||||
包装类型/包装方式这一栏里的「托盘」只算包装,不抄进增值服务。
|
||||
「增值服务:」同一行为空时,不把下一行收进来,也不沿用上一句已经记下的服务。
|
||||
不挡查价。不改六态。海运、陆运不走这里。
|
||||
@@ -30,6 +31,8 @@ _CN_RE = re.compile("|".join(re.escape(word) for word in _CN_TERMS))
|
||||
_MUST_GO_RE = re.compile(r"(?i)MUST\s*GO")
|
||||
_DG_RE = re.compile(r"(?i)(?<![A-Za-z])DG(?![A-Za-z])")
|
||||
_TC_RE = re.compile(r"(?i)(?<![A-Za-z])TC(?![A-Za-z])")
|
||||
# 服务词后的加价/数量:允许空格,记成「词+数字」,不把空格留进销售可见文案。
|
||||
_QTY_SUFFIX_RE = re.compile(r"[^\S\n]*\+[^\S\n]*(\d+(?:\.\d+)?)")
|
||||
# 冒号后只吃同一行。\s 会跨行,空着的增值服务会把下一行「报价日期(非必填):」吞进来。
|
||||
_LABEL_RE = re.compile(
|
||||
r"增值服务(?:(非必填)|\(非必填\))?[^\S\n]*[::][^\S\n]*([^\n]*)"
|
||||
@@ -76,12 +79,13 @@ def harvest_added_services(text: str) -> list[str]:
|
||||
|
||||
补问示例行「(如:托盘、…)」整行不看,避免清单被当成已填。
|
||||
「不带电」不算带电;「卡转低消」不算再加一项卡转。
|
||||
「TC +1.5」记成 TC+1.5,数量跟服务名同一项。
|
||||
副作用:无。
|
||||
"""
|
||||
raw = _without_clarify_examples(text or "")
|
||||
events: list[tuple[int, str]] = []
|
||||
# 包装栏里的托盘已在 _canonical_events 排除,这里只收正文里真正的服务词。
|
||||
events.extend(_canonical_events(raw))
|
||||
events.extend((start, word) for start, _end, word in _canonical_events(raw))
|
||||
for match in _LABEL_RE.finditer(raw):
|
||||
value = (match.group(1) or "").strip()
|
||||
if not value or value.startswith("(如") or value.startswith("(如"):
|
||||
@@ -100,7 +104,52 @@ def harvest_added_services(text: str) -> list[str]:
|
||||
if extra:
|
||||
events.append((base + at + max(piece.find(extra), 0), extra))
|
||||
events.sort(key=lambda item: item[0])
|
||||
return _dedupe([word for _pos, word in events])
|
||||
result = _dedupe([word for _pos, word in events])
|
||||
# #region agent log
|
||||
try:
|
||||
import json as _dbg_json
|
||||
import time as _dbg_time
|
||||
from pathlib import Path as _DbgPath
|
||||
|
||||
_tails: list[dict[str, str]] = []
|
||||
for _m in list(_TC_RE.finditer(raw)) + list(_CN_RE.finditer(raw)):
|
||||
if _m.group(0) not in {"TC", "tc", "托盘"} and _m.group(0).casefold() != "tc":
|
||||
continue
|
||||
_end = _m.end()
|
||||
_tails.append(
|
||||
{
|
||||
"word": _m.group(0),
|
||||
"after": raw[_end : _end + 12],
|
||||
}
|
||||
)
|
||||
_payload = {
|
||||
"sessionId": "5ea5b3",
|
||||
"runId": "post-fix",
|
||||
"hypothesisId": "H1",
|
||||
"location": "added_services.py:harvest_added_services",
|
||||
"message": "harvest result vs trailing modifiers",
|
||||
"data": {
|
||||
"text_preview": (raw or "")[:120],
|
||||
"found": result,
|
||||
"match_tails": _tails,
|
||||
},
|
||||
"timestamp": int(_dbg_time.time() * 1000),
|
||||
}
|
||||
for _path in (
|
||||
_DbgPath(__file__).resolve().parents[3] / "debug-5ea5b3.log",
|
||||
_DbgPath(r"C:/Users/ytd_extdev_0903/incoming/debug-5ea5b3.log"),
|
||||
_DbgPath(r"d:/own/友通达询价智能体(新)/debug-5ea5b3.log"),
|
||||
):
|
||||
try:
|
||||
_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _path.open("a", encoding="utf-8") as _f:
|
||||
_f.write(_dbg_json.dumps(_payload, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
return result
|
||||
|
||||
|
||||
def apply_air_value_added(
|
||||
@@ -115,6 +164,7 @@ def apply_air_value_added(
|
||||
只应在空运调用。副作用:返回新字典,不改入参。
|
||||
"""
|
||||
merged = dict(facts or {})
|
||||
before_added = str(merged.get("added_services") or "")
|
||||
found = harvest_added_services(text)
|
||||
labeled = _labeled_added_value(text)
|
||||
# 销售把「增值服务:」留空:这一栏就是空的。
|
||||
@@ -122,23 +172,81 @@ def apply_air_value_added(
|
||||
# 本句没写这一栏、也没提到服务时,才保留已经记下的旧值。
|
||||
if labeled == "":
|
||||
items = found
|
||||
branch = "labeled_empty"
|
||||
elif found:
|
||||
items = _dedupe(split_added_services(previous) + found)
|
||||
branch = "found_merge"
|
||||
else:
|
||||
items = _dedupe(
|
||||
split_added_services(previous)
|
||||
+ split_added_services(str(merged.get("added_services") or ""))
|
||||
)
|
||||
branch = "keep_previous"
|
||||
if items:
|
||||
merged["added_services"] = "、".join(items)
|
||||
elif not str(merged.get("added_services") or "").strip():
|
||||
merged.pop("added_services", None)
|
||||
_keep_stated_package(text, merged)
|
||||
# #region agent log
|
||||
try:
|
||||
import json as _dbg_json
|
||||
import time as _dbg_time
|
||||
from pathlib import Path as _DbgPath
|
||||
|
||||
_payload = {
|
||||
"sessionId": "5ea5b3",
|
||||
"runId": "post-fix",
|
||||
"hypothesisId": "H2",
|
||||
"location": "added_services.py:apply_air_value_added",
|
||||
"message": "apply merge before/after",
|
||||
"data": {
|
||||
"text_preview": (text or "")[:120],
|
||||
"before_added": before_added,
|
||||
"found": found,
|
||||
"previous": previous,
|
||||
"labeled": labeled,
|
||||
"branch": branch,
|
||||
"after_added": str(merged.get("added_services") or ""),
|
||||
"pack": str(merged.get("包装方式") or ""),
|
||||
},
|
||||
"timestamp": int(_dbg_time.time() * 1000),
|
||||
}
|
||||
for _path in (
|
||||
_DbgPath(__file__).resolve().parents[3] / "debug-5ea5b3.log",
|
||||
_DbgPath(r"C:/Users/ytd_extdev_0903/incoming/debug-5ea5b3.log"),
|
||||
_DbgPath(r"d:/own/友通达询价智能体(新)/debug-5ea5b3.log"),
|
||||
):
|
||||
try:
|
||||
_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _path.open("a", encoding="utf-8") as _f:
|
||||
_f.write(_dbg_json.dumps(_payload, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
return merged
|
||||
|
||||
|
||||
def _canonical_events(raw: str) -> list[tuple[int, str]]:
|
||||
"""按原文位置列出 12 个标准词。重叠区间只留更长的那一项。"""
|
||||
def _attach_qty_suffix(raw: str, start: int, end: int, word: str) -> tuple[int, int, str]:
|
||||
"""
|
||||
服务词后面若有 +数字,并进同一项。
|
||||
|
||||
例:TC +1.5 → (起点, 终点, "TC+1.5")。没有数量则原样返回。
|
||||
为何:销售口播常写「TC +1.5、托盘 +1」,只记服务名会丢数量。
|
||||
"""
|
||||
match = _QTY_SUFFIX_RE.match(raw or "", end)
|
||||
if not match:
|
||||
return start, end, word
|
||||
return start, match.end(), f"{word}+{match.group(1)}"
|
||||
|
||||
|
||||
def _canonical_events(raw: str) -> list[tuple[int, int, str]]:
|
||||
"""
|
||||
按原文位置列出 12 个标准词(可带 +数量)。
|
||||
|
||||
返回 (起点, 终点, 展示词)。重叠区间只留更长的那一项。
|
||||
"""
|
||||
found: list[tuple[int, int, str]] = []
|
||||
package_pallets = _package_pallet_spans(raw)
|
||||
for match in _CN_RE.finditer(raw):
|
||||
@@ -147,21 +255,21 @@ def _canonical_events(raw: str) -> list[tuple[int, str]]:
|
||||
continue
|
||||
if word == "托盘" and _span_inside(match.start(), match.end(), package_pallets):
|
||||
continue
|
||||
found.append((match.start(), match.end(), word))
|
||||
found.append(_attach_qty_suffix(raw, match.start(), match.end(), word))
|
||||
for match in _MUST_GO_RE.finditer(raw):
|
||||
found.append((match.start(), match.end(), "MUST GO"))
|
||||
found.append(_attach_qty_suffix(raw, match.start(), match.end(), "MUST GO"))
|
||||
for match in _DG_RE.finditer(raw):
|
||||
found.append((match.start(), match.end(), "DG"))
|
||||
found.append(_attach_qty_suffix(raw, match.start(), match.end(), "DG"))
|
||||
for match in _TC_RE.finditer(raw):
|
||||
found.append((match.start(), match.end(), "TC"))
|
||||
found.append(_attach_qty_suffix(raw, match.start(), match.end(), "TC"))
|
||||
found.sort(key=lambda item: (item[0], -(item[1] - item[0])))
|
||||
accepted: list[tuple[int, int]] = []
|
||||
events: list[tuple[int, str]] = []
|
||||
events: list[tuple[int, int, str]] = []
|
||||
for start, end, word in found:
|
||||
if any(not (end <= left or start >= right) for left, right in accepted):
|
||||
continue
|
||||
accepted.append((start, end))
|
||||
events.append((start, word))
|
||||
events.append((start, end, word))
|
||||
return events
|
||||
|
||||
|
||||
@@ -171,31 +279,19 @@ def _outside_list_text(piece: str) -> str:
|
||||
|
||||
「托盘」本身会被标准词扫到,这里不重复收。
|
||||
「打木架和带电」只把打木架留下来,带电仍由标准词按出现位置记。
|
||||
带 +数量 的标准词整段擦掉,避免残留「+1.5」再当成名单外服务。
|
||||
"""
|
||||
spans = _canonical_events(piece)
|
||||
if not spans:
|
||||
return piece.strip()
|
||||
chars = list(piece)
|
||||
for start, word in spans:
|
||||
# _canonical_events 只有起点。用标准词长度擦掉;MUST GO 用原文长度。
|
||||
end = start + _span_len(piece, start, word)
|
||||
for start, end, _word in spans:
|
||||
for index in range(start, min(end, len(chars))):
|
||||
chars[index] = " "
|
||||
rest = _FILLER.sub("", "".join(chars))
|
||||
return rest.strip()
|
||||
|
||||
|
||||
def _span_len(piece: str, start: int, word: str) -> int:
|
||||
"""标准词在原文里占几位。MUST GO 中间空格可有可无。"""
|
||||
if word == "MUST GO":
|
||||
match = _MUST_GO_RE.match(piece, start)
|
||||
if match:
|
||||
return match.end() - match.start()
|
||||
if word in {"DG", "TC"}:
|
||||
return 2
|
||||
return len(word)
|
||||
|
||||
|
||||
def _labeled_added_value(text: str) -> str | None:
|
||||
"""
|
||||
原话里有没有单独的「增值服务」栏。
|
||||
|
||||
Reference in New Issue
Block a user