feat(ai): add tenant-safe hierarchical expense learning
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user