feat(server): 更新用户代理服务架构,增强用户行为追踪和会话管理功能,包含schema、service和单元测试
This commit is contained in:
@@ -46,7 +46,7 @@ def test_user_agent_query_returns_readable_answer_and_actions() -> None:
|
||||
assert len(response.suggested_actions) >= 1
|
||||
|
||||
|
||||
def test_user_agent_prefers_runtime_model_answer_when_available(monkeypatch) -> None:
|
||||
def test_user_agent_returns_readable_query_answer_when_runtime_model_is_skipped(monkeypatch) -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
@@ -56,11 +56,7 @@ def test_user_agent_prefers_runtime_model_answer_when_available(monkeypatch) ->
|
||||
)
|
||||
)
|
||||
service = UserAgentService(db)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_generate_answer_with_model",
|
||||
lambda *args, **kwargs: "这是模型回答",
|
||||
)
|
||||
monkeypatch.setattr(service, "_generate_answer_with_model", lambda *args, **kwargs: "这是模型回答")
|
||||
|
||||
response = service.respond(
|
||||
UserAgentRequest(
|
||||
@@ -72,7 +68,8 @@ def test_user_agent_prefers_runtime_model_answer_when_available(monkeypatch) ->
|
||||
)
|
||||
)
|
||||
|
||||
assert response.answer == "这是模型回答"
|
||||
assert "共 2 笔" in response.answer
|
||||
assert "8800.00" in response.answer
|
||||
|
||||
|
||||
def test_user_agent_sanitizes_model_thinking_blocks() -> None:
|
||||
@@ -144,7 +141,7 @@ def test_user_agent_guides_implicit_expense_draft_request() -> None:
|
||||
|
||||
assert response.review_payload is not None
|
||||
assert response.answer == response.review_payload.body_message
|
||||
assert response.review_payload.intent_summary.startswith("我理解你这次想报销业务招待费。")
|
||||
assert response.review_payload.intent_summary.startswith("识别到您希望报销一笔“业务招待费”费用。")
|
||||
assert response.review_payload.missing_slots == ["客户名称", "参与人员", "票据附件"]
|
||||
assert [item.action_type for item in response.review_payload.confirmation_actions] == [
|
||||
"cancel_review",
|
||||
@@ -187,7 +184,102 @@ def test_user_agent_guides_narrative_with_day_before_yesterday() -> None:
|
||||
slot_map = {item.key: item for item in response.review_payload.slot_cards}
|
||||
assert slot_map["time_range"].raw_value == "前天"
|
||||
assert slot_map["time_range"].value == "2026-05-11"
|
||||
assert "时间:2026-05-11" in response.review_payload.intent_summary
|
||||
assert "时间为 2026-05-11" in response.review_payload.intent_summary
|
||||
|
||||
|
||||
def test_user_agent_attachment_only_upload_uses_generic_scene_reason_without_fabrication() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我上传了 1 份票据,请结合附件名称给出报销建议并尽量生成草稿。",
|
||||
user_id="pytest",
|
||||
context_json={
|
||||
"attachment_names": ["didi-trip.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "didi-trip.png",
|
||||
"summary": "滴滴出行 订单金额 32 元",
|
||||
"text": "滴滴出行 订单金额 32 元",
|
||||
"document_type": "taxi_receipt",
|
||||
"scene_code": "transport",
|
||||
}
|
||||
],
|
||||
"user_input_text": "",
|
||||
},
|
||||
)
|
||||
)
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我上传了 1 份票据,请结合附件名称给出报销建议并尽量生成草稿。\n附件名称:didi-trip.png",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"attachment_names": ["didi-trip.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "didi-trip.png",
|
||||
"summary": "滴滴出行 订单金额 32 元",
|
||||
"text": "滴滴出行 订单金额 32 元",
|
||||
"document_type": "taxi_receipt",
|
||||
"scene_code": "transport",
|
||||
}
|
||||
],
|
||||
"user_input_text": "",
|
||||
},
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
slot_map = {item.key: item for item in response.review_payload.slot_cards}
|
||||
assert slot_map["reason"].value == "交通出行"
|
||||
assert slot_map["reason"].status == "inferred"
|
||||
|
||||
|
||||
def test_user_agent_transport_flow_infers_reason_and_does_not_require_location_or_merchant() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我上传了交通票据,帮我生成报销草稿",
|
||||
user_id="pytest",
|
||||
)
|
||||
)
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我上传了交通票据,帮我生成报销草稿",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"attachment_names": ["didi-trip.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "didi-trip.png",
|
||||
"summary": "滴滴出行 支付金额 32 元",
|
||||
"text": "滴滴出行 支付金额 32 元",
|
||||
"document_type": "taxi_receipt",
|
||||
"scene_code": "transport",
|
||||
"scene_label": "交通票据",
|
||||
}
|
||||
],
|
||||
},
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
slot_map = {item.key: item for item in response.review_payload.slot_cards}
|
||||
assert slot_map["reason"].value == "交通出行"
|
||||
assert slot_map["reason"].status == "inferred"
|
||||
assert "酒店/商户" not in response.review_payload.missing_slots
|
||||
assert "地点" not in response.review_payload.missing_slots
|
||||
assert "事由说明" not in response.review_payload.missing_slots
|
||||
|
||||
|
||||
def test_user_agent_risk_response_includes_rule_citations() -> None:
|
||||
@@ -347,7 +439,238 @@ def test_user_agent_builds_review_payload_for_multi_document_expense_flow() -> N
|
||||
"save_draft",
|
||||
]
|
||||
assert any(item.scene_label == "业务招待费" for item in response.review_payload.document_cards)
|
||||
assert f"时间:{yesterday}" in response.review_payload.intent_summary
|
||||
assert f"时间为 {yesterday}" in response.review_payload.intent_summary
|
||||
slot_map = {item.key: item for item in response.review_payload.slot_cards}
|
||||
assert slot_map["time_range"].value == yesterday
|
||||
assert slot_map["time_range"].raw_value == "昨天"
|
||||
|
||||
|
||||
def test_user_agent_sums_multi_document_amounts_from_synonym_fields() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我上传了两张交通票据,帮我生成报销草稿",
|
||||
user_id="pytest",
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png", "停车票.jpg"],
|
||||
"attachment_count": 2,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "滴滴行程单.png",
|
||||
"summary": "滴滴出行电子行程单",
|
||||
"text": "滴滴出行 订单金额 ¥32.50",
|
||||
"avg_score": 0.94,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "支付金额", "value": "32.50"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
{
|
||||
"filename": "停车票.jpg",
|
||||
"summary": "停车票",
|
||||
"text": "停车费 合计 18 元",
|
||||
"avg_score": 0.92,
|
||||
"document_fields": [
|
||||
{"key": "total_amount", "label": "合计金额", "value": "18"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我上传了两张交通票据,帮我生成报销草稿",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png", "停车票.jpg"],
|
||||
"attachment_count": 2,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "滴滴行程单.png",
|
||||
"summary": "滴滴出行电子行程单",
|
||||
"text": "滴滴出行 订单金额 ¥32.50",
|
||||
"avg_score": 0.94,
|
||||
"document_fields": [
|
||||
{"key": "amount", "label": "支付金额", "value": "32.50"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
{
|
||||
"filename": "停车票.jpg",
|
||||
"summary": "停车票",
|
||||
"text": "停车费 合计 18 元",
|
||||
"avg_score": 0.92,
|
||||
"document_fields": [
|
||||
{"key": "total_amount", "label": "合计金额", "value": "18"},
|
||||
],
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
},
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
slot_map = {item.key: item for item in response.review_payload.slot_cards}
|
||||
assert slot_map["amount"].value == "50.50元"
|
||||
document_field_labels = [
|
||||
field.label
|
||||
for card in response.review_payload.document_cards
|
||||
for field in card.fields
|
||||
]
|
||||
assert "金额" in document_field_labels
|
||||
|
||||
|
||||
def test_user_agent_prefers_larger_decimal_amount_from_ocr_text_candidates() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我上传了打车票据,帮我生成报销草稿",
|
||||
user_id="pytest",
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "滴滴行程单.png",
|
||||
"summary": "滴滴出行电子行程单",
|
||||
"text": "滴滴出行 支付金额 1 元,实付 13.4 元,订单号 12345678",
|
||||
"avg_score": 0.94,
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我上传了打车票据,帮我生成报销草稿",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "滴滴行程单.png",
|
||||
"summary": "滴滴出行电子行程单",
|
||||
"text": "滴滴出行 支付金额 1 元,实付 13.4 元,订单号 12345678",
|
||||
"avg_score": 0.94,
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
},
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
slot_map = {item.key: item for item in response.review_payload.slot_cards}
|
||||
assert slot_map["amount"].value == "13.40元"
|
||||
|
||||
|
||||
def test_user_agent_review_payload_keeps_document_preview_data() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我上传了打车票据,帮我生成报销草稿",
|
||||
user_id="pytest",
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "滴滴行程单.png",
|
||||
"summary": "滴滴出行电子行程单",
|
||||
"text": "滴滴出行 实付 13.4 元",
|
||||
"avg_score": 0.94,
|
||||
"preview_kind": "image",
|
||||
"preview_data_url": "data:image/png;base64,ZmFrZQ==",
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我上传了打车票据,帮我生成报销草稿",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png"],
|
||||
"attachment_count": 1,
|
||||
"ocr_documents": [
|
||||
{
|
||||
"filename": "滴滴行程单.png",
|
||||
"summary": "滴滴出行电子行程单",
|
||||
"text": "滴滴出行 实付 13.4 元",
|
||||
"avg_score": 0.94,
|
||||
"preview_kind": "image",
|
||||
"preview_data_url": "data:image/png;base64,ZmFrZQ==",
|
||||
"warnings": [],
|
||||
},
|
||||
],
|
||||
},
|
||||
tool_payload={"draft_only": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
assert response.review_payload.document_cards[0].preview_kind == "image"
|
||||
assert response.review_payload.document_cards[0].preview_data_url.startswith("data:image/png;base64,")
|
||||
|
||||
|
||||
def test_user_agent_prompts_existing_draft_association_choice_for_multi_documents() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我上传了两张票据,帮我生成报销草稿",
|
||||
user_id="pytest",
|
||||
)
|
||||
)
|
||||
|
||||
response = UserAgentService(db).respond(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我上传了两张票据,帮我生成报销草稿",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"attachment_names": ["滴滴行程单.png", "餐饮发票.jpg"],
|
||||
"attachment_count": 2,
|
||||
"ocr_documents": [
|
||||
{"filename": "滴滴行程单.png", "summary": "滴滴出行 金额 32 元", "text": "滴滴出行 金额 32 元"},
|
||||
{"filename": "餐饮发票.jpg", "summary": "餐饮发票 金额 68 元", "text": "餐饮发票 金额 68 元"},
|
||||
],
|
||||
},
|
||||
tool_payload={
|
||||
"pending_association_decision": True,
|
||||
"association_candidate_claim_no": "EXP-202605-008",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert response.review_payload is not None
|
||||
assert response.review_payload.can_proceed is False
|
||||
assert [item.action_type for item in response.review_payload.confirmation_actions] == [
|
||||
"cancel_review",
|
||||
"edit_review",
|
||||
"link_to_existing_draft",
|
||||
"create_new_claim_from_documents",
|
||||
]
|
||||
assert "EXP-202605-008" in response.answer
|
||||
|
||||
Reference in New Issue
Block a user