Files
inquiry_robot/inquiry-agent/agent/llm/mode_extract_adjust.py
T

171 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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