161 lines
7.0 KiB
Python
161 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""布莱迪压力表 - 订单附件识别 命令行入口。
|
||
|
||
用法:
|
||
python main.py --id 26B742 # 单个总排号(粗分类,默认)
|
||
python main.py --id 26B742,26B743,26B744 # 批量,逗号分隔
|
||
python main.py --ids-file ids.txt # 批量,文件每行一个总排号
|
||
python main.py --id 26B742 --mode fine # 精分类(输出"大类:细分"格式)
|
||
python main.py --id 26B742 --config other.yaml # 指定其他配置文件
|
||
python main.py --id 26B742 --pretty # 格式化输出JSON(默认单行紧凑)
|
||
python main.py --id 26B742 --log-dir /tmp/logs # 覆盖配置文件里的日志目录
|
||
python main.py --id 26B742 --enable-other # 允许模型使用"其他"兜底类目
|
||
|
||
分类模式(--mode):
|
||
coarse (默认) - 只判断大类: 资料/配件/耗材(启用 --enable-other 时还有"其他")
|
||
fine - 输出"大类:细分类目",如"配件:针型阀""资料:说明书"
|
||
|
||
"其他"兜底类目(--enable-other 或配置文件 business.enable_other_category):
|
||
默认关闭。关闭时,模型和格式校验都不知道"其他"这个选项存在,行为与
|
||
未支持"其他"之前完全一致。开启后,遇到确实是随货附件、但不属于任何
|
||
已列出细分类目的情况,模型可以归为"其他",避免这类附件被漏判、或被
|
||
模型勉强套进不准确的类目里。命令行只能把它从"关"打开成"开",无法
|
||
反过来用命令行强制关闭配置文件里已经打开的设置。
|
||
|
||
输出:
|
||
每个总排号一行 JSON(JSON Lines 格式),例如:
|
||
{"zong_pai_hao": "26B742", "status": "ok", "has_attachment": true, "types": ["资料"]}
|
||
{"zong_pai_hao": "26B744", "status": "ok", "has_attachment": true, "types": ["配件:针型阀", "配件:表弯管"]}
|
||
{"zong_pai_hao": "XXXXXX", "status": "not_found", "has_attachment": null, "types": []}
|
||
|
||
types 是唯一的类型字段,不再有单独的 fine_types:
|
||
coarse模式下 types 是大类列表;fine模式下每一项都是"大类:细分类目"。
|
||
|
||
status 字段含义:
|
||
ok - 识别成功
|
||
not_found - 数据库中查不到该总排号
|
||
empty_param - 该总排号存在,但新参数字段为空
|
||
llm_call_error - LLM 调用失败(网络/接口错误,重试耗尽)
|
||
llm_format_error - LLM 返回内容格式不符合约定(重试耗尽)
|
||
db_error - 数据库连接/查询失败(影响整批)
|
||
|
||
日志:
|
||
每次实际发起LLM调用的总排号,都会在日志目录下生成一个独立的日志文件
|
||
(文件名含总排号和时间戳),完整记录发给模型的对话内容、模型的每一次
|
||
原始回复(含被判定格式错误、触发重试的)、以及最终解析结果。查不到
|
||
(not_found)或参数为空(empty_param)的总排号不产生日志文件。
|
||
日志目录默认读取配置文件 business.log_dir,可用 --log-dir 临时覆盖。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import logging
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))
|
||
|
||
from classifier import classify_batch # noqa: E402
|
||
from config_loader import ConfigError, load_config # noqa: E402
|
||
|
||
|
||
def _parse_ids(args: argparse.Namespace) -> list[str]:
|
||
"""从 --id 或 --ids-file 解析出总排号列表,去除空白项。"""
|
||
ids: list[str] = []
|
||
if args.id:
|
||
ids.extend(s.strip() for s in args.id.split(",") if s.strip())
|
||
if args.ids_file:
|
||
p = Path(args.ids_file)
|
||
if not p.exists():
|
||
print(f"[错误] 总排号文件不存在: {p.resolve()}", file=sys.stderr)
|
||
sys.exit(1)
|
||
with p.open("r", encoding="utf-8") as f:
|
||
ids.extend(line.strip() for line in f if line.strip())
|
||
return ids
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(
|
||
description="布莱迪压力表订单附件识别工具",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog=__doc__,
|
||
)
|
||
parser.add_argument(
|
||
"--id", type=str, default=None,
|
||
help="总排号,单个或逗号分隔的多个,例如: 26B742 或 26B742,26B743",
|
||
)
|
||
parser.add_argument(
|
||
"--ids-file", type=str, default=None,
|
||
help="包含总排号的文本文件路径,每行一个总排号",
|
||
)
|
||
parser.add_argument(
|
||
"--config", type=str, default="config.yaml",
|
||
help="配置文件路径 (默认: config.yaml)",
|
||
)
|
||
parser.add_argument(
|
||
"--mode", type=str, default=None, choices=["coarse", "fine"],
|
||
help="分类粒度: coarse(粗分类,默认) 或 fine(精分类)。不指定则使用配置文件 business.default_mode",
|
||
)
|
||
parser.add_argument(
|
||
"--log-dir", type=str, default=None,
|
||
help="LLM对话日志存放目录。不指定则使用配置文件 business.log_dir",
|
||
)
|
||
parser.add_argument(
|
||
"--pretty", action="store_true",
|
||
help="以缩进格式输出JSON数组,而非默认的紧凑JSON Lines格式",
|
||
)
|
||
parser.add_argument(
|
||
"--enable-other", action="store_true",
|
||
help="允许模型使用'其他'兜底类目。不指定则使用配置文件 business.enable_other_category"
|
||
"(缺省为关闭);本开关只能从命令行打开,无法用命令行强制关闭配置文件里已打开的设置",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
if not args.id and not args.ids_file:
|
||
parser.error("必须提供 --id 或 --ids-file 其中之一")
|
||
|
||
ids = _parse_ids(args)
|
||
if not ids:
|
||
print("[错误] 未解析到任何有效的总排号", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
try:
|
||
cfg = load_config(args.config)
|
||
except ConfigError as e:
|
||
print(f"[配置错误] {e}", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
logging.basicConfig(
|
||
level=getattr(logging, cfg["business"]["log_level"], logging.INFO),
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
stream=sys.stderr, # 日志走stderr,保证stdout只有干净的JSON结果,便于管道/重定向
|
||
)
|
||
|
||
mode = args.mode or cfg["business"]["default_mode"]
|
||
log_dir = args.log_dir or cfg["business"]["log_dir"]
|
||
# enable_other_category 是后加的可选配置项,缺省按"关闭"处理;--enable-other
|
||
# 只能从"关"打开成"开",不支持反过来用命令行强制关闭配置文件里已打开的设置。
|
||
enable_other = bool(args.enable_other) or bool(cfg["business"].get("enable_other_category", False))
|
||
|
||
results = classify_batch(
|
||
db_cfg=cfg["database"],
|
||
llm_cfg=cfg["llm"],
|
||
zong_pai_hao_list=ids,
|
||
max_workers=cfg["business"]["max_workers"],
|
||
mode=mode,
|
||
log_dir=log_dir,
|
||
enable_other=enable_other,
|
||
)
|
||
|
||
if args.pretty:
|
||
print(json.dumps(results, ensure_ascii=False, indent=2))
|
||
else:
|
||
for r in results:
|
||
print(json.dumps(r, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|