|
5 | 5 |
|
6 | 6 | import pytest |
7 | 7 |
|
8 | | -from src.tool_classifier.agentic_loop import AgenticLoop |
9 | | -from src.tool_classifier.enums import AgenticLoopStatus |
10 | | -from src.tool_classifier.param_extractor import ParamExtractionResult |
| 8 | +from tool_classifier.agentic_loop import AgenticLoop |
| 9 | +from tool_classifier.enums import AgenticLoopStatus |
| 10 | +from tool_classifier.param_extractor import ParamExtractionResult |
11 | 11 |
|
12 | 12 |
|
13 | 13 | # --------------------------------------------------------------------------- |
@@ -540,6 +540,7 @@ async def fake_to_thread(fn: Any, *args: Any, **kwargs: Any) -> Any: |
540 | 540 | _HISTORY, |
541 | 541 | {"validFrom": "2026-01-01"}, |
542 | 542 | "en", |
| 543 | + 1, |
543 | 544 | ) |
544 | 545 |
|
545 | 546 |
|
@@ -963,3 +964,134 @@ async def test_user_exit_during_stream_returns_empty_tokens(self) -> None: |
963 | 964 | assert tokens == [] |
964 | 965 | # Collected params returned unchanged on exit |
965 | 966 | assert result.collected_params == {"validFrom": "2026-01-01"} |
| 967 | + |
| 968 | + |
| 969 | +# --------------------------------------------------------------------------- |
| 970 | +# seeded_params — L2 param_update pre-population at turn 0 |
| 971 | +# --------------------------------------------------------------------------- |
| 972 | + |
| 973 | + |
| 974 | +class TestSeededParamsTurn0: |
| 975 | + """Verify that seeded_params from L2 follow-up routing are merged into |
| 976 | + collected_params at turn 0 only, with collected_params taking priority.""" |
| 977 | + |
| 978 | + @pytest.mark.asyncio |
| 979 | + async def test_seeded_params_merged_at_turn_0(self) -> None: |
| 980 | + """seeded_params are prepended to collected_params when turn_count=0.""" |
| 981 | + # Extractor returns only validFrom as newly extracted; countryIsoCode comes |
| 982 | + # from seeded_params. |
| 983 | + extractor_mock = _make_extractor_mock( |
| 984 | + _extraction( |
| 985 | + {"validFrom": "2026-01-01"}, |
| 986 | + [], # nothing missing — both params will be present after seed merge |
| 987 | + "none", |
| 988 | + ) |
| 989 | + ) |
| 990 | + loop = _make_loop(extractor_mock) |
| 991 | + |
| 992 | + result = await loop.run_turn( |
| 993 | + chat_id=_CHAT_ID, |
| 994 | + user_message="January 2026", |
| 995 | + conversation_history=[], |
| 996 | + params_schema=_SCHEMA_TWO_REQUIRED, |
| 997 | + collected_params={}, |
| 998 | + turn_count=0, |
| 999 | + seeded_params={"countryIsoCode": "EE"}, |
| 1000 | + ) |
| 1001 | + |
| 1002 | + # Both params present → COMPLETED |
| 1003 | + assert result.status == AgenticLoopStatus.COMPLETED |
| 1004 | + assert result.collected_params.get("countryIsoCode") == "EE" |
| 1005 | + assert result.collected_params.get("validFrom") == "2026-01-01" |
| 1006 | + |
| 1007 | + @pytest.mark.asyncio |
| 1008 | + async def test_collected_params_override_seeded_params(self) -> None: |
| 1009 | + """collected_params values beat seeded_params when the key overlaps.""" |
| 1010 | + extractor_mock = _make_extractor_mock( |
| 1011 | + _extraction( |
| 1012 | + {"validFrom": "2026-06-01"}, |
| 1013 | + [], |
| 1014 | + "none", |
| 1015 | + ) |
| 1016 | + ) |
| 1017 | + loop = _make_loop(extractor_mock) |
| 1018 | + |
| 1019 | + result = await loop.run_turn( |
| 1020 | + chat_id=_CHAT_ID, |
| 1021 | + user_message="June 2026", |
| 1022 | + conversation_history=[], |
| 1023 | + params_schema=_SCHEMA_TWO_REQUIRED, |
| 1024 | + collected_params={"countryIsoCode": "LV"}, # explicit value takes priority |
| 1025 | + turn_count=0, |
| 1026 | + seeded_params={"countryIsoCode": "EE"}, # seeded value must be overridden |
| 1027 | + ) |
| 1028 | + |
| 1029 | + assert result.collected_params.get("countryIsoCode") == "LV" |
| 1030 | + |
| 1031 | + @pytest.mark.asyncio |
| 1032 | + async def test_seeded_params_not_applied_on_subsequent_turns(self) -> None: |
| 1033 | + """seeded_params are ignored when turn_count > 0.""" |
| 1034 | + extractor_mock = _make_extractor_mock( |
| 1035 | + _extraction({}, ["countryIsoCode", "validFrom"], "Which country and date?") |
| 1036 | + ) |
| 1037 | + loop = _make_loop(extractor_mock) |
| 1038 | + |
| 1039 | + result = await loop.run_turn( |
| 1040 | + chat_id=_CHAT_ID, |
| 1041 | + user_message="hello", |
| 1042 | + conversation_history=[], |
| 1043 | + params_schema=_SCHEMA_TWO_REQUIRED, |
| 1044 | + collected_params={}, |
| 1045 | + turn_count=1, # NOT turn 0 → seeded_params must be ignored |
| 1046 | + seeded_params={"countryIsoCode": "EE", "validFrom": "2026-01-01"}, |
| 1047 | + ) |
| 1048 | + |
| 1049 | + # Even though seeded_params would satisfy all required params, they should |
| 1050 | + # not be applied after turn 0 → still NEEDS_INPUT |
| 1051 | + assert result.status == AgenticLoopStatus.NEEDS_INPUT |
| 1052 | + # seeded values not present in collected_params |
| 1053 | + assert "countryIsoCode" not in result.collected_params |
| 1054 | + assert "validFrom" not in result.collected_params |
| 1055 | + |
| 1056 | + @pytest.mark.asyncio |
| 1057 | + async def test_seeded_params_none_does_not_raise(self) -> None: |
| 1058 | + """Passing seeded_params=None (default) at turn 0 behaves normally.""" |
| 1059 | + extractor_mock = _make_extractor_mock( |
| 1060 | + _extraction({}, ["countryIsoCode", "validFrom"], "Which country?") |
| 1061 | + ) |
| 1062 | + loop = _make_loop(extractor_mock) |
| 1063 | + |
| 1064 | + result = await loop.run_turn( |
| 1065 | + chat_id=_CHAT_ID, |
| 1066 | + user_message="hello", |
| 1067 | + conversation_history=[], |
| 1068 | + params_schema=_SCHEMA_TWO_REQUIRED, |
| 1069 | + collected_params={}, |
| 1070 | + turn_count=0, |
| 1071 | + seeded_params=None, |
| 1072 | + ) |
| 1073 | + |
| 1074 | + assert result.status == AgenticLoopStatus.NEEDS_INPUT |
| 1075 | + |
| 1076 | + @pytest.mark.asyncio |
| 1077 | + async def test_seeded_params_partial_fill_still_asks_for_missing(self) -> None: |
| 1078 | + """seeded_params satisfy only one of two required params → still NEEDS_INPUT.""" |
| 1079 | + extractor_mock = _make_extractor_mock( |
| 1080 | + _extraction({}, ["validFrom"], "From which date?") |
| 1081 | + ) |
| 1082 | + loop = _make_loop(extractor_mock) |
| 1083 | + |
| 1084 | + result = await loop.run_turn( |
| 1085 | + chat_id=_CHAT_ID, |
| 1086 | + user_message="Estonia", |
| 1087 | + conversation_history=[], |
| 1088 | + params_schema=_SCHEMA_TWO_REQUIRED, |
| 1089 | + collected_params={}, |
| 1090 | + turn_count=0, |
| 1091 | + seeded_params={"countryIsoCode": "EE"}, # only one param seeded |
| 1092 | + ) |
| 1093 | + |
| 1094 | + # validFrom still missing → NEEDS_INPUT |
| 1095 | + assert result.status == AgenticLoopStatus.NEEDS_INPUT |
| 1096 | + # But seeded countryIsoCode should be present |
| 1097 | + assert result.collected_params.get("countryIsoCode") == "EE" |
0 commit comments