ummaya 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ummaya +76 -31
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -2
- package/src/ummaya/engine/engine.py +17 -34
- package/src/ummaya/engine/models.py +10 -4
- package/src/ummaya/engine/query.py +108 -5
- package/src/ummaya/ipc/stdio.py +576 -50
- package/src/ummaya/tools/executor.py +52 -6
- package/src/ummaya/tools/hira/hospital_search.py +15 -2
- package/src/ummaya/tools/location_adapters.py +25 -1
- package/src/ummaya/tools/mvp_surface.py +47 -46
- package/src/ummaya/tools/nmc/emergency_search.py +53 -1
- package/src/ummaya/tools/search.py +63 -2
- package/tui/package.json +1 -1
- package/tui/src/query.ts +1 -1
- package/tui/src/services/api/claude.ts +52 -3
- package/tui/src/tools/AdapterTool/AdapterTool.ts +262 -4
- package/tui/src/tools/LookupPrimitive/prompt.ts +19 -30
- package/tui/src/tools/ResolveLocationPrimitive/prompt.ts +12 -10
- package/tui/src/tools/SubmitPrimitive/prompt.ts +10 -6
- package/tui/src/tools/VerifyPrimitive/prompt.ts +10 -5
- package/uv.lock +1 -1
package/src/ummaya/ipc/stdio.py
CHANGED
|
@@ -66,6 +66,7 @@ _tracer = trace.get_tracer(__name__)
|
|
|
66
66
|
_BACKGROUND_FRAME_KINDS: frozenset[str] = frozenset(
|
|
67
67
|
{"chat_request", "user_input", "tool_call", "plugin_op"}
|
|
68
68
|
)
|
|
69
|
+
_ROOT_PRIMITIVE_TOOL_IDS: Final[frozenset[str]] = frozenset({"locate", "find", "check", "send"})
|
|
69
70
|
_SCOPE_ENTRY_RE = re.compile(r"^(find|send|check):[a-z0-9_]+\.[a-z0-9_-]+$")
|
|
70
71
|
_TOOL_ID_SCOPE_RE = re.compile(r"^(?P<verb>find|send|check):(?P<tool_id>[a-z][a-z0-9_]*[a-z0-9])$")
|
|
71
72
|
_LEGACY_SCOPE_VERB_ALIASES: Final[dict[str, str]] = {
|
|
@@ -187,6 +188,21 @@ _PRIMITIVE_ERROR_REASONS: Final[frozenset[str]] = frozenset(
|
|
|
187
188
|
"verify_tool_choice_mismatch",
|
|
188
189
|
}
|
|
189
190
|
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _should_append_tui_tool_to_llm_tools(
|
|
194
|
+
tui_name: object | None,
|
|
195
|
+
backend_tool_names: set[object],
|
|
196
|
+
*,
|
|
197
|
+
has_concrete_backend_tools: bool,
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""Return whether a TUI-sent tool should remain in the model tool list."""
|
|
200
|
+
if tui_name and tui_name in backend_tool_names:
|
|
201
|
+
return False
|
|
202
|
+
_ = has_concrete_backend_tools
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
|
|
190
206
|
_VERIFY_QUERY_REQUIREMENTS: Final[tuple[tuple[tuple[str, ...], dict[str, str]], ...]] = (
|
|
191
207
|
(
|
|
192
208
|
("간편인증", "pass 인증", "kakao 인증", "naver 인증"),
|
|
@@ -1535,6 +1551,19 @@ def _primitive_payload_is_success(payload: object, *, primitive: str) -> bool:
|
|
|
1535
1551
|
return True
|
|
1536
1552
|
|
|
1537
1553
|
|
|
1554
|
+
def _primitive_payload_result_dict(payload: object) -> dict[str, object] | None:
|
|
1555
|
+
"""Extract the primitive result object from direct or wrapped payloads."""
|
|
1556
|
+
if not isinstance(payload, dict):
|
|
1557
|
+
return None
|
|
1558
|
+
result = payload.get("result")
|
|
1559
|
+
if not isinstance(result, dict):
|
|
1560
|
+
return None
|
|
1561
|
+
nested_result = result.get("result")
|
|
1562
|
+
if result.get("ok") is True and isinstance(nested_result, dict):
|
|
1563
|
+
return cast("dict[str, object]", nested_result)
|
|
1564
|
+
return cast("dict[str, object]", result)
|
|
1565
|
+
|
|
1566
|
+
|
|
1538
1567
|
def _canonical_primitive_args(args: dict[str, object]) -> str:
|
|
1539
1568
|
"""Stable signature for comparing repeated primitive calls."""
|
|
1540
1569
|
try:
|
|
@@ -1543,6 +1572,15 @@ def _canonical_primitive_args(args: dict[str, object]) -> str:
|
|
|
1543
1572
|
return repr(sorted(args.items()))
|
|
1544
1573
|
|
|
1545
1574
|
|
|
1575
|
+
def _copy_primitive_args(args: dict[str, object]) -> dict[str, object]:
|
|
1576
|
+
"""Copy primitive arguments while detaching the nested params dict."""
|
|
1577
|
+
copied = dict(args)
|
|
1578
|
+
params = copied.get("params")
|
|
1579
|
+
if isinstance(params, dict):
|
|
1580
|
+
copied["params"] = dict(params)
|
|
1581
|
+
return copied
|
|
1582
|
+
|
|
1583
|
+
|
|
1546
1584
|
def _conversation_has_successful_identical_primitive_call( # noqa: C901
|
|
1547
1585
|
llm_messages: list[Any],
|
|
1548
1586
|
*,
|
|
@@ -1682,17 +1720,37 @@ def _latest_successful_primitive_result(
|
|
|
1682
1720
|
llm_messages: list[Any],
|
|
1683
1721
|
*,
|
|
1684
1722
|
primitive: str,
|
|
1723
|
+
registry: Any = None,
|
|
1685
1724
|
) -> dict[str, object] | None:
|
|
1686
1725
|
"""Return the most recent successful primitive result payload."""
|
|
1726
|
+
if registry is not None:
|
|
1727
|
+
matching_call_ids = _primitive_call_ids_for_tool(
|
|
1728
|
+
llm_messages,
|
|
1729
|
+
primitive=primitive,
|
|
1730
|
+
registry=registry,
|
|
1731
|
+
)
|
|
1732
|
+
for msg in reversed(llm_messages):
|
|
1733
|
+
payload = _tool_result_payload_for_primitive_call(
|
|
1734
|
+
msg,
|
|
1735
|
+
primitive=primitive,
|
|
1736
|
+
matching_call_ids=matching_call_ids,
|
|
1737
|
+
)
|
|
1738
|
+
if payload is None or not _primitive_payload_is_success(
|
|
1739
|
+
payload,
|
|
1740
|
+
primitive=primitive,
|
|
1741
|
+
):
|
|
1742
|
+
continue
|
|
1743
|
+
result = _primitive_payload_result_dict(payload)
|
|
1744
|
+
if result is not None:
|
|
1745
|
+
return result
|
|
1746
|
+
|
|
1687
1747
|
for msg in reversed(llm_messages):
|
|
1688
1748
|
payload = _tool_result_payload_for_primitive(msg, primitive=primitive)
|
|
1689
1749
|
if payload is None or not _primitive_payload_is_success(payload, primitive=primitive):
|
|
1690
1750
|
continue
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
if isinstance(result, dict):
|
|
1695
|
-
return cast("dict[str, object]", result)
|
|
1751
|
+
result = _primitive_payload_result_dict(payload)
|
|
1752
|
+
if result is not None:
|
|
1753
|
+
return result
|
|
1696
1754
|
return None
|
|
1697
1755
|
|
|
1698
1756
|
|
|
@@ -1842,6 +1900,27 @@ def _nonempty_str(value: object) -> str | None:
|
|
|
1842
1900
|
return stripped or None
|
|
1843
1901
|
|
|
1844
1902
|
|
|
1903
|
+
_KOREAN_SIDO_ABBREVIATIONS: dict[str, str] = {
|
|
1904
|
+
"서울": "서울특별시",
|
|
1905
|
+
"부산": "부산광역시",
|
|
1906
|
+
"대구": "대구광역시",
|
|
1907
|
+
"인천": "인천광역시",
|
|
1908
|
+
"광주": "광주광역시",
|
|
1909
|
+
"대전": "대전광역시",
|
|
1910
|
+
"울산": "울산광역시",
|
|
1911
|
+
"세종": "세종특별자치시",
|
|
1912
|
+
"경기": "경기도",
|
|
1913
|
+
"강원": "강원특별자치도",
|
|
1914
|
+
"충북": "충청북도",
|
|
1915
|
+
"충남": "충청남도",
|
|
1916
|
+
"전북": "전북특별자치도",
|
|
1917
|
+
"전남": "전라남도",
|
|
1918
|
+
"경북": "경상북도",
|
|
1919
|
+
"경남": "경상남도",
|
|
1920
|
+
"제주": "제주특별자치도",
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
|
|
1845
1924
|
def _region_pair_from_address_text(text: object) -> tuple[str, str] | None:
|
|
1846
1925
|
"""Derive NMC Q0/Q1 from a structured Korean address string.
|
|
1847
1926
|
|
|
@@ -1853,7 +1932,7 @@ def _region_pair_from_address_text(text: object) -> tuple[str, str] | None:
|
|
|
1853
1932
|
parts = [part.strip() for part in text.split() if part.strip()]
|
|
1854
1933
|
if len(parts) < 2:
|
|
1855
1934
|
return None
|
|
1856
|
-
q0 = parts[0]
|
|
1935
|
+
q0 = _KOREAN_SIDO_ABBREVIATIONS.get(parts[0], parts[0])
|
|
1857
1936
|
if not q0.endswith(("특별시", "광역시", "특별자치시", "특별자치도", "도")):
|
|
1858
1937
|
return None
|
|
1859
1938
|
q1 = parts[1]
|
|
@@ -1920,10 +1999,132 @@ def _locate_result_coords(result: dict[str, object]) -> tuple[float, float] | No
|
|
|
1920
1999
|
return None
|
|
1921
2000
|
|
|
1922
2001
|
|
|
2002
|
+
def _locate_result_adm_cd(result: dict[str, object]) -> str | None:
|
|
2003
|
+
"""Extract a 10-digit administrative code from a locate result."""
|
|
2004
|
+
candidates: list[object] = [result.get("adm_cd"), result.get("region"), result]
|
|
2005
|
+
for candidate in candidates:
|
|
2006
|
+
if not isinstance(candidate, dict):
|
|
2007
|
+
continue
|
|
2008
|
+
for key in ("code", "adm_cd", "h_code", "b_code"):
|
|
2009
|
+
value = candidate.get(key)
|
|
2010
|
+
if isinstance(value, str) and len(value) == 10 and value.isdigit():
|
|
2011
|
+
return value
|
|
2012
|
+
return None
|
|
2013
|
+
|
|
2014
|
+
|
|
2015
|
+
_REVERSE_GEOCODE_TOOL_IDS = frozenset({"kakao_coord_to_region", "sgis_adm_cd_lookup"})
|
|
2016
|
+
_GENERIC_NMC_QN_FILTERS = frozenset(
|
|
2017
|
+
{
|
|
2018
|
+
"응급실",
|
|
2019
|
+
"야간응급실",
|
|
2020
|
+
"야간 응급실",
|
|
2021
|
+
"응급의료",
|
|
2022
|
+
"응급의료기관",
|
|
2023
|
+
"응급의료센터",
|
|
2024
|
+
"응급센터",
|
|
2025
|
+
"응급 병원",
|
|
2026
|
+
"emergency",
|
|
2027
|
+
"emergency room",
|
|
2028
|
+
"er",
|
|
2029
|
+
}
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
|
|
2033
|
+
def _normalized_nmc_qn_filter(value: object) -> str | None:
|
|
2034
|
+
"""Return a valid NMC institution-name filter or None for generic intent."""
|
|
2035
|
+
if not isinstance(value, str):
|
|
2036
|
+
return None
|
|
2037
|
+
stripped = value.strip()
|
|
2038
|
+
if not stripped:
|
|
2039
|
+
return None
|
|
2040
|
+
lowered = stripped.lower()
|
|
2041
|
+
if lowered in _GENERIC_NMC_QN_FILTERS:
|
|
2042
|
+
return None
|
|
2043
|
+
if "," in stripped or "," in stripped:
|
|
2044
|
+
return None
|
|
2045
|
+
return stripped
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def _nmc_lookup_params_with_clean_qn(
|
|
2049
|
+
args_obj: dict[str, object],
|
|
2050
|
+
) -> tuple[object, dict[str, object]]:
|
|
2051
|
+
"""Copy NMC params and remove generic emergency-intent QN filters."""
|
|
2052
|
+
raw_params = args_obj.get("params")
|
|
2053
|
+
params = dict(raw_params) if isinstance(raw_params, dict) else {}
|
|
2054
|
+
if "qn" in params:
|
|
2055
|
+
qn_filter = _normalized_nmc_qn_filter(params.get("qn"))
|
|
2056
|
+
if qn_filter is None:
|
|
2057
|
+
params.pop("qn", None)
|
|
2058
|
+
else:
|
|
2059
|
+
params["qn"] = qn_filter
|
|
2060
|
+
return raw_params, params
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
def _is_whole_degree_pair(lat: object, lon: object) -> bool:
|
|
2064
|
+
"""Return True for rounded whole-degree WGS-84 coordinate pairs."""
|
|
2065
|
+
if isinstance(lat, bool) or isinstance(lon, bool):
|
|
2066
|
+
return False
|
|
2067
|
+
if not isinstance(lat, int | float) or not isinstance(lon, int | float):
|
|
2068
|
+
return False
|
|
2069
|
+
return float(lat).is_integer() and float(lon).is_integer()
|
|
2070
|
+
|
|
2071
|
+
|
|
2072
|
+
def _normalize_reverse_geocode_args_from_prior_locate(
|
|
2073
|
+
fname: str,
|
|
2074
|
+
args_obj: dict[str, object],
|
|
2075
|
+
llm_messages: list[Any],
|
|
2076
|
+
*,
|
|
2077
|
+
registry: Any = None,
|
|
2078
|
+
) -> dict[str, object]:
|
|
2079
|
+
"""Copy exact locate coordinates into reverse-geocode retries.
|
|
2080
|
+
|
|
2081
|
+
KMA forecast adapters may accept coarse lat/lon because they convert to a
|
|
2082
|
+
grid internally, but reverse-geocode adapters resolve an exact coordinate
|
|
2083
|
+
back to an administrative region. When the model rounded a coordinate that
|
|
2084
|
+
was already available in the prior locate result, keep the selected adapter
|
|
2085
|
+
and repair only this derived argument pair.
|
|
2086
|
+
"""
|
|
2087
|
+
if fname != "locate" or args_obj.get("tool_id") not in _REVERSE_GEOCODE_TOOL_IDS:
|
|
2088
|
+
return args_obj
|
|
2089
|
+
|
|
2090
|
+
raw_params = args_obj.get("params")
|
|
2091
|
+
if not isinstance(raw_params, dict):
|
|
2092
|
+
return args_obj
|
|
2093
|
+
|
|
2094
|
+
if not _is_whole_degree_pair(raw_params.get("lat"), raw_params.get("lon")):
|
|
2095
|
+
return args_obj
|
|
2096
|
+
|
|
2097
|
+
locate_result = _latest_successful_primitive_result(
|
|
2098
|
+
llm_messages,
|
|
2099
|
+
primitive="locate",
|
|
2100
|
+
registry=registry,
|
|
2101
|
+
)
|
|
2102
|
+
if locate_result is None:
|
|
2103
|
+
return args_obj
|
|
2104
|
+
|
|
2105
|
+
coords = _locate_result_coords(locate_result)
|
|
2106
|
+
if coords is None:
|
|
2107
|
+
return args_obj
|
|
2108
|
+
|
|
2109
|
+
next_params = dict(raw_params)
|
|
2110
|
+
next_params["lat"], next_params["lon"] = coords
|
|
2111
|
+
normalized = dict(args_obj)
|
|
2112
|
+
normalized["params"] = next_params
|
|
2113
|
+
logger.info(
|
|
2114
|
+
"locate: normalized %s rounded lat/lon from prior locate lat=%s lon=%s",
|
|
2115
|
+
args_obj.get("tool_id"),
|
|
2116
|
+
coords[0],
|
|
2117
|
+
coords[1],
|
|
2118
|
+
)
|
|
2119
|
+
return normalized
|
|
2120
|
+
|
|
2121
|
+
|
|
1923
2122
|
def _normalize_nmc_lookup_args_from_prior_locate(
|
|
1924
2123
|
fname: str,
|
|
1925
2124
|
args_obj: dict[str, object],
|
|
1926
2125
|
llm_messages: list[Any],
|
|
2126
|
+
*,
|
|
2127
|
+
registry: Any = None,
|
|
1927
2128
|
) -> dict[str, object]:
|
|
1928
2129
|
"""Fill NMC region-mode params from the latest successful locate result.
|
|
1929
2130
|
|
|
@@ -1936,8 +2137,7 @@ def _normalize_nmc_lookup_args_from_prior_locate(
|
|
|
1936
2137
|
if fname != "find" or args_obj.get("tool_id") != "nmc_emergency_search":
|
|
1937
2138
|
return args_obj
|
|
1938
2139
|
|
|
1939
|
-
raw_params = args_obj
|
|
1940
|
-
params = dict(raw_params) if isinstance(raw_params, dict) else {}
|
|
2140
|
+
raw_params, params = _nmc_lookup_params_with_clean_qn(args_obj)
|
|
1941
2141
|
limit = params.get("limit")
|
|
1942
2142
|
needs_default_limit = not isinstance(limit, int) or isinstance(limit, bool)
|
|
1943
2143
|
|
|
@@ -1947,9 +2147,17 @@ def _normalize_nmc_lookup_args_from_prior_locate(
|
|
|
1947
2147
|
and bool(_nonempty_str(params.get("q1")))
|
|
1948
2148
|
)
|
|
1949
2149
|
if has_region_params and not needs_default_limit:
|
|
2150
|
+
if params != raw_params and isinstance(raw_params, dict):
|
|
2151
|
+
normalized = dict(args_obj)
|
|
2152
|
+
normalized["params"] = params
|
|
2153
|
+
return normalized
|
|
1950
2154
|
return args_obj
|
|
1951
2155
|
|
|
1952
|
-
locate_result = _latest_successful_primitive_result(
|
|
2156
|
+
locate_result = _latest_successful_primitive_result(
|
|
2157
|
+
llm_messages,
|
|
2158
|
+
primitive="locate",
|
|
2159
|
+
registry=registry,
|
|
2160
|
+
)
|
|
1953
2161
|
if locate_result is None:
|
|
1954
2162
|
if has_region_params and needs_default_limit:
|
|
1955
2163
|
normalized = dict(args_obj)
|
|
@@ -1959,8 +2167,39 @@ def _normalize_nmc_lookup_args_from_prior_locate(
|
|
|
1959
2167
|
return normalized
|
|
1960
2168
|
return args_obj
|
|
1961
2169
|
|
|
2170
|
+
return _normalize_nmc_lookup_args_from_locate_result(args_obj, locate_result)
|
|
2171
|
+
|
|
2172
|
+
|
|
2173
|
+
def _normalize_nmc_lookup_args_from_locate_result(
|
|
2174
|
+
args_obj: dict[str, object],
|
|
2175
|
+
locate_result: dict[str, object],
|
|
2176
|
+
) -> dict[str, object]:
|
|
2177
|
+
"""Fill NMC region-mode params from an already observed locate result."""
|
|
2178
|
+
|
|
2179
|
+
raw_params, params = _nmc_lookup_params_with_clean_qn(args_obj)
|
|
2180
|
+
limit = params.get("limit")
|
|
2181
|
+
needs_default_limit = not isinstance(limit, int) or isinstance(limit, bool)
|
|
2182
|
+
|
|
2183
|
+
has_region_params = (
|
|
2184
|
+
params.get("mode") == "region"
|
|
2185
|
+
and bool(_nonempty_str(params.get("q0")))
|
|
2186
|
+
and bool(_nonempty_str(params.get("q1")))
|
|
2187
|
+
)
|
|
2188
|
+
if has_region_params and not needs_default_limit:
|
|
2189
|
+
if params != raw_params and isinstance(raw_params, dict):
|
|
2190
|
+
normalized = dict(args_obj)
|
|
2191
|
+
normalized["params"] = params
|
|
2192
|
+
return normalized
|
|
2193
|
+
return args_obj
|
|
2194
|
+
|
|
1962
2195
|
region_pair = _locate_result_region_pair(locate_result)
|
|
1963
2196
|
if region_pair is None:
|
|
2197
|
+
if has_region_params and needs_default_limit:
|
|
2198
|
+
normalized = dict(args_obj)
|
|
2199
|
+
next_params = dict(params)
|
|
2200
|
+
next_params["limit"] = 5
|
|
2201
|
+
normalized["params"] = next_params
|
|
2202
|
+
return normalized
|
|
1964
2203
|
return args_obj
|
|
1965
2204
|
origin_coords = _locate_result_coords(locate_result)
|
|
1966
2205
|
|
|
@@ -1988,6 +2227,182 @@ def _normalize_nmc_lookup_args_from_prior_locate(
|
|
|
1988
2227
|
return normalized
|
|
1989
2228
|
|
|
1990
2229
|
|
|
2230
|
+
_HIRA_DEPARTMENT_HINTS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
2231
|
+
(re.compile(r"소아청소년과|소아과|pediatrics?", re.IGNORECASE), "소아청소년과"),
|
|
2232
|
+
(re.compile(r"이비인후과|ent\b", re.IGNORECASE), "이비인후과"),
|
|
2233
|
+
(re.compile(r"내과|internal medicine", re.IGNORECASE), "내과"),
|
|
2234
|
+
(re.compile(r"피부과|dermatology", re.IGNORECASE), "피부과"),
|
|
2235
|
+
(re.compile(r"정형외과|orthopedics?", re.IGNORECASE), "정형외과"),
|
|
2236
|
+
(re.compile(r"산부인과|obgyn|ob/gyn", re.IGNORECASE), "산부인과"),
|
|
2237
|
+
(re.compile(r"안과|ophthalmology", re.IGNORECASE), "안과"),
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
def _hira_department_from_query(user_query: str) -> str | None:
|
|
2242
|
+
for pattern, value in _HIRA_DEPARTMENT_HINTS:
|
|
2243
|
+
if pattern.search(user_query):
|
|
2244
|
+
return value
|
|
2245
|
+
return None
|
|
2246
|
+
|
|
2247
|
+
|
|
2248
|
+
def _normalize_hira_lookup_args_from_prior_locate(
|
|
2249
|
+
fname: str,
|
|
2250
|
+
args_obj: dict[str, object],
|
|
2251
|
+
llm_messages: list[Any],
|
|
2252
|
+
user_query: str,
|
|
2253
|
+
*,
|
|
2254
|
+
registry: Any = None,
|
|
2255
|
+
) -> dict[str, object]:
|
|
2256
|
+
"""Fill HIRA coordinate-radius params from the latest successful locate result."""
|
|
2257
|
+
if fname != "find" or args_obj.get("tool_id") != "hira_hospital_search":
|
|
2258
|
+
return args_obj
|
|
2259
|
+
locate_result = _latest_successful_primitive_result(
|
|
2260
|
+
llm_messages,
|
|
2261
|
+
primitive="locate",
|
|
2262
|
+
registry=registry,
|
|
2263
|
+
)
|
|
2264
|
+
if locate_result is None:
|
|
2265
|
+
return args_obj
|
|
2266
|
+
return _normalize_hira_lookup_args_from_locate_result(args_obj, locate_result, user_query)
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _normalize_hira_lookup_args_from_locate_result(
|
|
2270
|
+
args_obj: dict[str, object],
|
|
2271
|
+
locate_result: dict[str, object],
|
|
2272
|
+
user_query: str,
|
|
2273
|
+
) -> dict[str, object]:
|
|
2274
|
+
"""Fill HIRA coordinate-radius params from an already observed locate result."""
|
|
2275
|
+
|
|
2276
|
+
raw_params = args_obj.get("params")
|
|
2277
|
+
params = dict(raw_params) if isinstance(raw_params, dict) else {}
|
|
2278
|
+
valid_coords = (
|
|
2279
|
+
isinstance(params.get("xPos"), int | float)
|
|
2280
|
+
and isinstance(params.get("yPos"), int | float)
|
|
2281
|
+
and not _is_whole_degree_pair(params.get("yPos"), params.get("xPos"))
|
|
2282
|
+
)
|
|
2283
|
+
valid_radius = (
|
|
2284
|
+
isinstance(params.get("radius"), int)
|
|
2285
|
+
and not isinstance(params.get("radius"), bool)
|
|
2286
|
+
and 1 <= int(params["radius"]) <= 10000
|
|
2287
|
+
)
|
|
2288
|
+
|
|
2289
|
+
changed = not isinstance(raw_params, dict)
|
|
2290
|
+
if not valid_coords:
|
|
2291
|
+
coords = _locate_result_coords(locate_result)
|
|
2292
|
+
if coords is None:
|
|
2293
|
+
return args_obj
|
|
2294
|
+
lat, lon = coords
|
|
2295
|
+
params["xPos"] = lon
|
|
2296
|
+
params["yPos"] = lat
|
|
2297
|
+
changed = True
|
|
2298
|
+
|
|
2299
|
+
if not valid_radius:
|
|
2300
|
+
params["radius"] = 2000
|
|
2301
|
+
changed = True
|
|
2302
|
+
|
|
2303
|
+
if params.get("dgsbjt") in (None, ""):
|
|
2304
|
+
department = _hira_department_from_query(user_query)
|
|
2305
|
+
if department is not None:
|
|
2306
|
+
params["dgsbjt"] = department
|
|
2307
|
+
changed = True
|
|
2308
|
+
|
|
2309
|
+
if not changed:
|
|
2310
|
+
return args_obj
|
|
2311
|
+
normalized = dict(args_obj)
|
|
2312
|
+
normalized["params"] = params
|
|
2313
|
+
logger.info(
|
|
2314
|
+
"find: normalized hira_hospital_search params from prior locate "
|
|
2315
|
+
"xPos=%s yPos=%s radius=%s dgsbjt=%s",
|
|
2316
|
+
params.get("xPos"),
|
|
2317
|
+
params.get("yPos"),
|
|
2318
|
+
params.get("radius"),
|
|
2319
|
+
params.get("dgsbjt"),
|
|
2320
|
+
)
|
|
2321
|
+
return normalized
|
|
2322
|
+
|
|
2323
|
+
|
|
2324
|
+
def _normalize_koroad_lookup_args_from_prior_locate(
|
|
2325
|
+
fname: str,
|
|
2326
|
+
args_obj: dict[str, object],
|
|
2327
|
+
llm_messages: list[Any],
|
|
2328
|
+
*,
|
|
2329
|
+
registry: Any = None,
|
|
2330
|
+
) -> dict[str, object]:
|
|
2331
|
+
"""Fill KOROAD adm_cd/year params from the latest successful locate result."""
|
|
2332
|
+
if fname != "find" or args_obj.get("tool_id") != "koroad_accident_hazard_search":
|
|
2333
|
+
return args_obj
|
|
2334
|
+
locate_result = _latest_successful_primitive_result(
|
|
2335
|
+
llm_messages,
|
|
2336
|
+
primitive="locate",
|
|
2337
|
+
registry=registry,
|
|
2338
|
+
)
|
|
2339
|
+
if locate_result is None:
|
|
2340
|
+
return args_obj
|
|
2341
|
+
return _normalize_koroad_lookup_args_from_locate_result(args_obj, locate_result)
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
def _normalize_koroad_lookup_args_from_locate_result(
|
|
2345
|
+
args_obj: dict[str, object],
|
|
2346
|
+
locate_result: dict[str, object],
|
|
2347
|
+
) -> dict[str, object]:
|
|
2348
|
+
"""Fill KOROAD adm_cd/year params from an already observed locate result."""
|
|
2349
|
+
|
|
2350
|
+
raw_params = args_obj.get("params")
|
|
2351
|
+
params = dict(raw_params) if isinstance(raw_params, dict) else {}
|
|
2352
|
+
changed = not isinstance(raw_params, dict)
|
|
2353
|
+
|
|
2354
|
+
adm_cd = params.get("adm_cd")
|
|
2355
|
+
if not (isinstance(adm_cd, str) and len(adm_cd) == 10 and adm_cd.isdigit()):
|
|
2356
|
+
adm_cd_from_locate = _locate_result_adm_cd(locate_result)
|
|
2357
|
+
if adm_cd_from_locate is None:
|
|
2358
|
+
return args_obj
|
|
2359
|
+
params["adm_cd"] = adm_cd_from_locate
|
|
2360
|
+
changed = True
|
|
2361
|
+
|
|
2362
|
+
year = params.get("year")
|
|
2363
|
+
if not isinstance(year, int) or isinstance(year, bool):
|
|
2364
|
+
# Latest KOROAD frequentzoneLg dataset code currently maps to 2024 in
|
|
2365
|
+
# accident_hazard_search._YEAR_TO_SEARCH_CD. Keep the adapter contract
|
|
2366
|
+
# explicit instead of asking the model to guess a dataset vintage.
|
|
2367
|
+
params["year"] = 2024
|
|
2368
|
+
changed = True
|
|
2369
|
+
|
|
2370
|
+
if not changed:
|
|
2371
|
+
return args_obj
|
|
2372
|
+
normalized = dict(args_obj)
|
|
2373
|
+
normalized["params"] = params
|
|
2374
|
+
logger.info(
|
|
2375
|
+
"find: normalized koroad_accident_hazard_search params from prior locate adm_cd=%s year=%s",
|
|
2376
|
+
params.get("adm_cd"),
|
|
2377
|
+
params.get("year"),
|
|
2378
|
+
)
|
|
2379
|
+
return normalized
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
def _normalize_lookup_args_from_cached_locate_result(
|
|
2383
|
+
fname: str,
|
|
2384
|
+
args_obj: dict[str, object],
|
|
2385
|
+
locate_result: dict[str, object] | None,
|
|
2386
|
+
*,
|
|
2387
|
+
user_query: str = "",
|
|
2388
|
+
) -> dict[str, object]:
|
|
2389
|
+
"""Apply locate-derived argument repair in inbound concrete tool dispatch."""
|
|
2390
|
+
if fname != "find" or locate_result is None:
|
|
2391
|
+
return args_obj
|
|
2392
|
+
tool_id = args_obj.get("tool_id")
|
|
2393
|
+
if tool_id == "nmc_emergency_search":
|
|
2394
|
+
return _normalize_nmc_lookup_args_from_locate_result(args_obj, locate_result)
|
|
2395
|
+
if tool_id == "hira_hospital_search":
|
|
2396
|
+
return _normalize_hira_lookup_args_from_locate_result(
|
|
2397
|
+
args_obj,
|
|
2398
|
+
locate_result,
|
|
2399
|
+
user_query,
|
|
2400
|
+
)
|
|
2401
|
+
if tool_id == "koroad_accident_hazard_search":
|
|
2402
|
+
return _normalize_koroad_lookup_args_from_locate_result(args_obj, locate_result)
|
|
2403
|
+
return args_obj
|
|
2404
|
+
|
|
2405
|
+
|
|
1991
2406
|
def _check_sensitive_lookup_terminated_without_lookup(
|
|
1992
2407
|
llm_messages: list[Any],
|
|
1993
2408
|
user_query: str,
|
|
@@ -3018,6 +3433,39 @@ def _maybe_reroute_locate_admin_keyword_args(
|
|
|
3018
3433
|
return {**args_obj, "tool_id": "kakao_address_search", "params": next_params}
|
|
3019
3434
|
|
|
3020
3435
|
|
|
3436
|
+
_POI_ADDRESS_QUERY_RE: Final = re.compile(
|
|
3437
|
+
r"(역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|병원|의원|약국|랜드마크)"
|
|
3438
|
+
)
|
|
3439
|
+
_ROAD_ADDRESS_QUERY_RE: Final = re.compile(r"(?:로|길)\s*\d{0,4}(?:번길)?")
|
|
3440
|
+
|
|
3441
|
+
|
|
3442
|
+
def _maybe_reroute_locate_poi_address_args(
|
|
3443
|
+
fname: str,
|
|
3444
|
+
args_obj: dict[str, Any],
|
|
3445
|
+
) -> dict[str, Any]:
|
|
3446
|
+
"""Rewrite named-place Kakao address calls to the documented keyword adapter."""
|
|
3447
|
+
|
|
3448
|
+
if fname != "locate" or args_obj.get("tool_id") != "kakao_address_search":
|
|
3449
|
+
return args_obj
|
|
3450
|
+
params = args_obj.get("params")
|
|
3451
|
+
if not isinstance(params, dict):
|
|
3452
|
+
return args_obj
|
|
3453
|
+
query = params.get("query")
|
|
3454
|
+
if not isinstance(query, str):
|
|
3455
|
+
return args_obj
|
|
3456
|
+
if not _POI_ADDRESS_QUERY_RE.search(query):
|
|
3457
|
+
return args_obj
|
|
3458
|
+
if _ROAD_ADDRESS_QUERY_RE.search(query):
|
|
3459
|
+
return args_obj
|
|
3460
|
+
|
|
3461
|
+
next_params = {**params, "query": query.strip()}
|
|
3462
|
+
logger.info(
|
|
3463
|
+
"locate: rerouted Kakao address POI query to keyword search: %r",
|
|
3464
|
+
query,
|
|
3465
|
+
)
|
|
3466
|
+
return {**args_obj, "tool_id": "kakao_keyword_search", "params": next_params}
|
|
3467
|
+
|
|
3468
|
+
|
|
3021
3469
|
def _effective_chat_max_tokens(requested: int) -> int:
|
|
3022
3470
|
"""Clamp interactive chat completions so bad tool-routing loops fail fast."""
|
|
3023
3471
|
raw = os.getenv("UMMAYA_CHAT_MAX_TOKENS")
|
|
@@ -3345,7 +3793,8 @@ def _check_chain_prerequisite( # noqa: C901
|
|
|
3345
3793
|
"nmc_emergency_search({mode:'region', q0:region.region_1depth_name, "
|
|
3346
3794
|
"q1:region.region_2depth_name, origin_lat:coords.lat, "
|
|
3347
3795
|
"origin_lon:coords.lon, limit:<N>}). Do NOT retry coordinate mode for "
|
|
3348
|
-
"station/neighborhood ER search
|
|
3796
|
+
"station/neighborhood ER search. Leave qn unset unless the citizen "
|
|
3797
|
+
"explicitly gave a specific institution name."
|
|
3349
3798
|
)
|
|
3350
3799
|
|
|
3351
3800
|
# Require a successful prior locate for this context. If resolve
|
|
@@ -3388,8 +3837,8 @@ def _check_chain_prerequisite( # noqa: C901
|
|
|
3388
3837
|
"then kakao_coord_to_region({lat:<lat>, lon:<lon>}), "
|
|
3389
3838
|
"then call nmc_emergency_search({mode:'region', q0:region.region_1depth_name, "
|
|
3390
3839
|
"q1:region.region_2depth_name, origin_lat:coords.lat, origin_lon:coords.lon, "
|
|
3391
|
-
"limit:<N>}). Do NOT guess coordinates
|
|
3392
|
-
"
|
|
3840
|
+
"limit:<N>}). Do NOT guess coordinates. Leave qn unset unless the citizen "
|
|
3841
|
+
"explicitly gave a specific institution name."
|
|
3393
3842
|
)
|
|
3394
3843
|
if has_coord or schema_coord_fields:
|
|
3395
3844
|
field_kind = "coordinates"
|
|
@@ -4105,6 +4554,7 @@ async def run( # noqa: C901
|
|
|
4105
4554
|
# LLM to reconstruct auth_context from a prior tool result.
|
|
4106
4555
|
_session_auth_contexts: dict[str, object] = {}
|
|
4107
4556
|
_session_auth_session_ids: dict[str, str] = {}
|
|
4557
|
+
_session_latest_locate_results: dict[str, dict[str, object]] = {}
|
|
4108
4558
|
|
|
4109
4559
|
# Epic #2077 T010 — single ToolRegistry + ToolExecutor instance pair
|
|
4110
4560
|
# reused across every chat_request. Adapter registration happens lazily
|
|
@@ -4197,14 +4647,12 @@ async def run( # noqa: C901
|
|
|
4197
4647
|
if _llm_system_prompt_cached[0] is not None:
|
|
4198
4648
|
return _llm_system_prompt_cached[0] or None
|
|
4199
4649
|
try:
|
|
4200
|
-
from
|
|
4201
|
-
|
|
4202
|
-
|
|
4650
|
+
from ummaya.context.prompt_loader import ( # noqa: PLC0415
|
|
4651
|
+
PromptLoader,
|
|
4652
|
+
default_manifest_path,
|
|
4653
|
+
)
|
|
4203
4654
|
|
|
4204
|
-
|
|
4205
|
-
# stdio backend runs from repo root when invoked via
|
|
4206
|
-
# `uv run ummaya --ipc stdio`, so resolve relative to CWD.
|
|
4207
|
-
manifest = Path("prompts") / "manifest.yaml"
|
|
4655
|
+
manifest = default_manifest_path()
|
|
4208
4656
|
if not manifest.is_file():
|
|
4209
4657
|
_llm_system_prompt_cached[0] = ""
|
|
4210
4658
|
return None
|
|
@@ -4348,7 +4796,7 @@ async def run( # noqa: C901
|
|
|
4348
4796
|
_AVAILABLE_ADAPTERS_TOP_K = int( # noqa: N806 — env-derived constant
|
|
4349
4797
|
_os_chat_env.environ.get("UMMAYA_AVAILABLE_ADAPTERS_TOP_K", "5")
|
|
4350
4798
|
)
|
|
4351
|
-
_root_primitive_tool_ids =
|
|
4799
|
+
_root_primitive_tool_ids = _ROOT_PRIMITIVE_TOOL_IDS
|
|
4352
4800
|
|
|
4353
4801
|
def _select_concrete_adapter_tools_for_turn(user_query: str) -> list[Any]:
|
|
4354
4802
|
"""Return concrete, non-core adapter tools for this citizen turn.
|
|
@@ -5361,6 +5809,11 @@ async def run( # noqa: C901
|
|
|
5361
5809
|
"tool_id": str(args_obj.get("tool_id", fname)),
|
|
5362
5810
|
}
|
|
5363
5811
|
|
|
5812
|
+
if fname == "locate" and dispatch_error is None:
|
|
5813
|
+
locate_result = result_payload.get("result")
|
|
5814
|
+
if isinstance(locate_result, dict) and locate_result.get("kind") != "error":
|
|
5815
|
+
_session_latest_locate_results[session_id] = locate_result
|
|
5816
|
+
|
|
5364
5817
|
# Drain the outbound HTTP trace buffer + attach to the envelope.
|
|
5365
5818
|
outbound_traces = consume_outbound_capture(_outbound_trace_token)
|
|
5366
5819
|
if outbound_traces:
|
|
@@ -5501,8 +5954,9 @@ async def run( # noqa: C901
|
|
|
5501
5954
|
# UMMAYA now follows that shape: BM25/dense retrieval selects a small
|
|
5502
5955
|
# turn-local set of concrete adapter tools, and each selected
|
|
5503
5956
|
# GovAPITool is exported directly as an OpenAI-compatible function.
|
|
5504
|
-
#
|
|
5505
|
-
#
|
|
5957
|
+
# Keep the root primitives alongside that set to preserve the 0.2.1
|
|
5958
|
+
# CC-style loop contract: the model can paint progress prose, then call
|
|
5959
|
+
# a primitive dispatcher with a concrete adapter in `tool_id`.
|
|
5506
5960
|
registry = cast("Any", _ensure_tool_registry())
|
|
5507
5961
|
backend_tools_raw = [
|
|
5508
5962
|
t.to_openai_tool() for t in _select_concrete_adapter_tools_for_turn(latest_user_utt)
|
|
@@ -5517,9 +5971,14 @@ async def run( # noqa: C901
|
|
|
5517
5971
|
llm_tools: list[LLMToolDefinition] = [
|
|
5518
5972
|
LLMToolDefinition.model_validate(raw) for raw in backend_tools_raw
|
|
5519
5973
|
]
|
|
5974
|
+
has_concrete_backend_tools = bool(backend_tools_raw)
|
|
5520
5975
|
for t in frame.tools:
|
|
5521
5976
|
tui_name = getattr(getattr(t, "function", None), "name", None)
|
|
5522
|
-
if
|
|
5977
|
+
if not _should_append_tui_tool_to_llm_tools(
|
|
5978
|
+
tui_name,
|
|
5979
|
+
backend_tool_names,
|
|
5980
|
+
has_concrete_backend_tools=has_concrete_backend_tools,
|
|
5981
|
+
):
|
|
5523
5982
|
continue
|
|
5524
5983
|
llm_tools.append(LLMToolDefinition.model_validate(t.model_dump()))
|
|
5525
5984
|
|
|
@@ -5605,14 +6064,14 @@ async def run( # noqa: C901
|
|
|
5605
6064
|
"base_time 추측 금지 — 위 hint 또는 그 이전 정시 사용.\n"
|
|
5606
6065
|
)
|
|
5607
6066
|
|
|
5608
|
-
#
|
|
5609
|
-
# function, NOT an LLM-callable
|
|
5610
|
-
# latest citizen utterance and inject
|
|
5611
|
-
#
|
|
5612
|
-
#
|
|
5613
|
-
#
|
|
5614
|
-
#
|
|
5615
|
-
#
|
|
6067
|
+
# 2026 tool-surface migration — adapter discovery is a backend
|
|
6068
|
+
# function, NOT an LLM-callable mode. Run the search against the
|
|
6069
|
+
# latest citizen utterance and inject top-K candidates into the
|
|
6070
|
+
# dynamic suffix as ``<available_adapters>``. The normal path is a
|
|
6071
|
+
# concrete adapter function call; the root primitive wrappers stay
|
|
6072
|
+
# available only for legacy transcript compatibility. Search-mode
|
|
6073
|
+
# calls were the source of the "● find(search:)" phantom tool-UI
|
|
6074
|
+
# noise that user surfaced via Layer 5 frame capture.
|
|
5616
6075
|
try:
|
|
5617
6076
|
for m in reversed(frame.messages):
|
|
5618
6077
|
if m.role == "user" and m.content:
|
|
@@ -5715,24 +6174,45 @@ async def run( # noqa: C901
|
|
|
5715
6174
|
for _turn in range(_AGENTIC_LOOP_MAX_TURNS):
|
|
5716
6175
|
message_id = str(uuid.uuid4())
|
|
5717
6176
|
assistant_text_chunks: list[str] = []
|
|
5718
|
-
#
|
|
5719
|
-
#
|
|
5720
|
-
#
|
|
5721
|
-
#
|
|
5722
|
-
#
|
|
5723
|
-
#
|
|
5724
|
-
#
|
|
5725
|
-
#
|
|
5726
|
-
# this turn invoked tools. When tools are invoked we suppress the
|
|
5727
|
-
# preamble entirely — the next turn produces the real answer
|
|
5728
|
-
# after the tool result is appended to context. When no tools
|
|
5729
|
-
# are invoked we flush the buffer as a single chunk so the prose
|
|
5730
|
-
# still reaches the citizen.
|
|
6177
|
+
# CC stream order: K-EXAONE may emit a visible progress sentence
|
|
6178
|
+
# before the structured ``tool_call_delta`` in the same assistant
|
|
6179
|
+
# turn. Claude Code commits that text block before opening the
|
|
6180
|
+
# following tool_use block, so the TUI can paint
|
|
6181
|
+
# ``assistant text → tool_call``. Buffer here only so textual
|
|
6182
|
+
# ``<tool_call>`` markers can be stripped accurately across chunk
|
|
6183
|
+
# boundaries; when a real ToolCallFrame is emitted below, flush the
|
|
6184
|
+
# cleaned visible text immediately before the tool frame.
|
|
5731
6185
|
buffered_visible: list[str] = []
|
|
5732
6186
|
tool_call_buf: dict[int, dict[str, str]] = {}
|
|
5733
6187
|
stream_error: Exception | None = None
|
|
5734
6188
|
stream_gate = StreamGate()
|
|
5735
6189
|
|
|
6190
|
+
async def _emit_buffered_visible_before_tool(current_message_id: str) -> None:
|
|
6191
|
+
"""Emit same-turn visible prose before opening a tool_use block."""
|
|
6192
|
+
nonlocal buffered_visible
|
|
6193
|
+
if not buffered_visible:
|
|
6194
|
+
return
|
|
6195
|
+
from ummaya.llm.tool_call_parser import ( # noqa: PLC0415
|
|
6196
|
+
strip_leaked_thinking_markers,
|
|
6197
|
+
)
|
|
6198
|
+
|
|
6199
|
+
merged_prose = strip_leaked_thinking_markers("".join(buffered_visible))
|
|
6200
|
+
buffered_visible = []
|
|
6201
|
+
if not merged_prose.strip():
|
|
6202
|
+
return
|
|
6203
|
+
await write_frame(
|
|
6204
|
+
AssistantChunkFrame(
|
|
6205
|
+
session_id=frame.session_id,
|
|
6206
|
+
correlation_id=frame.correlation_id,
|
|
6207
|
+
role="llm",
|
|
6208
|
+
ts=_utcnow(),
|
|
6209
|
+
kind="assistant_chunk",
|
|
6210
|
+
message_id=current_message_id,
|
|
6211
|
+
delta=merged_prose,
|
|
6212
|
+
done=False,
|
|
6213
|
+
)
|
|
6214
|
+
)
|
|
6215
|
+
|
|
5736
6216
|
def _append_tool_routing_observation(reason: str, message: str) -> None:
|
|
5737
6217
|
"""Add an internal routing repair instruction for the next model turn."""
|
|
5738
6218
|
llm_messages.append(
|
|
@@ -6339,11 +6819,10 @@ async def run( # noqa: C901
|
|
|
6339
6819
|
)
|
|
6340
6820
|
)
|
|
6341
6821
|
return
|
|
6342
|
-
# Tool calls present
|
|
6343
|
-
#
|
|
6344
|
-
#
|
|
6345
|
-
#
|
|
6346
|
-
buffered_visible.clear()
|
|
6822
|
+
# Tool calls present. Preserve any same-turn progress prose by
|
|
6823
|
+
# emitting it immediately before the ToolCallFrame below; do not
|
|
6824
|
+
# send a done=True chunk because this provider call must still stop
|
|
6825
|
+
# at assistant(tool_use), not at an assistant final answer.
|
|
6347
6826
|
|
|
6348
6827
|
# ---- T027/T029 — emit tool_call frames + register Futures -----
|
|
6349
6828
|
issued_calls: list[tuple[str, str]] = [] # (call_id, name)
|
|
@@ -6417,7 +6896,35 @@ async def run( # noqa: C901
|
|
|
6417
6896
|
fname = primitive_name
|
|
6418
6897
|
args_obj = {"tool_id": model_tool_name, "params": dict(model_args_obj)}
|
|
6419
6898
|
|
|
6899
|
+
raw_dispatch_args_obj = _copy_primitive_args(args_obj)
|
|
6900
|
+
|
|
6420
6901
|
args_obj = _maybe_reroute_locate_admin_keyword_args(fname, args_obj)
|
|
6902
|
+
args_obj = _maybe_reroute_locate_poi_address_args(fname, args_obj)
|
|
6903
|
+
args_obj = _normalize_reverse_geocode_args_from_prior_locate(
|
|
6904
|
+
fname,
|
|
6905
|
+
args_obj,
|
|
6906
|
+
llm_messages,
|
|
6907
|
+
registry=registry,
|
|
6908
|
+
)
|
|
6909
|
+
args_obj = _normalize_nmc_lookup_args_from_prior_locate(
|
|
6910
|
+
fname,
|
|
6911
|
+
args_obj,
|
|
6912
|
+
llm_messages,
|
|
6913
|
+
registry=registry,
|
|
6914
|
+
)
|
|
6915
|
+
args_obj = _normalize_hira_lookup_args_from_prior_locate(
|
|
6916
|
+
fname,
|
|
6917
|
+
args_obj,
|
|
6918
|
+
llm_messages,
|
|
6919
|
+
latest_user_utt,
|
|
6920
|
+
registry=registry,
|
|
6921
|
+
)
|
|
6922
|
+
args_obj = _normalize_koroad_lookup_args_from_prior_locate(
|
|
6923
|
+
fname,
|
|
6924
|
+
args_obj,
|
|
6925
|
+
llm_messages,
|
|
6926
|
+
registry=registry,
|
|
6927
|
+
)
|
|
6421
6928
|
args_obj = _normalize_lookup_args_for_query(fname, args_obj, latest_user_utt)
|
|
6422
6929
|
args_obj = _normalize_verify_args_for_query(fname, args_obj, latest_user_utt)
|
|
6423
6930
|
args_obj = _normalize_verify_tool_id_for_query(fname, args_obj, latest_user_utt)
|
|
@@ -6478,6 +6985,7 @@ async def run( # noqa: C901
|
|
|
6478
6985
|
ToolResultFrame,
|
|
6479
6986
|
)
|
|
6480
6987
|
|
|
6988
|
+
await _emit_buffered_visible_before_tool(message_id)
|
|
6481
6989
|
await write_frame(
|
|
6482
6990
|
ToolCallFrame(
|
|
6483
6991
|
session_id=frame.session_id,
|
|
@@ -6551,7 +7059,16 @@ async def run( # noqa: C901
|
|
|
6551
7059
|
)
|
|
6552
7060
|
continue
|
|
6553
7061
|
|
|
6554
|
-
|
|
7062
|
+
raw_duplicate = _canonical_primitive_args(
|
|
7063
|
+
raw_dispatch_args_obj
|
|
7064
|
+
) != _canonical_primitive_args(
|
|
7065
|
+
args_obj
|
|
7066
|
+
) and _conversation_has_successful_identical_primitive_call(
|
|
7067
|
+
llm_messages,
|
|
7068
|
+
primitive=fname,
|
|
7069
|
+
args=raw_dispatch_args_obj,
|
|
7070
|
+
)
|
|
7071
|
+
if raw_duplicate or _conversation_has_successful_identical_primitive_call(
|
|
6555
7072
|
llm_messages,
|
|
6556
7073
|
primitive=fname,
|
|
6557
7074
|
args=args_obj,
|
|
@@ -6652,6 +7169,7 @@ async def run( # noqa: C901
|
|
|
6652
7169
|
ToolResultFrame,
|
|
6653
7170
|
)
|
|
6654
7171
|
|
|
7172
|
+
await _emit_buffered_visible_before_tool(message_id)
|
|
6655
7173
|
await write_frame(
|
|
6656
7174
|
ToolCallFrame(
|
|
6657
7175
|
session_id=frame.session_id,
|
|
@@ -6807,6 +7325,7 @@ async def run( # noqa: C901
|
|
|
6807
7325
|
ToolResultFrame,
|
|
6808
7326
|
)
|
|
6809
7327
|
|
|
7328
|
+
await _emit_buffered_visible_before_tool(message_id)
|
|
6810
7329
|
await write_frame(
|
|
6811
7330
|
ToolCallFrame(
|
|
6812
7331
|
session_id=frame.session_id,
|
|
@@ -6884,6 +7403,7 @@ async def run( # noqa: C901
|
|
|
6884
7403
|
)
|
|
6885
7404
|
continue
|
|
6886
7405
|
|
|
7406
|
+
await _emit_buffered_visible_before_tool(message_id)
|
|
6887
7407
|
await write_frame(
|
|
6888
7408
|
ToolCallFrame(
|
|
6889
7409
|
session_id=frame.session_id,
|
|
@@ -7293,6 +7813,12 @@ async def run( # noqa: C901
|
|
|
7293
7813
|
dispatch_name = primitive_name
|
|
7294
7814
|
dispatch_args = {"tool_id": frame.name, "params": dict(args_obj)}
|
|
7295
7815
|
|
|
7816
|
+
dispatch_args = _normalize_lookup_args_from_cached_locate_result(
|
|
7817
|
+
dispatch_name,
|
|
7818
|
+
dispatch_args,
|
|
7819
|
+
_session_latest_locate_results.get(frame.session_id),
|
|
7820
|
+
)
|
|
7821
|
+
|
|
7296
7822
|
await _dispatch_primitive(
|
|
7297
7823
|
frame.call_id,
|
|
7298
7824
|
dispatch_name,
|