增加生成学校易耗品填写的转换程序

This commit is contained in:
wandering
2026-08-31 18:25:35 +08:00
parent 44496a54e6
commit 4fbb4c36ed
19 changed files with 777 additions and 365 deletions
-4
View File
@@ -19,9 +19,7 @@ from .llm_extractor import (
llm_query_text,
load_cache,
load_match_result,
merge_supplement_into_info,
parse_json_response,
process_user_supplement,
)
__all__ = [
@@ -34,9 +32,7 @@ __all__ = [
"load_cache",
"load_match_result",
"llm_query_text",
"merge_supplement_into_info",
"parse_json_response",
"process_user_supplement",
"EXTRACTION_PARALLEL_COUNT",
"FILE_EVENTS_LOG",
"SUPPORTED_EXTENSIONS",
+18 -7
View File
@@ -115,6 +115,8 @@ def _build_summary(data: dict[str, Any]) -> dict[str, str]:
"general": "普通发票",
"payment": "支付记录",
"application": "出差申请单",
"attachments": "其他附件",
"note": "易耗品单",
}
summary = {}
@@ -259,11 +261,12 @@ def extract_invoices(
- 出差事前申请单 -> 单独存储,不参与匹配
Returns:
(payment_records, applications, groups):
- payment_records: 支付记录列表(仅包含真实发票,不含申请单)
- applications: 出差事前申请单列表(单独存储,不参与支付匹配)
元组 (payment_records, applications, groups):
- payment_records: 支付记录列表(每项含关联发票 _matched_invoices)
- applications: 出差事前申请单列表
- groups: 按文档类型分组的字典
{'travel': [差旅发票], 'general': [普通发票], 'application': [出差事前申请单]}
{'travel': [差旅发票], 'general': [普通发票], 'application': [出差事前申请单],
'attachments': [其他附件]}
Raises:
ExtractionError: 当所有文件提取均失败时抛出,携带失败文件列表和原始错误。
@@ -280,13 +283,14 @@ def extract_invoices(
all_files = _find_all_files(directory)
if not all_files:
log.warning("未找到支持的文件")
return [], [], {"travel": [], "general": [], "application": []}
return [], [], {"travel": [], "general": [], "application": [], "attachments": []}
log.info(f"发现 {len(all_files)} 个文件")
all_invoices = []
all_cards = []
applications = []
attachments = []
# 记录失败文件及其错误信息
failed_files: list[tuple[str, str]] = []
@@ -318,6 +322,9 @@ def extract_invoices(
elif inv_type == "payment":
all_cards.append(result)
log.info(f"[{inv_type}] 已解析: {file_path.name}")
elif inv_type in ("attachments", "note"):
attachments.append(result)
log.info(f"[{inv_type}] 已解析: {file_path.name}")
elif result.get("invoice_number"):
all_invoices.append(result)
log.info(f"[{inv_type}] 已解析: {file_path.name}")
@@ -334,7 +341,9 @@ def extract_invoices(
details=error_details,
)
log.info(f"分类结果: 发票 {len(all_invoices)} 张, 支付记录 {len(all_cards)} 条, 申请单 {len(applications)} 份")
log.info(
f"分类结果: 发票 {len(all_invoices)} 张, 支付记录 {len(all_cards)} 条, 申请单 {len(applications)} 份, 附件 {len(attachments)} 个"
)
if not all_invoices:
log.warning("未成功解析任何发票")
@@ -349,10 +358,12 @@ def extract_invoices(
all_documents.extend(applications)
groups = classify_invoice_batch(all_documents)
groups["attachments"] = attachments
log.info(
f"文档分类: 差旅发票 {len(groups['travel'])} 张, "
f"普通发票 {len(groups['general'])} 张, "
f"出差事前申请单 {len(groups['application'])} 份"
f"出差事前申请单 {len(groups['application'])} 份, "
f"附件 {len(groups['attachments'])} 个"
)
return payment_records, applications, groups
+5 -90
View File
@@ -43,7 +43,6 @@ from ...infra.documents.invoice import CACHE_DIR_NAME
from ...infra.llm.prompt import (
build_invoice_system_prompt,
build_normal_info_system_prompt,
build_supplement_system_prompt,
build_travel_info_system_prompt,
)
@@ -386,6 +385,7 @@ def build_extraction_user_message(
cache_map: dict[str, Any],
match_result: dict[str, list[dict[str, Any]]],
previous_analysis: dict[str, Any] | None = None,
user_supplement: str | None = None,
) -> str:
"""构建提取请求的用户消息(供 Agent 调度使用)。
@@ -393,6 +393,7 @@ def build_extraction_user_message(
cache_map: 缓存数据映射。
match_result: 匹配结果。
previous_analysis: 上一轮 LLM 分析结果(可选,补充文件时传入作为历史上下文)。
user_supplement: 用户文字补充(可选,传入时作为修正上下文)。
Returns:
拼接好的用户消息字符串。
@@ -411,6 +412,9 @@ def build_extraction_user_message(
+ json.dumps(previous_analysis, ensure_ascii=False, indent=2)
)
if user_supplement:
parts.append("【用户文字补充】用户补充了以下文字信息,请据此更新你的分析结果。\n" + user_supplement)
if match_result:
parts.append(
"【发票与支付记录匹配结果】"
@@ -500,92 +504,3 @@ def extract_normal_info(
包含报销说明、发票总数、总金额、支付方式、附件清单等字段的字典。
"""
return _extract_info(source_dir, build_normal_info_system_prompt(), "普通发票")
# ------------------------------------------------------------------
# 用户补充信息处理
# ------------------------------------------------------------------
def process_user_supplement(
user_text: str,
extracted_info: dict[str, Any],
invoice_type: str,
source_dir: Path | None = None,
) -> dict[str, Any]:
"""让用户补充的文字信息通过 LLM 分析,返回需要更新的字段。
Args:
user_text: 用户输入的文字。
extracted_info: 当前已提取的报销信息。
invoice_type: "travel" 或 "normal"。
source_dir: 会话目录(可选,传入时启用 SSE 流式事件写入)。
Returns:
包含 updated_fields, changes, confidence, unparsed_info 的字典。
"""
system_prompt = build_supplement_system_prompt()
parts = [
f"发票类型: {'差旅报销' if invoice_type == 'travel' else '普通报销'}",
"",
"以下是当前已提取的报销信息:",
json.dumps(extracted_info, ensure_ascii=False, indent=2),
"",
f"用户补充信息:{user_text}",
"",
"=== 请分析用户输入并返回需要更新的字段 ===",
]
user_message = "\n".join(parts)
try:
response = llm_query_text(
system_prompt=system_prompt,
text=user_message,
reasoning_effort="low",
source_dir=source_dir,
)
result = parse_json_response(response)
log.info("LLM 补充信息分析完成")
return result
except Exception as e:
log.error("LLM 补充信息分析失败: %s", e)
return {
"updated_fields": {},
"changes": [],
"confidence": 0.0,
"unparsed_info": f"分析失败: {e}",
}
def merge_supplement_into_info(
extracted_info: dict[str, Any],
updated_fields: dict[str, Any],
) -> dict[str, Any]:
"""将 LLM 返回的更新字段合并到已提取的信息中。
支持点号路径(如 basic_info.travel_purpose)表示嵌套更新。
Args:
extracted_info: 当前已提取的报销信息。
updated_fields: LLM 返回的需要更新的字段。
Returns:
更新后的报销信息。
"""
import copy
result = copy.deepcopy(extracted_info)
for field_path, value in updated_fields.items():
parts = field_path.split(".")
current = result
for part in parts[:-1]:
if part not in current:
current[part] = {}
current = current[part]
current[parts[-1]] = value
log.info("更新字段 %s = %s", field_path, value)
return result
+2 -2
View File
@@ -25,7 +25,7 @@ log = get_logger("validator")
# 日期格式
# ------------------------------------------------------------------
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
DATE_PATTERN = re.compile(r"^\d{4}-(?:0?[1-9]|1[0-2])-(?:0?[1-9]|[12]\d|3[01])$")
def _is_valid_date(value: str) -> bool:
@@ -95,7 +95,7 @@ class ValidationConfig(TypedDict):
# 配置加载
# ------------------------------------------------------------------
_CONFIG_PATH = Path(__file__).parent.parent.parent / "config" / "validation_rules.json"
_CONFIG_PATH = Path(__file__).parent.parent.parent.parent / "config" / "validation_rules.json"
_cached_rules: ValidationConfig | None = None