from __future__ import annotations import unittest from pathlib import Path from app.schemas.configuration import WorkflowTemplatesConfig from app.services.classifier import ClassificationResult from app.services.multi_intent_detector import MultiIntentCandidate, MultiIntentDetectionResult from app.services.planner import CompositeWorkflowPlanner, HeuristicWorkflowPlanner, TemplateWorkflowPlanner from app.services.intent_registry import IntentRegistry class FakeClauseClassifier: def __init__(self, predictions: dict[str, list[dict[str, float | str]]]) -> None: self._predictions = predictions def predict(self, text, intents): _ = intents return ClassificationResult( intent=None, score=0.0, model_name="fake-bert-clause", backend_name="fake-bert-clause", raw_candidates=self._predictions.get(text, []), ) class FakeMultiIntentDetector: def __init__(self, predictions: dict[str, list[tuple[str, float]]]) -> None: self._predictions = predictions def detect(self, text, intents): _ = intents candidates = [ MultiIntentCandidate(intent_id=intent_id, score=score, label=intent_id) for intent_id, score in self._predictions.get(text, []) ] return MultiIntentDetectionResult( detected=len(candidates) >= 2, candidates=candidates, reason="fake detector", backend_name="fake-multi", ) class WorkflowTemplateTests(unittest.TestCase): def setUp(self) -> None: self.registry = IntentRegistry.from_json("app/data/intents.json") self.templates = WorkflowTemplatesConfig.model_validate_json( Path("config/workflows.yml").read_text(encoding="utf-8") ) def test_template_planner_matches_sequence_template(self) -> None: planner = TemplateWorkflowPlanner(self.templates) result = planner.plan("打开车窗,然后把空调调到20度", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.backend, "local-template") self.assertEqual(result.workflow_type, "sequence") self.assertEqual([step.intent_id for step in result.steps], ["cabin_window_open", "cabin_set_ac"]) self.assertEqual(result.steps[1].slots.get("temperature"), 20) def test_template_planner_matches_conditional_template(self) -> None: planner = TemplateWorkflowPlanner(self.templates) result = planner.plan("查一下订单A123456,如果还没发货就取消", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.workflow_type, "conditional") self.assertEqual([step.intent_id for step in result.steps], ["cs_query_order", "cs_cancel_order"]) self.assertEqual(result.steps[1].depends_on, [1]) self.assertTrue(result.steps[1].requires_confirmation) def test_composite_planner_falls_back_to_heuristic_when_template_misses(self) -> None: planner = CompositeWorkflowPlanner([TemplateWorkflowPlanner(self.templates), HeuristicWorkflowPlanner()]) result = planner.plan("打开车窗,并且播放轻音乐", self.registry.list()) self.assertTrue(result.accepted) self.assertIn(result.backend, {"local-template", "local-heuristic"}) self.assertEqual(result.workflow_type, "sequence") def test_heuristic_planner_parses_ac_then_window_close_sequence(self) -> None: planner = HeuristicWorkflowPlanner() result = planner.plan("打开空调,再把窗户降下来", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.backend, "local-heuristic") self.assertEqual(result.workflow_type, "sequence") self.assertEqual([step.intent_id for step in result.steps], ["cabin_ac_on", "cabin_window_close"]) def test_planner_metadata_contains_clause_analysis(self) -> None: planner = HeuristicWorkflowPlanner() result = planner.plan("打开空调,然后打开车窗", self.registry.list()) self.assertTrue(result.accepted) self.assertTrue(result.metadata.get("multi_intent_detected")) clause_analysis = result.metadata.get("clause_analysis", []) self.assertEqual(len(clause_analysis), 2) self.assertEqual(clause_analysis[0].get("selected_intent_id"), "cabin_ac_on") self.assertEqual(clause_analysis[1].get("selected_intent_id"), "cabin_window_open") def test_heuristic_planner_supports_shared_action_parallel_objects(self) -> None: planner = HeuristicWorkflowPlanner() result = planner.plan("打开空调和车窗", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.workflow_type, "sequence") self.assertEqual([step.intent_id for step in result.steps], ["cabin_ac_on", "cabin_window_open"]) def test_heuristic_planner_supports_parallel_objects_with_suffix_action(self) -> None: planner = HeuristicWorkflowPlanner() result = planner.plan("把车窗和天窗打开", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.workflow_type, "sequence") self.assertEqual([step.intent_id for step in result.steps], ["cabin_window_open", "cabin_sunroof_open"]) def test_heuristic_planner_supports_parallel_clause_with_bing_connector(self) -> None: planner = HeuristicWorkflowPlanner() result = planner.plan("打开车窗并播放轻音乐", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.workflow_type, "sequence") self.assertEqual([step.intent_id for step in result.steps], ["cabin_window_open", "cabin_play_music"]) def test_heuristic_planner_can_use_clause_classifier_to_rescue_semantic_clause(self) -> None: planner = HeuristicWorkflowPlanner( clause_classifier=FakeClauseClassifier( { "车里太闷了": [ {"label": "cabin_window_open", "intent_id": "cabin_window_open", "score": 0.83}, ], "来点轻音乐": [ {"label": "cabin_play_music", "intent_id": "cabin_play_music", "score": 0.91}, ], } ) ) result = planner.plan("车里太闷了,然后来点轻音乐", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual(result.workflow_type, "sequence") self.assertEqual([step.intent_id for step in result.steps], ["cabin_window_open", "cabin_play_music"]) clause_analysis = result.metadata.get("clause_analysis", []) self.assertGreater(clause_analysis[0].get("candidates", [])[0].get("model_score", 0.0), 0.8) def test_heuristic_planner_can_use_multi_intent_detector_prior(self) -> None: planner = HeuristicWorkflowPlanner( clause_classifier=FakeClauseClassifier( { "来点轻音乐": [ {"label": "cabin_play_music", "intent_id": "cabin_play_music", "score": 0.91}, ], } ), multi_intent_detector=FakeMultiIntentDetector( { "顺便开下车窗,再来点轻音乐": [ ("cabin_window_open", 0.87), ("cabin_play_music", 0.82), ] } ), ) result = planner.plan("顺便开下车窗,再来点轻音乐", self.registry.list()) self.assertTrue(result.accepted) self.assertEqual([step.intent_id for step in result.steps], ["cabin_window_open", "cabin_play_music"]) detector_meta = result.metadata.get("multi_intent_detector") or {} self.assertTrue(detector_meta.get("detected")) self.assertEqual(len(detector_meta.get("candidates", [])), 2) if __name__ == "__main__": unittest.main()