"""Chat intent classifier tests: rule fallback, LLM client, fallback composition.""" from __future__ import annotations from typing import Any from app.chat_intents import CHAT_INTENT_REGISTRY_VERSION, ChatIntent, ChatTurnClassification from app.chat_intent_classifier import ( ChatIntentClassifier, FallbackChatIntentClassifier, LLMChatIntentClassifier, RuleBasedChatIntentClassifier, build_chat_intent_classifier, build_chat_state_summary, ) from app.settings import Settings PROFILE = { "job_type": "campus", "target_position": "后端工程师", "resume_content": { "sections": [ {"kind": "project_experience", "items": [{"project_name": "AI Career Copilot"}]}, ] }, } SUMMARY = build_chat_state_summary(PROFILE, {"draft": {"section": "education"}}) def classify_rule(message: str) -> ChatTurnClassification: return RuleBasedChatIntentClassifier().classify(message, state_summary=SUMMARY) def test_rule_classifier_implements_protocol() -> None: assert isinstance(RuleBasedChatIntentClassifier(), ChatIntentClassifier) def test_rule_classifier_maps_legacy_keyword_signals() -> None: assert classify_rule("没有").intent is ChatIntent.NO_INFO revise = classify_rule("保留原文,不要用这版优化稿") assert revise.intent is ChatIntent.REVISE_PROPOSAL assert revise.revision_instruction assert classify_rule("把学校名字改成东莞城市学院").intent is ChatIntent.EDIT_IDENTITY new_entry = classify_rule("新增一段教育经历") assert new_entry.intent is ChatIntent.NEW_ENTRY assert new_entry.target_section == "education" def test_rule_classifier_routes_edit_question_chitchat_and_facts() -> None: edit = classify_rule("修改一下我之前写的那个 AI Career Copilot 项目经历") assert edit.intent is ChatIntent.EDIT_ENTRY assert edit.target_entry_hint == "AI Career Copilot" question = classify_rule("这段经历怎么写比较好?") assert question.intent is ChatIntent.ASK_QUESTION assert question.user_question assert classify_rule("好的,谢谢").intent is ChatIntent.CHITCHAT facts = classify_rule("负责后端接口开发,使用 Python 和 FastAPI") assert facts.intent is ChatIntent.PROVIDE_FACTS assert facts.facts and facts.facts[0].text assert facts.confidence < 0.5 def test_state_summary_compacts_profile_and_draft() -> None: assert SUMMARY["job_type"] == "campus" assert SUMMARY["target_position"] == "后端工程师" assert SUMMARY["confirmed_entries"] == [ {"section": "project_experience", "label": "AI Career Copilot"} ] assert SUMMARY["draft_section"] == "education" assert build_chat_state_summary(PROFILE)["draft_section"] is None class _StubClient: def __init__(self, result: Any) -> None: self.result = result self.calls: list[dict[str, Any]] = [] def complete(self, **kwargs: Any) -> Any: self.calls.append(kwargs) return self.result def test_llm_classifier_uses_registry_prompt_and_schema() -> None: expected = ChatTurnClassification(intent="ask_question", user_question="怎么写?") client = _StubClient(expected) result = LLMChatIntentClassifier(client).classify("怎么写?", state_summary=SUMMARY) assert result is expected call = client.calls[0] assert call["schema"] is ChatTurnClassification assert call["schema_name"] == "chat_intent_classification" assert call["payload"]["message"] == "怎么写?" assert call["payload"]["state_summary"] is SUMMARY assert call["payload"]["registry_version"] == CHAT_INTENT_REGISTRY_VERSION prompt = call["system_prompt"] assert CHAT_INTENT_REGISTRY_VERSION in prompt for intent in ChatIntent: assert intent.value in prompt assert "保留原文" in prompt # few-shot examples reach the prompt class _FailingClassifier: def classify(self, message: str, *, state_summary: dict[str, Any]) -> ChatTurnClassification: raise RuntimeError("boom") def test_fallback_classifier_degrades_to_rules_on_llm_failure() -> None: classifier = FallbackChatIntentClassifier(_FailingClassifier(), RuleBasedChatIntentClassifier()) result = classifier.classify("没有", state_summary=SUMMARY) assert result.intent is ChatIntent.NO_INFO def test_factory_returns_rules_without_openai_and_fallback_with_openai() -> None: rule_only = build_chat_intent_classifier(Settings(llm_provider="rule")) assert isinstance(rule_only, RuleBasedChatIntentClassifier) composed = build_chat_intent_classifier(Settings(llm_provider="openai", openai_api_key="k")) assert isinstance(composed, FallbackChatIntentClassifier) assert isinstance(composed.fallback, RuleBasedChatIntentClassifier)