refactor: 架构重组 — doc/bot → core/infra,新增 Agent 调度模块
- src/doc/ 拆分为 src/core/extraction/, matching/, validation/(核心业务逻辑) - src/bot/ 重命名为 src/infra/browser/(浏览器自动化基础设施) - fill_consumable_doc.py → src/infra/documents/consumable.py - 新增 Agent 调度模块:coordinator.py, events.py, session.py,重构 orchestrator.py - 更新 AGENTS.md、README.md 及所有子目录 README
This commit is contained in:
@@ -12,7 +12,7 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from src import exceptions
|
||||
from src.doc.extractor import extract_invoices
|
||||
from src.core.extraction import extract_invoices
|
||||
|
||||
# 字段键名(与源码中的字符串字面量保持一致)
|
||||
K_INVOICE_TYPE = "invoice_type"
|
||||
@@ -78,7 +78,7 @@ class TestExtractInvoices:
|
||||
"""extract_invoices 编排函数"""
|
||||
|
||||
def test_empty_directory(self, tmp_path: Path, monkeypatch):
|
||||
monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [])
|
||||
monkeypatch.setattr("src.core.extraction.extractor._find_all_files", lambda d: [])
|
||||
|
||||
records, apps, groups = extract_invoices(str(tmp_path))
|
||||
assert records == []
|
||||
@@ -89,8 +89,8 @@ class TestExtractInvoices:
|
||||
pdf = tmp_path / "broken.pdf"
|
||||
pdf.touch()
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [pdf])
|
||||
monkeypatch.setattr("src.doc.extractor._extract_document", lambda p, c, s: (None, "parse error"))
|
||||
monkeypatch.setattr("src.core.extraction.extractor._find_all_files", lambda d: [pdf])
|
||||
monkeypatch.setattr("src.core.extraction.extractor._extract_document", lambda p, c, s: (None, "parse error"))
|
||||
|
||||
with pytest.raises(exceptions.ExtractionError) as exc_info:
|
||||
extract_invoices(str(tmp_path))
|
||||
@@ -107,16 +107,14 @@ class TestExtractInvoices:
|
||||
inv1 = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
|
||||
inv2 = _make_invoice("INV002", 200.0, INVOICE_TYPE_GENERAL)
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._find_all_files", lambda d: [pdf1, pdf2])
|
||||
monkeypatch.setattr("src.core.extraction.extractor._find_all_files", lambda d: [pdf1, pdf2])
|
||||
|
||||
call_index = [0]
|
||||
path_to_result = {pdf1: inv1, pdf2: inv2}
|
||||
|
||||
def fake_extract(path, cache_dir, source_dir):
|
||||
idx = call_index[0]
|
||||
call_index[0] += 1
|
||||
return (inv1, None) if idx == 0 else (inv2, None)
|
||||
return (path_to_result[path], None)
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
|
||||
monkeypatch.setattr("src.core.extraction.extractor._extract_document", fake_extract)
|
||||
|
||||
def fake_match(invoices, cards):
|
||||
return [
|
||||
@@ -131,7 +129,7 @@ class TestExtractInvoices:
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
|
||||
monkeypatch.setattr("src.core.matching.matcher.match_invoices_to_cards", fake_match)
|
||||
|
||||
records, apps, groups = extract_invoices(str(tmp_path))
|
||||
|
||||
@@ -153,19 +151,17 @@ class TestExtractInvoices:
|
||||
inv_general = _make_invoice("GEN001", 150.0, INVOICE_TYPE_GENERAL)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.doc.extractor._find_all_files",
|
||||
"src.core.extraction.extractor._find_all_files",
|
||||
lambda d: [pdf1, pdf2, pdf3],
|
||||
)
|
||||
|
||||
invoices_list = [inv_train, inv_hotel, inv_general]
|
||||
call_index = [0]
|
||||
path_to_result = dict(zip([pdf1, pdf2, pdf3], invoices_list, strict=True))
|
||||
|
||||
def fake_extract(path, cache_dir, source_dir):
|
||||
idx = call_index[0]
|
||||
call_index[0] += 1
|
||||
return (invoices_list[idx], None)
|
||||
return (path_to_result[path], None)
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
|
||||
monkeypatch.setattr("src.core.extraction.extractor._extract_document", fake_extract)
|
||||
|
||||
def fake_match(invoices, cards):
|
||||
return [
|
||||
@@ -198,7 +194,7 @@ class TestExtractInvoices:
|
||||
},
|
||||
]
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
|
||||
monkeypatch.setattr("src.core.matching.matcher.match_invoices_to_cards", fake_match)
|
||||
|
||||
records, apps, groups = extract_invoices(str(tmp_path))
|
||||
|
||||
@@ -221,19 +217,16 @@ class TestExtractInvoices:
|
||||
app = _make_application()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.doc.extractor._find_all_files",
|
||||
"src.core.extraction.extractor._find_all_files",
|
||||
lambda d: [pdf1, pdf2],
|
||||
)
|
||||
|
||||
results = [inv, app]
|
||||
call_index = [0]
|
||||
path_to_result = {pdf1: inv, pdf2: app}
|
||||
|
||||
def fake_extract(path, cache_dir, source_dir):
|
||||
idx = call_index[0]
|
||||
call_index[0] += 1
|
||||
return (results[idx], None)
|
||||
return (path_to_result[path], None)
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
|
||||
monkeypatch.setattr("src.core.extraction.extractor._extract_document", fake_extract)
|
||||
|
||||
def fake_match(invoices, cards):
|
||||
return [
|
||||
@@ -248,7 +241,7 @@ class TestExtractInvoices:
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
|
||||
monkeypatch.setattr("src.core.matching.matcher.match_invoices_to_cards", fake_match)
|
||||
|
||||
records, apps, groups = extract_invoices(str(tmp_path))
|
||||
|
||||
@@ -266,19 +259,16 @@ class TestExtractInvoices:
|
||||
card = _make_card(300.0)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.doc.extractor._find_all_files",
|
||||
"src.core.extraction.extractor._find_all_files",
|
||||
lambda d: [pdf1, pdf2],
|
||||
)
|
||||
|
||||
results = [inv, card]
|
||||
call_index = [0]
|
||||
path_to_result = {pdf1: inv, pdf2: card}
|
||||
|
||||
def fake_extract(path, cache_dir, source_dir):
|
||||
idx = call_index[0]
|
||||
call_index[0] += 1
|
||||
return (results[idx], None)
|
||||
return (path_to_result[path], None)
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
|
||||
monkeypatch.setattr("src.core.extraction.extractor._extract_document", fake_extract)
|
||||
|
||||
def fake_match(invoices, cards):
|
||||
return [
|
||||
@@ -293,7 +283,7 @@ class TestExtractInvoices:
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
|
||||
monkeypatch.setattr("src.core.matching.matcher.match_invoices_to_cards", fake_match)
|
||||
|
||||
records, apps, groups = extract_invoices(str(tmp_path))
|
||||
|
||||
@@ -309,19 +299,17 @@ class TestExtractInvoices:
|
||||
inv = _make_invoice("INV001", 300.0, INVOICE_TYPE_GENERAL)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.doc.extractor._find_all_files",
|
||||
"src.core.extraction.extractor._find_all_files",
|
||||
lambda d: [pdf1, pdf2],
|
||||
)
|
||||
|
||||
results = [inv, None]
|
||||
call_index = [0]
|
||||
path_to_result = {pdf1: inv, pdf2: None}
|
||||
|
||||
def fake_extract(path, cache_dir, source_dir):
|
||||
idx = call_index[0]
|
||||
call_index[0] += 1
|
||||
return (results[idx], "parse error") if results[idx] is None else (results[idx], None)
|
||||
result = path_to_result[path]
|
||||
return (result, "parse error") if result is None else (result, None)
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor._extract_document", fake_extract)
|
||||
monkeypatch.setattr("src.core.extraction.extractor._extract_document", fake_extract)
|
||||
|
||||
def fake_match(invoices, cards):
|
||||
return [
|
||||
@@ -336,7 +324,7 @@ class TestExtractInvoices:
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr("src.doc.extractor.match_invoices_to_cards", fake_match)
|
||||
monkeypatch.setattr("src.core.matching.matcher.match_invoices_to_cards", fake_match)
|
||||
|
||||
records, apps, groups = extract_invoices(str(tmp_path))
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
覆盖发票分类、CSV 列定义、常量校验。
|
||||
"""
|
||||
|
||||
from src.doc.invoice import (
|
||||
from src.infra.documents import (
|
||||
INVOICE_LEVEL_COLUMNS,
|
||||
PAYMENT_RECORD_COLUMNS,
|
||||
classify_invoice_batch,
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.doc.llm_extractor import (
|
||||
from src.core.extraction import (
|
||||
_image_to_base64,
|
||||
extract_document,
|
||||
parse_json_response,
|
||||
@@ -131,7 +131,7 @@ class TestExtractDocument:
|
||||
def fake_query(system_prompt, text, image_b64, max_tokens=4096):
|
||||
return response_text
|
||||
|
||||
monkeypatch.setattr("src.doc.llm_extractor._llm_query_multimodal", fake_query)
|
||||
monkeypatch.setattr("src.core.extraction.llm_extractor._llm_query_multimodal", fake_query)
|
||||
|
||||
def test_success(self, tmp_path: Path, monkeypatch):
|
||||
img_path = tmp_path / "card.png"
|
||||
@@ -156,7 +156,7 @@ class TestExtractDocument:
|
||||
def fake_query(system_prompt, text, image_b64, max_tokens=4096):
|
||||
raise RuntimeError("模型不可用")
|
||||
|
||||
monkeypatch.setattr("src.doc.llm_extractor._llm_query_multimodal", fake_query)
|
||||
monkeypatch.setattr("src.core.extraction.llm_extractor._llm_query_multimodal", fake_query)
|
||||
with pytest.raises(RuntimeError, match="模型不可用"):
|
||||
extract_document(img_path)
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestExtractDocument:
|
||||
received_b64 = image_b64s
|
||||
return json.dumps({K_CARD_DATE: "2026-01-01", K_CARD_NO: "0000", K_CARD_AMOUNT: "100"})
|
||||
|
||||
monkeypatch.setattr("src.doc.llm_extractor._llm_query_multimodal", capture_b64)
|
||||
monkeypatch.setattr("src.core.extraction.llm_extractor._llm_query_multimodal", capture_b64)
|
||||
extract_document(img_path)
|
||||
|
||||
assert received_b64 is not None
|
||||
|
||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src.doc.matcher import (
|
||||
from src.core.matching import (
|
||||
_build_invoice_summary,
|
||||
_build_payment_records,
|
||||
_invoices_to_records,
|
||||
|
||||
Reference in New Issue
Block a user