feat(ai): add tenant-safe hierarchical expense learning

This commit is contained in:
caoxiaozhu
2026-07-16 14:30:41 +08:00
parent 6bdf65bc24
commit ee88a36baf
65 changed files with 6909 additions and 232 deletions

View File

@@ -1,9 +1,7 @@
from __future__ import annotations
from collections.abc import Generator
from datetime import datetime
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
@@ -55,6 +53,7 @@ def _observation(db: Session, key: str = "risk:c1:dup") -> RiskObservation:
)
db.flush()
obs = RiskObservation(
tenant_id="tenant-a",
observation_key=key,
subject_type="expense_claim",
subject_key="claim:c1",
@@ -91,6 +90,7 @@ def test_ingest_confirmed_persists_sample_and_calls_store() -> None:
)
assert sample is not None
assert sample.label == "confirmed"
assert sample.tenant_id == "tenant-a"
assert sample.sample_key == f"obs:{obs.id}"
assert "重复发票" in sample.case_text
assert "确认重复发票" in sample.conclusion_text
@@ -98,13 +98,38 @@ def test_ingest_confirmed_persists_sample_and_calls_store() -> None:
fake_store.upsert.assert_called_once()
def test_ingest_extracts_business_scene_and_rule_identity() -> None:
with _build_session() as db:
obs = _observation(db, key="risk:c1:identity")
obs.feedback_status = "confirmed"
obs.control_stage = "reimbursement"
obs.policy_refs_json = [{"rule_code": "TRAVEL-001"}]
obs.decision_trace_json = {"rule_version": "v2.3"}
service = FewShotIngestionService(db)
with patch.object(
service,
"_store",
return_value=MagicMock(upsert=MagicMock(return_value=None)),
):
sample = service.ingest_observation_feedback(
obs,
MagicMock(feedback_type="confirm", comment="确认", actor="audit"),
)
assert sample is not None
assert sample.scene == "expense_reimbursement"
assert sample.policy_ref == "TRAVEL-001"
assert sample.rule_version == "v2.3"
def test_ingest_false_positive_also_persisted() -> None:
with _build_session() as db:
obs = _observation(db, key="risk:c2:fp")
obs.feedback_status = "false_positive"
db.commit()
service = FewShotIngestionService(db)
with patch.object(service, "_store", return_value=MagicMock(upsert=MagicMock(return_value=None))):
with patch.object(
service, "_store", return_value=MagicMock(upsert=MagicMock(return_value=None))
):
sample = service.ingest_observation_feedback(
obs,
MagicMock(feedback_type="false_positive", comment="", actor="audit"),
@@ -164,6 +189,7 @@ def test_create_feedback_hook_triggers_ingestion() -> None:
service.create_feedback(
obs.observation_key,
RiskObservationFeedbackCreate(feedback_type="confirm", actor="audit"),
tenant_id="tenant-a",
)
assert len(ingest_calls) == 1
assert ingest_calls[0][1] == "confirm"
@@ -178,7 +204,10 @@ def test_create_feedback_hook_skipped_for_comment_feedback() -> None:
) as mock_ingest:
service.create_feedback(
obs.observation_key,
RiskObservationFeedbackCreate(feedback_type="comment", action="note", actor="audit"),
RiskObservationFeedbackCreate(
feedback_type="comment", action="note", actor="audit"
),
tenant_id="tenant-a",
)
mock_ingest.assert_not_called()
@@ -195,6 +224,7 @@ def test_create_feedback_hook_swallows_ingestion_failure() -> None:
feedback = service.create_feedback(
obs.observation_key,
RiskObservationFeedbackCreate(feedback_type="confirm", actor="audit"),
tenant_id="tenant-a",
)
assert feedback.feedback_type == "confirm"
@@ -210,5 +240,6 @@ def test_create_feedback_hook_respects_feature_flag(monkeypatch: pytest.MonkeyPa
service.create_feedback(
obs.observation_key,
RiskObservationFeedbackCreate(feedback_type="confirm", actor="audit"),
tenant_id="tenant-a",
)
mock_ingest.assert_not_called()