落地销售调价:按自然语言改价并单独记销售价,拉群和后台不再把调后价当成 TMS 原价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
DeepSeek B:调价意图与抽项。
|
||||
|
||||
本文件职责:判断是不是调价、抽出费用/方向/比例或改到金额。
|
||||
禁止:模型改价;禁止把某一句口令写成唯一识别;未确认数据用途不得出站。
|
||||
调用:私聊/群 Worker 或 inbox 线程在已开网时;禁止回调线程同步跑。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from agent.llm.modes import LlmMode
|
||||
from agent.schema.quote_adjust import AdjustExtract, AdjustItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXTRACT_ADJUST_TOOL = "extract_quote_adjust"
|
||||
|
||||
|
||||
def extract_adjust_tool_definition() -> dict[str, Any]:
|
||||
"""B 抽调价项的 tool。金额只原样抽出,不算新价。"""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": EXTRACT_ADJUST_TOOL,
|
||||
"description": (
|
||||
"Decide if the sales message is adjusting an existing quote "
|
||||
"and extract each fee change. Do not require a fixed phrase."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"is_adjust": {"type": "boolean"},
|
||||
"wants_total": {"type": "boolean"},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"fee_name": {"type": "string"},
|
||||
"kind": {"type": "string", "enum": ["percent", "target"]},
|
||||
"direction": {"type": "string", "enum": ["up", "down", "set"]},
|
||||
"value": {"type": "number"},
|
||||
},
|
||||
"required": ["fee_name", "kind", "direction", "value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["is_adjust", "wants_total", "items"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def parse_adjust_payload(raw: Any) -> Optional[AdjustExtract]:
|
||||
"""把模型 JSON / tool 参数收成 AdjustExtract。结构不对返回 None,不猜。"""
|
||||
data = raw
|
||||
if isinstance(raw, str):
|
||||
text = raw.strip()
|
||||
if text.startswith("```"):
|
||||
text = text.strip("`")
|
||||
if text.lower().startswith("json"):
|
||||
text = text[4:].strip()
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
if "is_adjust" not in data:
|
||||
return None
|
||||
items: list[AdjustItem] = []
|
||||
for row in data.get("items") or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
kind = str(row.get("kind") or "percent").strip()
|
||||
direction = str(row.get("direction") or "up").strip()
|
||||
if kind not in {"percent", "target"}:
|
||||
continue
|
||||
if direction not in {"up", "down", "set"}:
|
||||
continue
|
||||
try:
|
||||
value = float(row.get("value"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
items.append(
|
||||
AdjustItem(
|
||||
fee_name=str(row.get("fee_name") or "").strip(),
|
||||
kind=kind,
|
||||
direction=direction,
|
||||
value=value,
|
||||
)
|
||||
)
|
||||
return AdjustExtract(
|
||||
is_adjust=bool(data.get("is_adjust")),
|
||||
items=items,
|
||||
wants_total=bool(data.get("wants_total")),
|
||||
)
|
||||
|
||||
|
||||
def build_adjust_messages(text: str, fee_names: list[str]) -> list[dict[str, str]]:
|
||||
"""B 的系统/用户消息。口令不写死,只抽项不算价。"""
|
||||
names = "、".join(n for n in fee_names if n) or "(当前报价未列出费用名)"
|
||||
return [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"你是询价调价抽取器。只根据销售原话判断是不是在调已有报价,并抽出每一项。"
|
||||
"必须调用 extract_quote_adjust。不要对销售说话,不要改价。"
|
||||
"口令不写死:上升、上浮、往上抬、加一成、涨一点、改到某金额、在现有报价基础上调整都算调价。"
|
||||
"没点名费用、只说报价:is_adjust=true,wants_total=false,items 为空。"
|
||||
"只动总销售价/总成本价/合计/总价/汇总价:wants_total=true,items 仍要一条(fee_name 可空),带 kind/direction/value。"
|
||||
"kind 只能 percent 或 target;direction 只能 up/down/set。"
|
||||
"value 必须来自原话数字;一成=10。禁止编造费用名和金额。"
|
||||
"不是调价(问候、新询价、谢谢)时 is_adjust=false。"
|
||||
f"当前可调费用名:{names}"
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": text},
|
||||
]
|
||||
|
||||
|
||||
def invoke_extract_adjust(
|
||||
text: str,
|
||||
*,
|
||||
fee_names: list[str] | None = None,
|
||||
allow_network: bool = False,
|
||||
chat_fn: Optional[Any] = None,
|
||||
) -> Optional[AdjustExtract]:
|
||||
"""
|
||||
调用 DeepSeek B 抽调价。失败返回 None,由调用方走本地兜底。
|
||||
|
||||
chat_fn:单测注入。allow_network=False 且无 chat_fn:不打外网。
|
||||
"""
|
||||
if chat_fn is None and not allow_network:
|
||||
return None
|
||||
from agent.llm.http_client import LlmHttpClient
|
||||
|
||||
client = LlmHttpClient.from_settings()
|
||||
chat = chat_fn or client.chat
|
||||
result = chat(
|
||||
mode=LlmMode.EXTRACT_FIELDS,
|
||||
messages=build_adjust_messages(text, list(fee_names or [])),
|
||||
tools=[extract_adjust_tool_definition()],
|
||||
tool_choice={"type": "function", "function": {"name": EXTRACT_ADJUST_TOOL}},
|
||||
)
|
||||
if not getattr(result, "ok", False):
|
||||
logger.info("extract.adjust 失败 err=%s", getattr(result, "error", ""))
|
||||
return None
|
||||
raw_args: Any = None
|
||||
for call in getattr(result, "tool_calls", None) or []:
|
||||
fn = (call or {}).get("function") or {}
|
||||
if fn.get("name") == EXTRACT_ADJUST_TOOL:
|
||||
raw_args = fn.get("arguments")
|
||||
break
|
||||
if raw_args is None and getattr(result, "content", ""):
|
||||
raw_args = result.content
|
||||
parsed = parse_adjust_payload(raw_args)
|
||||
logger.info(
|
||||
"extract.adjust ok=%s items=%s wants_total=%s",
|
||||
None if parsed is None else parsed.is_adjust,
|
||||
0 if parsed is None else len(parsed.items),
|
||||
False if parsed is None else parsed.wants_total,
|
||||
)
|
||||
return parsed
|
||||
Reference in New Issue
Block a user