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:
wandering
2026-07-02 18:36:19 +08:00
parent 7c137c5214
commit 1b35f07fd7
69 changed files with 2965 additions and 1548 deletions

View File

@@ -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))

View File

@@ -3,7 +3,7 @@
覆盖发票分类、CSV 列定义、常量校验。
"""
from src.doc.invoice import (
from src.infra.documents import (
INVOICE_LEVEL_COLUMNS,
PAYMENT_RECORD_COLUMNS,
classify_invoice_batch,

View File

@@ -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

View File

@@ -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,