generated from kgod/ai-review-template
自内部仓库剥离深度优化与 RAG 知识库后的交付版本: - Builder 对话式简历生成(FSM + 意图路由 LLM 兜底增强) - 条目级轻度优化:事实覆盖门禁 + STAR/bullet 修复链,功能/简介/成果与技术栈同级保护 - 简历导入:DOCX/PDF 解析、结构归一、手机号脱敏 - PostgreSQL 运行时 + Alembic 迁移链 Co-Authored-By: Claude <noreply@anthropic.com>
133 lines
5.1 KiB
Python
133 lines
5.1 KiB
Python
"""B-Step1c: intent router settings, shadow logger, and Builder flow mount (P0 observe-only)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from app.chat_intent_classifier import RuleBasedChatIntentClassifier
|
|
from app.chat_intent_shadow import (
|
|
ChatIntentShadowLogger,
|
|
build_chat_intent_shadow,
|
|
)
|
|
from app.chat_intents import ChatTurnClassification
|
|
from app.settings import Settings, load_settings
|
|
from builder_flow_helpers import create_builder_session, send_message
|
|
|
|
|
|
def test_intent_router_settings_defaults() -> None:
|
|
settings = Settings()
|
|
assert settings.intent_router_mode == "off"
|
|
assert settings.intent_model is None
|
|
|
|
|
|
def test_intent_router_settings_from_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
|
|
monkeypatch.setenv("RESUME_AGENT_INTENT_ROUTER_MODE", "shadow")
|
|
monkeypatch.setenv("RESUME_AGENT_INTENT_MODEL", "kimi-k3")
|
|
settings = load_settings(tmp_path / "missing.env")
|
|
assert settings.intent_router_mode == "shadow"
|
|
assert settings.intent_model == "kimi-k3"
|
|
monkeypatch.setenv("RESUME_AGENT_INTENT_ROUTER_MODE", "bogus")
|
|
with pytest.raises(ValueError, match="INTENT_ROUTER_MODE"):
|
|
load_settings(tmp_path / "missing.env")
|
|
|
|
|
|
class _StubClassifier:
|
|
def __init__(self, result: ChatTurnClassification | None = None, exc: Exception | None = None) -> None:
|
|
self.result = result
|
|
self.exc = exc
|
|
|
|
def classify(self, message: str, *, state_summary: dict[str, Any]) -> ChatTurnClassification:
|
|
if self.exc:
|
|
raise self.exc
|
|
assert self.result is not None
|
|
return self.result
|
|
|
|
|
|
def _captured_events(monkeypatch: pytest.MonkeyPatch) -> list[tuple[str, dict[str, Any]]]:
|
|
events: list[tuple[str, dict[str, Any]]] = []
|
|
monkeypatch.setattr(
|
|
"app.chat_intent_shadow.log_ai_event",
|
|
lambda event, **fields: events.append((event, fields)),
|
|
)
|
|
return events
|
|
|
|
|
|
def test_shadow_logs_llm_vs_rule_disagreement(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
events = _captured_events(monkeypatch)
|
|
shadow = ChatIntentShadowLogger(
|
|
_StubClassifier(ChatTurnClassification(intent="ask_question", confidence=0.9)),
|
|
RuleBasedChatIntentClassifier(),
|
|
)
|
|
shadow.observe("没有", state_summary={})
|
|
assert events == [
|
|
(
|
|
"chat_intent_shadow",
|
|
{
|
|
"registry_version": "1",
|
|
"rule_intent": "no_info",
|
|
"llm_intent": "ask_question",
|
|
"llm_confidence": 0.9,
|
|
"disagreement": True,
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def test_shadow_logs_agreement_and_survives_llm_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
events = _captured_events(monkeypatch)
|
|
agree = ChatIntentShadowLogger(
|
|
_StubClassifier(ChatTurnClassification(intent="no_info")),
|
|
RuleBasedChatIntentClassifier(),
|
|
)
|
|
agree.observe("没有", state_summary={})
|
|
assert events[0][1]["disagreement"] is False
|
|
|
|
failing = ChatIntentShadowLogger(
|
|
_StubClassifier(exc=RuntimeError("boom")),
|
|
RuleBasedChatIntentClassifier(),
|
|
)
|
|
failing.observe("没有", state_summary={}) # must not raise
|
|
assert events[1][0] == "chat_intent_shadow_error"
|
|
|
|
|
|
def test_build_chat_intent_shadow_requires_shadow_mode_and_openai() -> None:
|
|
openai = {"llm_provider": "openai", "openai_api_key": "k"}
|
|
assert build_chat_intent_shadow(Settings(**openai, intent_router_mode="off")) is None
|
|
assert build_chat_intent_shadow(Settings(llm_provider="rule", intent_router_mode="shadow")) is None
|
|
shadow = build_chat_intent_shadow(Settings(**openai, intent_router_mode="shadow"))
|
|
assert isinstance(shadow, ChatIntentShadowLogger)
|
|
tuned = build_chat_intent_shadow(Settings(**openai, intent_router_mode="shadow", intent_model="kimi-k3"))
|
|
assert tuned is not None
|
|
assert tuned.primary._client.settings.openai_model == "kimi-k3"
|
|
|
|
|
|
class _Spy:
|
|
def __init__(self, *, raises: bool = False) -> None:
|
|
self.raises = raises
|
|
self.calls: list[dict[str, Any]] = []
|
|
|
|
def observe(self, message: str, *, state_summary: dict[str, Any]) -> None:
|
|
if self.raises:
|
|
raise RuntimeError("spy boom")
|
|
self.calls.append({"message": message, "state_summary": state_summary})
|
|
|
|
|
|
def test_process_message_notifies_mounted_shadow(client: Any, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
agent = client.app.state.resume_agent
|
|
spy = _Spy()
|
|
monkeypatch.setattr(agent, "_chat_intent_shadow", spy, raising=False)
|
|
session_id, _ = create_builder_session(client)
|
|
send_message(client, session_id, "新增一段教育经历")
|
|
assert spy.calls[0]["message"] == "新增一段教育经历"
|
|
assert "confirmed_entries" in spy.calls[0]["state_summary"]
|
|
|
|
|
|
def test_shadow_failure_never_breaks_routing(client: Any, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
agent = client.app.state.resume_agent
|
|
monkeypatch.setattr(agent, "_chat_intent_shadow", _Spy(raises=True), raising=False)
|
|
session_id, _ = create_builder_session(client)
|
|
body = send_message(client, session_id, "新增一段教育经历")
|
|
assert body["turn"]["role"] == "assistant"
|