ummaya 0.2.2 → 0.2.4
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/README.md +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/prompts/manifest.yaml +2 -2
- package/prompts/session_guidance_v1.md +3 -1
- package/prompts/system_v1.md +8 -7
- package/pyproject.toml +2 -7
- package/src/ummaya/context/builder.py +17 -11
- package/src/ummaya/engine/engine.py +27 -7
- package/src/ummaya/engine/query.py +20 -0
- package/src/ummaya/evidence/__init__.py +25 -0
- package/src/ummaya/evidence/__main__.py +7 -0
- package/src/ummaya/evidence/models.py +58 -0
- package/src/ummaya/evidence/runner.py +308 -0
- package/src/ummaya/evidence/task_registry.py +264 -0
- package/src/ummaya/ipc/frame_schema.py +47 -0
- package/src/ummaya/ipc/stdio.py +1349 -90
- package/src/ummaya/llm/client.py +132 -56
- package/src/ummaya/llm/reasoning.py +84 -0
- package/src/ummaya/tools/discovery_bridge.py +17 -1
- package/src/ummaya/tools/executor.py +32 -12
- package/src/ummaya/tools/geocoding/kakao_client.py +1 -2
- package/src/ummaya/tools/kma/apihub_catalog.py +984 -1
- package/src/ummaya/tools/kma/apihub_structured_adapter.py +86 -6
- package/src/ummaya/tools/kma/apihub_url_adapter.py +593 -0
- package/src/ummaya/tools/kma/apihub_url_catalog.py +296 -0
- package/src/ummaya/tools/location_adapters.py +8 -6
- package/src/ummaya/tools/manifest_metadata.py +16 -3
- package/src/ummaya/tools/mvp_surface.py +2 -2
- package/src/ummaya/tools/nmc/emergency_search.py +8 -6
- package/src/ummaya/tools/register_all.py +9 -0
- package/src/ummaya/tools/resolve_location.py +4 -4
- package/src/ummaya/tools/search.py +664 -18
- package/src/ummaya/tools/verified_data_go_kr/_manifest.py +115 -25
- package/src/ummaya/tools/verified_data_go_kr/airkorea_air_quality.py +109 -4
- package/src/ummaya/tools/verified_data_go_kr/nmc_aed_site.py +108 -2
- package/src/ummaya/tools/verified_data_go_kr/pps_bid_public_info.py +174 -9
- package/src/ummaya/tools/verified_data_go_kr/tago_bus_arrival.py +66 -3
- package/src/ummaya/tools/verified_data_go_kr/tago_bus_location.py +12 -2
- package/src/ummaya/tools/verified_data_go_kr/tago_bus_route.py +8 -2
- package/src/ummaya/tools/verified_data_go_kr/tago_bus_route_station.py +114 -0
- package/src/ummaya/tools/verified_data_go_kr/tago_bus_station.py +14 -3
- package/src/ummaya/tools/verify_canonical_map.py +21 -0
- package/tui/package.json +1 -2
- package/tui/src/QueryEngine.ts +4 -0
- package/tui/src/cli/handlers/auth.ts +1 -1
- package/tui/src/cli/handlers/mcp.tsx +3 -3
- package/tui/src/cli/print.ts +69 -18
- package/tui/src/cli/update.ts +13 -13
- package/tui/src/commands/copy/index.ts +1 -1
- package/tui/src/commands/cost/cost.ts +2 -2
- package/tui/src/commands/init-verifiers.ts +5 -5
- package/tui/src/commands/init.ts +30 -30
- package/tui/src/commands/insights.ts +43 -43
- package/tui/src/commands/install-github-app/install-github-app.tsx +2 -2
- package/tui/src/commands/install-github-app/setupGitHubActions.ts +3 -3
- package/tui/src/commands/install.tsx +5 -5
- package/tui/src/commands/mcp/addCommand.ts +5 -5
- package/tui/src/commands/mcp/xaaIdpCommand.ts +2 -2
- package/tui/src/commands/plugin/ManageMarketplaces.tsx +2 -2
- package/tui/src/commands/reasoning/index.ts +13 -0
- package/tui/src/commands/reasoning/reasoning.tsx +177 -0
- package/tui/src/commands/thinkback/thinkback.tsx +3 -3
- package/tui/src/commands.ts +2 -0
- package/tui/src/components/Messages.tsx +2 -1
- package/tui/src/components/Spinner.tsx +2 -2
- package/tui/src/components/design-system/LoadingState.tsx +2 -2
- package/tui/src/ipc/codec.ts +26 -0
- package/tui/src/ipc/frames.generated.ts +398 -303
- package/tui/src/ipc/llmClient.ts +130 -51
- package/tui/src/ipc/llmTypes.ts +16 -1
- package/tui/src/ipc/schema/frame.schema.json +1 -3475
- package/tui/src/main.tsx +3 -0
- package/tui/src/query.ts +467 -2
- package/tui/src/screens/REPL.tsx +3 -3
- package/tui/src/services/api/claude.ts +54 -25
- package/tui/src/services/api/client.ts +33 -12
- package/tui/src/services/api/ummaya.ts +70 -16
- package/tui/src/skills/bundled/stuck.ts +12 -12
- package/tui/src/state/AppStateStore.ts +7 -0
- package/tui/src/tools/AdapterTool/AdapterTool.ts +590 -7
- package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +43 -17
- package/tui/src/tools/LookupPrimitive/prompt.ts +7 -6
- package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +40 -19
- package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +25 -9
- package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +25 -9
- package/tui/src/tools/_shared/citizenUserText.ts +49 -0
- package/tui/src/tools/_shared/directPublicDataGuard.ts +362 -0
- package/tui/src/tools/_shared/kmaAnalysisGuard.ts +197 -0
- package/tui/src/tools/_shared/kmaAviationGuard.ts +70 -0
- package/tui/src/tools/_shared/locationInputRepair.ts +112 -0
- package/tui/src/tools/_shared/nmcAedGuard.ts +234 -0
- package/tui/src/tools/_shared/protectedCheckGuard.ts +207 -0
- package/tui/src/tools/_shared/rootPrimitiveInput.ts +67 -0
- package/tui/src/tools/_shared/textToolCallGuard.ts +91 -0
- package/tui/src/tools/_shared/toolChoiceRepair.ts +866 -0
- package/tui/src/utils/attachments.ts +1 -1
- package/tui/src/utils/kExaoneReasoning.ts +138 -0
- package/tui/src/utils/messages.ts +1 -0
- package/tui/src/utils/multiToolLayout.ts +13 -0
- package/tui/src/utils/processUserInput/processSlashCommand.tsx +2 -2
- package/tui/src/utils/processUserInput/processUserInput.ts +26 -0
- package/tui/src/utils/settings/applySettingsChange.ts +4 -0
- package/tui/src/utils/settings/types.ts +9 -3
- package/tui/src/utils/stats.ts +1 -1
- package/uv.lock +1 -15
- package/assets/copilot-gate-logo.svg +0 -58
- package/assets/govon-logo.svg +0 -40
- package/src/ummaya/eval/__init__.py +0 -5
- package/src/ummaya/eval/retrieval.py +0 -713
- package/tui/src/utils/messageStream.ts +0 -186
package/src/ummaya/llm/client.py
CHANGED
|
@@ -10,7 +10,7 @@ import logging
|
|
|
10
10
|
import os
|
|
11
11
|
import random
|
|
12
12
|
import time
|
|
13
|
-
from collections.abc import AsyncIterator
|
|
13
|
+
from collections.abc import AsyncIterator, Iterator
|
|
14
14
|
from contextvars import Token
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from typing import TYPE_CHECKING, cast
|
|
@@ -39,6 +39,7 @@ from ummaya.llm.models import (
|
|
|
39
39
|
ToolCall,
|
|
40
40
|
ToolDefinition,
|
|
41
41
|
)
|
|
42
|
+
from ummaya.llm.reasoning import ReasoningMode, resolve_reasoning_policy
|
|
42
43
|
from ummaya.llm.usage import UsageTracker
|
|
43
44
|
from ummaya.observability.semconv import (
|
|
44
45
|
ERROR_TYPE,
|
|
@@ -238,6 +239,7 @@ class LLMClient:
|
|
|
238
239
|
presence_penalty: float = 0.0,
|
|
239
240
|
max_tokens: int = 1024,
|
|
240
241
|
stop: list[str] | None = None,
|
|
242
|
+
reasoning_mode: ReasoningMode | str | None = None,
|
|
241
243
|
) -> ChatCompletionResponse:
|
|
242
244
|
"""Send a non-streaming chat completion request.
|
|
243
245
|
|
|
@@ -271,6 +273,7 @@ class LLMClient:
|
|
|
271
273
|
tools=tools,
|
|
272
274
|
tool_choice=tool_choice,
|
|
273
275
|
stream=False,
|
|
276
|
+
reasoning_mode=reasoning_mode,
|
|
274
277
|
)
|
|
275
278
|
|
|
276
279
|
logger.debug(
|
|
@@ -365,6 +368,7 @@ class LLMClient:
|
|
|
365
368
|
presence_penalty: float = 0.0,
|
|
366
369
|
max_tokens: int = 1024,
|
|
367
370
|
stop: list[str] | None = None,
|
|
371
|
+
reasoning_mode: ReasoningMode | str | None = None,
|
|
368
372
|
) -> AsyncIterator[StreamEvent]:
|
|
369
373
|
"""Send a streaming chat completion request.
|
|
370
374
|
|
|
@@ -403,7 +407,9 @@ class LLMClient:
|
|
|
403
407
|
tools=tools,
|
|
404
408
|
tool_choice=tool_choice,
|
|
405
409
|
stream=True,
|
|
410
|
+
reasoning_mode=reasoning_mode,
|
|
406
411
|
)
|
|
412
|
+
allow_reasoning = payload.get("include_reasoning") is True
|
|
407
413
|
|
|
408
414
|
logger.debug(
|
|
409
415
|
"LLM stream request: model=%s messages=%d",
|
|
@@ -447,7 +453,11 @@ class LLMClient:
|
|
|
447
453
|
_finalize: dict[str, object] = {}
|
|
448
454
|
|
|
449
455
|
try:
|
|
450
|
-
async for event in self._stream_with_retry(
|
|
456
|
+
async for event in self._stream_with_retry(
|
|
457
|
+
payload,
|
|
458
|
+
_finalize,
|
|
459
|
+
allow_reasoning=allow_reasoning,
|
|
460
|
+
):
|
|
451
461
|
active_span.detach()
|
|
452
462
|
yield event
|
|
453
463
|
active_span.attach()
|
|
@@ -490,6 +500,8 @@ class LLMClient:
|
|
|
490
500
|
self,
|
|
491
501
|
payload: dict[str, object],
|
|
492
502
|
_finalize: dict[str, object],
|
|
503
|
+
*,
|
|
504
|
+
allow_reasoning: bool,
|
|
493
505
|
) -> AsyncIterator[StreamEvent]:
|
|
494
506
|
"""Execute stream() with Retry-After-first backoff loop (T015/T016).
|
|
495
507
|
|
|
@@ -607,7 +619,10 @@ class LLMClient:
|
|
|
607
619
|
if chunk_info.get("model"):
|
|
608
620
|
_response_model = chunk_info["model"]
|
|
609
621
|
|
|
610
|
-
async for event in self._parse_sse_line(
|
|
622
|
+
async for event in self._parse_sse_line(
|
|
623
|
+
line,
|
|
624
|
+
allow_reasoning=allow_reasoning,
|
|
625
|
+
):
|
|
611
626
|
yield event
|
|
612
627
|
if event.type == "done":
|
|
613
628
|
_duration_ms = (time.monotonic() - _stream_start) * 1000
|
|
@@ -848,7 +863,12 @@ class LLMClient:
|
|
|
848
863
|
if i + step < n:
|
|
849
864
|
await asyncio.sleep(_LLM_STREAM_PACE_S)
|
|
850
865
|
|
|
851
|
-
async def _parse_sse_line(
|
|
866
|
+
async def _parse_sse_line(
|
|
867
|
+
self,
|
|
868
|
+
line: str,
|
|
869
|
+
*,
|
|
870
|
+
allow_reasoning: bool = False,
|
|
871
|
+
) -> AsyncIterator[StreamEvent]:
|
|
852
872
|
"""Parse a single SSE line and yield corresponding StreamEvent(s)."""
|
|
853
873
|
if not line or not line.startswith("data: "):
|
|
854
874
|
return
|
|
@@ -859,14 +879,35 @@ class LLMClient:
|
|
|
859
879
|
yield StreamEvent(type="done")
|
|
860
880
|
return
|
|
861
881
|
|
|
882
|
+
chunk = self._decode_sse_payload(payload_text)
|
|
883
|
+
if chunk is None:
|
|
884
|
+
return
|
|
885
|
+
|
|
886
|
+
usage_event = self._usage_event_from_chunk(chunk)
|
|
887
|
+
if usage_event is not None:
|
|
888
|
+
yield usage_event
|
|
889
|
+
|
|
890
|
+
async for event in self._events_from_sse_choices(
|
|
891
|
+
chunk,
|
|
892
|
+
allow_reasoning=allow_reasoning,
|
|
893
|
+
):
|
|
894
|
+
yield event
|
|
895
|
+
|
|
896
|
+
def _decode_sse_payload(self, payload_text: str) -> dict[str, object] | None:
|
|
897
|
+
"""Decode a JSON SSE payload, returning None for malformed chunks."""
|
|
862
898
|
try:
|
|
863
899
|
chunk = json.loads(payload_text)
|
|
864
900
|
except json.JSONDecodeError:
|
|
865
901
|
logger.warning("Failed to parse SSE chunk: %r", payload_text)
|
|
866
|
-
return
|
|
902
|
+
return None
|
|
903
|
+
return chunk if isinstance(chunk, dict) else None
|
|
867
904
|
|
|
905
|
+
def _usage_event_from_chunk(self, chunk: dict[str, object]) -> StreamEvent | None:
|
|
906
|
+
"""Debit usage from a stream chunk and return the corresponding event."""
|
|
868
907
|
if "usage" in chunk and chunk["usage"] is not None:
|
|
869
908
|
raw_usage = chunk["usage"]
|
|
909
|
+
if not isinstance(raw_usage, dict):
|
|
910
|
+
return None
|
|
870
911
|
usage = TokenUsage(
|
|
871
912
|
input_tokens=raw_usage.get("prompt_tokens", 0),
|
|
872
913
|
output_tokens=raw_usage.get("completion_tokens", 0),
|
|
@@ -877,66 +918,104 @@ class LLMClient:
|
|
|
877
918
|
usage.output_tokens,
|
|
878
919
|
)
|
|
879
920
|
self._usage.debit(usage)
|
|
880
|
-
|
|
921
|
+
return StreamEvent(type="usage", usage=usage)
|
|
922
|
+
return None
|
|
881
923
|
|
|
924
|
+
async def _events_from_sse_choices(
|
|
925
|
+
self,
|
|
926
|
+
chunk: dict[str, object],
|
|
927
|
+
*,
|
|
928
|
+
allow_reasoning: bool,
|
|
929
|
+
) -> AsyncIterator[StreamEvent]:
|
|
930
|
+
"""Yield stream events for the first OpenAI-compatible choice delta."""
|
|
882
931
|
choices = chunk.get("choices")
|
|
883
932
|
if not choices:
|
|
884
933
|
return
|
|
934
|
+
if not isinstance(choices, list):
|
|
935
|
+
return
|
|
885
936
|
|
|
886
937
|
choice = choices[0]
|
|
938
|
+
if not isinstance(choice, dict):
|
|
939
|
+
return
|
|
887
940
|
delta = choice.get("delta", {})
|
|
941
|
+
if not isinstance(delta, dict):
|
|
942
|
+
return
|
|
888
943
|
|
|
944
|
+
async for event in self._events_from_sse_delta(
|
|
945
|
+
delta,
|
|
946
|
+
allow_reasoning=allow_reasoning,
|
|
947
|
+
):
|
|
948
|
+
yield event
|
|
949
|
+
|
|
950
|
+
async def _events_from_sse_delta(
|
|
951
|
+
self,
|
|
952
|
+
delta: dict[str, object],
|
|
953
|
+
*,
|
|
954
|
+
allow_reasoning: bool,
|
|
955
|
+
) -> AsyncIterator[StreamEvent]:
|
|
956
|
+
"""Yield content, reasoning, and tool-call events from a delta object."""
|
|
889
957
|
if "content" in delta and delta["content"] is not None:
|
|
890
958
|
# CC reference: services/api/claude.ts:2113 (text_delta content_block_delta).
|
|
891
|
-
content = delta["content"]
|
|
959
|
+
content = str(delta["content"])
|
|
892
960
|
async for sub in self._pace_text_chunk(content, "content"):
|
|
893
961
|
yield sub
|
|
894
962
|
elif "reasoning_content" in delta and delta["reasoning_content"] is not None:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
963
|
+
async for sub in self._events_from_reasoning_delta(
|
|
964
|
+
str(delta["reasoning_content"]),
|
|
965
|
+
allow_reasoning=allow_reasoning,
|
|
966
|
+
):
|
|
967
|
+
yield sub
|
|
968
|
+
|
|
969
|
+
if "tool_calls" in delta and delta["tool_calls"]:
|
|
970
|
+
for event in self._events_from_tool_call_deltas(delta["tool_calls"]):
|
|
971
|
+
yield event
|
|
972
|
+
|
|
973
|
+
async def _events_from_reasoning_delta(
|
|
974
|
+
self,
|
|
975
|
+
reasoning_text: str,
|
|
976
|
+
*,
|
|
977
|
+
allow_reasoning: bool,
|
|
978
|
+
) -> AsyncIterator[StreamEvent]:
|
|
979
|
+
"""Yield reasoning text only when the request opted into reasoning parsing."""
|
|
980
|
+
if not allow_reasoning:
|
|
904
981
|
logger.debug(
|
|
905
|
-
"
|
|
982
|
+
"Suppressed unexpected reasoning_content while include_reasoning=false (len=%d)",
|
|
906
983
|
len(reasoning_text),
|
|
907
984
|
)
|
|
908
|
-
|
|
909
|
-
|
|
985
|
+
return
|
|
986
|
+
logger.debug(
|
|
987
|
+
"Forwarding reasoning_content as thinking_delta (len=%d)",
|
|
988
|
+
len(reasoning_text),
|
|
989
|
+
)
|
|
990
|
+
async for sub in self._pace_text_chunk(reasoning_text, "thinking"):
|
|
991
|
+
yield sub
|
|
910
992
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
func =
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
)
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
function_name=func.get("name"),
|
|
938
|
-
function_args_delta=func.get("arguments"),
|
|
939
|
-
)
|
|
993
|
+
def _events_from_tool_call_deltas(self, tool_calls: object) -> Iterator[StreamEvent]:
|
|
994
|
+
"""Yield tool-call deltas without logging raw argument content."""
|
|
995
|
+
if not isinstance(tool_calls, list):
|
|
996
|
+
return
|
|
997
|
+
for tc_delta in tool_calls:
|
|
998
|
+
if not isinstance(tc_delta, dict):
|
|
999
|
+
continue
|
|
1000
|
+
func = tc_delta.get("function", {})
|
|
1001
|
+
if not isinstance(func, dict):
|
|
1002
|
+
func = {}
|
|
1003
|
+
_args_field = func.get("arguments")
|
|
1004
|
+
_args_len = len(_args_field) if isinstance(_args_field, str) else 0
|
|
1005
|
+
logger.debug(
|
|
1006
|
+
"tool_call_delta idx=%s id=%s name=%r args_len=%d",
|
|
1007
|
+
tc_delta.get("index"),
|
|
1008
|
+
tc_delta.get("id"),
|
|
1009
|
+
func.get("name"),
|
|
1010
|
+
_args_len,
|
|
1011
|
+
)
|
|
1012
|
+
yield StreamEvent(
|
|
1013
|
+
type="tool_call_delta",
|
|
1014
|
+
tool_call_index=tc_delta.get("index"),
|
|
1015
|
+
tool_call_id=tc_delta.get("id"),
|
|
1016
|
+
function_name=func.get("name"),
|
|
1017
|
+
function_args_delta=func.get("arguments"),
|
|
1018
|
+
)
|
|
940
1019
|
|
|
941
1020
|
def _build_payload(
|
|
942
1021
|
self,
|
|
@@ -950,6 +1029,7 @@ class LLMClient:
|
|
|
950
1029
|
tools: list[ToolDefinition | dict[str, object]] | None = None,
|
|
951
1030
|
tool_choice: str | dict[str, object] | None = None,
|
|
952
1031
|
stream: bool,
|
|
1032
|
+
reasoning_mode: ReasoningMode | str | None = None,
|
|
953
1033
|
) -> dict[str, object]:
|
|
954
1034
|
"""Construct the JSON payload for a chat completions request.
|
|
955
1035
|
|
|
@@ -979,13 +1059,7 @@ class LLMClient:
|
|
|
979
1059
|
is not treated as normal assistant text and is never required for the
|
|
980
1060
|
default CLI/TUI path.
|
|
981
1061
|
"""
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
enable_thinking = os.environ.get("UMMAYA_K_EXAONE_THINKING", "false").lower() in (
|
|
985
|
-
"true",
|
|
986
|
-
"1",
|
|
987
|
-
"yes",
|
|
988
|
-
)
|
|
1062
|
+
reasoning = resolve_reasoning_policy(reasoning_mode)
|
|
989
1063
|
|
|
990
1064
|
payload: dict[str, object] = {
|
|
991
1065
|
"model": self._config.model,
|
|
@@ -999,7 +1073,9 @@ class LLMClient:
|
|
|
999
1073
|
# enable_thinking=False the model emits an answer directly
|
|
1000
1074
|
# without the <think>...</think> trace, dropping first-token
|
|
1001
1075
|
# latency from ~60-180s to <10s for typical citizen prompts.
|
|
1002
|
-
"chat_template_kwargs": {"enable_thinking": enable_thinking},
|
|
1076
|
+
"chat_template_kwargs": {"enable_thinking": reasoning.enable_thinking},
|
|
1077
|
+
"parse_reasoning": reasoning.parse_reasoning,
|
|
1078
|
+
"include_reasoning": reasoning.include_reasoning,
|
|
1003
1079
|
}
|
|
1004
1080
|
if stop is not None:
|
|
1005
1081
|
payload["stop"] = stop
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""K-EXAONE/FriendliAI reasoning payload policy."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
ReasoningMode = Literal["fast", "balanced", "deep", "diagnostic", "auto"]
|
|
12
|
+
ReasoningModeSource = Literal["env", "session", "legacy-env", "default"]
|
|
13
|
+
|
|
14
|
+
_MODES: set[str] = {"fast", "balanced", "deep", "diagnostic", "auto"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ResolvedReasoningPolicy:
|
|
19
|
+
"""Provider-facing reasoning policy for one request."""
|
|
20
|
+
|
|
21
|
+
mode: ReasoningMode
|
|
22
|
+
source: ReasoningModeSource
|
|
23
|
+
enable_thinking: bool
|
|
24
|
+
parse_reasoning: bool
|
|
25
|
+
include_reasoning: bool
|
|
26
|
+
persist_thinking: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_reasoning_mode(value: object) -> ReasoningMode | None:
|
|
30
|
+
"""Return a valid reasoning mode from an untrusted value."""
|
|
31
|
+
if value is None:
|
|
32
|
+
return None
|
|
33
|
+
normalized = str(value).lower()
|
|
34
|
+
if normalized in _MODES:
|
|
35
|
+
return normalized # type: ignore[return-value]
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_reasoning_policy(
|
|
40
|
+
reasoning_mode: ReasoningMode | str | None = None,
|
|
41
|
+
*,
|
|
42
|
+
env: Mapping[str, str] | None = None,
|
|
43
|
+
) -> ResolvedReasoningPolicy:
|
|
44
|
+
"""Resolve request, settings, and env state into FriendliAI payload fields."""
|
|
45
|
+
effective_env = os.environ if env is None else env
|
|
46
|
+
env_mode = parse_reasoning_mode(effective_env.get("UMMAYA_K_EXAONE_REASONING_MODE"))
|
|
47
|
+
if env_mode is not None:
|
|
48
|
+
return _policy_for(env_mode, "env")
|
|
49
|
+
|
|
50
|
+
explicit_mode = parse_reasoning_mode(reasoning_mode)
|
|
51
|
+
if explicit_mode is not None:
|
|
52
|
+
return _policy_for(explicit_mode, "session")
|
|
53
|
+
|
|
54
|
+
legacy_mode = _legacy_thinking_mode(effective_env)
|
|
55
|
+
if legacy_mode is not None:
|
|
56
|
+
return _policy_for(legacy_mode, "legacy-env")
|
|
57
|
+
|
|
58
|
+
return _policy_for("balanced", "default")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _legacy_thinking_mode(env: Mapping[str, str]) -> ReasoningMode | None:
|
|
62
|
+
raw = env.get("UMMAYA_K_EXAONE_THINKING")
|
|
63
|
+
if raw is None:
|
|
64
|
+
return None
|
|
65
|
+
normalized = raw.lower()
|
|
66
|
+
if normalized in {"1", "true", "yes"}:
|
|
67
|
+
return "deep"
|
|
68
|
+
if normalized in {"0", "false", "no"}:
|
|
69
|
+
return "fast"
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _policy_for(
|
|
74
|
+
mode: ReasoningMode,
|
|
75
|
+
source: ReasoningModeSource,
|
|
76
|
+
) -> ResolvedReasoningPolicy:
|
|
77
|
+
enable_thinking = mode in {"deep", "diagnostic"}
|
|
78
|
+
return ResolvedReasoningPolicy(
|
|
79
|
+
mode=mode,
|
|
80
|
+
source=source,
|
|
81
|
+
enable_thinking=enable_thinking,
|
|
82
|
+
parse_reasoning=True,
|
|
83
|
+
include_reasoning=enable_thinking,
|
|
84
|
+
)
|
|
@@ -138,7 +138,11 @@ _VERIFY_FAMILIES: list[dict[str, Any]] = [
|
|
|
138
138
|
"네이버인증",
|
|
139
139
|
"토스인증",
|
|
140
140
|
"정부24",
|
|
141
|
+
"홈택스",
|
|
142
|
+
"국세청",
|
|
141
143
|
"민원",
|
|
144
|
+
"증명원",
|
|
145
|
+
"소득금액증명원",
|
|
142
146
|
"주민등록등본",
|
|
143
147
|
"발급",
|
|
144
148
|
],
|
|
@@ -149,12 +153,20 @@ _VERIFY_FAMILIES: list[dict[str, Any]] = [
|
|
|
149
153
|
"Naver",
|
|
150
154
|
"Toss",
|
|
151
155
|
"gov24",
|
|
156
|
+
"hometax",
|
|
157
|
+
"national tax service",
|
|
152
158
|
"civil petition",
|
|
159
|
+
"income certificate",
|
|
153
160
|
"resident registration certificate",
|
|
154
161
|
"issuance",
|
|
155
162
|
],
|
|
156
163
|
"endpoint": "https://api.gateway.ummaya.gov.kr/v1/verify/simple",
|
|
157
164
|
"policy_authority": "https://www.kftc.or.kr/",
|
|
165
|
+
"scope_rules": (
|
|
166
|
+
"Scope rule: simple-auth module verification uses the canonical "
|
|
167
|
+
"check tool_id 'mock_verify_module_simple_auth'. Do not set tool_id "
|
|
168
|
+
"to 'simple_auth_module'; that string is only the internal family_hint."
|
|
169
|
+
),
|
|
158
170
|
},
|
|
159
171
|
{
|
|
160
172
|
"tool_id": "mock_verify_module_any_id_sso",
|
|
@@ -221,7 +233,9 @@ _VERIFY_FAMILIES: list[dict[str, Any]] = [
|
|
|
221
233
|
"scope_rules": (
|
|
222
234
|
"Scope rule: mobile ID identity verification uses exactly scope_list "
|
|
223
235
|
"['check:mobile_id.identity']; do not invent find:identity.info "
|
|
224
|
-
"or find:identity.verify scopes."
|
|
236
|
+
"or find:identity.verify scopes. Use the canonical check tool_id "
|
|
237
|
+
"'mock_verify_mobile_id'. Do not set tool_id to 'mobile_id'; that "
|
|
238
|
+
"string is only the internal family_hint."
|
|
225
239
|
),
|
|
226
240
|
},
|
|
227
241
|
{
|
|
@@ -304,6 +318,8 @@ def _verify_to_govapitool(entry: dict[str, Any]) -> GovAPITool:
|
|
|
304
318
|
output_schema=_OpaqueOutput,
|
|
305
319
|
llm_description=(
|
|
306
320
|
"Use only through the core check primitive: "
|
|
321
|
+
f"check({{tool_id: '{entry['tool_id']}', params: {{...}}}}). "
|
|
322
|
+
"Canonical action-scope form: "
|
|
307
323
|
f"check(tool_id='{entry['tool_id']}', params={{...}}). "
|
|
308
324
|
"Do not call this adapter through find.\n\n"
|
|
309
325
|
f"{scope_clause}"
|
|
@@ -30,6 +30,7 @@ from ummaya.tools.envelope import make_error_envelope, normalize
|
|
|
30
30
|
from ummaya.tools.errors import (
|
|
31
31
|
EnvelopeNormalizationError,
|
|
32
32
|
LookupErrorReason,
|
|
33
|
+
ToolExecutionError,
|
|
33
34
|
ToolNotFoundError,
|
|
34
35
|
UmmayaToolError,
|
|
35
36
|
)
|
|
@@ -79,6 +80,11 @@ def _classify_adapter_exception(exc: Exception) -> tuple[LookupErrorReason, bool
|
|
|
79
80
|
if isinstance(exc, Layer3GateViolation):
|
|
80
81
|
# Programming error: stub handler was reached despite auth-gate — never retry.
|
|
81
82
|
return (LookupErrorReason.upstream_unavailable, False)
|
|
83
|
+
if isinstance(exc, ToolExecutionError) and isinstance(exc.cause, httpx.HTTPStatusError):
|
|
84
|
+
status_code = exc.cause.response.status_code
|
|
85
|
+
if status_code in {400, 401, 403, 404}:
|
|
86
|
+
return (LookupErrorReason.upstream_unavailable, False)
|
|
87
|
+
return (LookupErrorReason.upstream_unavailable, True)
|
|
82
88
|
if isinstance(exc, LiveAdapterProxyConfigurationError):
|
|
83
89
|
return (LookupErrorReason.upstream_unavailable, False)
|
|
84
90
|
if isinstance(exc, (ValueError, TypeError, KMADomainError)):
|
|
@@ -118,6 +124,19 @@ def _adapter_validation_recovery_hint(tool_id: str) -> str:
|
|
|
118
124
|
" prefer nmc_emergency_search when authenticated, or a location POI"
|
|
119
125
|
" search when no authenticated NMC session is present."
|
|
120
126
|
)
|
|
127
|
+
if tool_id == "nmc_aed_site_locate":
|
|
128
|
+
return (
|
|
129
|
+
" REGION FILTER ONLY: this AED adapter uses official NMC Q0/Q1"
|
|
130
|
+
" region filters, not ER-search mode. If you have"
|
|
131
|
+
" a place name, call kakao_keyword_search({query:'<장소명>'}), then"
|
|
132
|
+
" kakao_coord_to_region({lat:<lat>, lon:<lon>}). Re-invoke this"
|
|
133
|
+
" tool as nmc_aed_site_locate({q0:region.region_1depth_name,"
|
|
134
|
+
" q1:region.region_2depth_name, page_no:1, num_of_rows:10,"
|
|
135
|
+
" origin_lat:<original place lat>, origin_lon:<original place lon>})."
|
|
136
|
+
" origin_lat/origin_lon are optional client-side distance-sort fields;"
|
|
137
|
+
" copy them from the coordinate-producing locate result when available."
|
|
138
|
+
" Do NOT pass mode, lat/lon, or ER-only fields."
|
|
139
|
+
)
|
|
121
140
|
if tool_id in {"kakao_coord_to_region", "sgis_adm_cd_lookup"}:
|
|
122
141
|
return (
|
|
123
142
|
" COPY EXACT COORDINATES: call this reverse-geocode adapter with"
|
|
@@ -311,8 +330,9 @@ class ToolExecutor:
|
|
|
311
330
|
reason=reason,
|
|
312
331
|
message=(
|
|
313
332
|
f"Adapter '{tool_id}' raised {type(exc).__name__}: {str(exc)[:240]}. "
|
|
314
|
-
"Do NOT fabricate a response from prior knowledge;
|
|
315
|
-
"
|
|
333
|
+
"Do NOT fabricate a response from prior knowledge; explain that "
|
|
334
|
+
"the lookup failed, cite the official agency channel, and ask "
|
|
335
|
+
"before trying a different adapter."
|
|
316
336
|
),
|
|
317
337
|
request_id=request_id,
|
|
318
338
|
elapsed_ms=_elapsed(),
|
|
@@ -480,20 +500,20 @@ class ToolExecutor:
|
|
|
480
500
|
recovery_hint = _adapter_validation_recovery_hint(tool_id)
|
|
481
501
|
if not recovery_hint and tool_id == "nmc_emergency_search":
|
|
482
502
|
recovery_hint = (
|
|
483
|
-
" LOCATE FIRST: call
|
|
503
|
+
" LOCATE FIRST: call a concrete locate adapter from"
|
|
484
504
|
" <available_adapters>. For a named place use"
|
|
485
|
-
"
|
|
486
|
-
"
|
|
487
|
-
"
|
|
488
|
-
"
|
|
505
|
+
" kakao_keyword_search({query:'<지역명>'}), then call"
|
|
506
|
+
" kakao_coord_to_region({lat:<lat>, lon:<lon>}). Re-invoke"
|
|
507
|
+
" this tool as nmc_emergency_search({mode:'region',"
|
|
508
|
+
" q0:region.region_1depth_name,"
|
|
489
509
|
" q1:region.region_2depth_name, origin_lat:<lat>, origin_lon:<lon>,"
|
|
490
|
-
" limit:<N>}. Copy decimal WGS-84 coordinates exactly from locate;"
|
|
510
|
+
" limit:<N>}). Copy decimal WGS-84 coordinates exactly from locate;"
|
|
491
511
|
" do NOT round, guess coordinates, or set QN unless the citizen"
|
|
492
512
|
" gave a specific institution name."
|
|
493
513
|
)
|
|
494
514
|
elif not recovery_hint and need_resolve:
|
|
495
515
|
recovery_hint = (
|
|
496
|
-
" LOCATE FIRST: call
|
|
516
|
+
" LOCATE FIRST: call the appropriate concrete locate adapter"
|
|
497
517
|
" from <available_adapters> to obtain the missing coordinates /"
|
|
498
518
|
" admin code, then re-invoke this tool with the returned values."
|
|
499
519
|
" Do NOT guess coordinates or codes from prior knowledge."
|
|
@@ -566,8 +586,8 @@ class ToolExecutor:
|
|
|
566
586
|
f"Adapter '{tool_id}' raised an exception during upstream call. "
|
|
567
587
|
f"Detail: {_exc_summary}. "
|
|
568
588
|
"Do NOT fabricate a response from prior knowledge — tell the citizen "
|
|
569
|
-
"the lookup failed, cite the official agency channel, and
|
|
570
|
-
"
|
|
589
|
+
"the lookup failed, cite the official agency channel, and ask "
|
|
590
|
+
"before retrying or trying a different tool."
|
|
571
591
|
),
|
|
572
592
|
request_id=request_id,
|
|
573
593
|
elapsed_ms=_elapsed(),
|
|
@@ -637,7 +657,7 @@ class ToolExecutor:
|
|
|
637
657
|
f"expected envelope schema. Detail: {_exc_detail}. "
|
|
638
658
|
"Do NOT fabricate a response from prior knowledge — tell the citizen "
|
|
639
659
|
"the data could not be parsed, cite the official agency channel, and "
|
|
640
|
-
"
|
|
660
|
+
"ask before retrying or trying a different tool."
|
|
641
661
|
),
|
|
642
662
|
request_id=request_id,
|
|
643
663
|
elapsed_ms=_elapsed(),
|
|
@@ -19,8 +19,7 @@ business logic. Reference: PyKakao 1.x ``Local`` class
|
|
|
19
19
|
endpoints as six methods on a single facade — the industry-standard
|
|
20
20
|
Korean wrapper for this API. UMMAYA originally shipped only
|
|
21
21
|
``search_address`` (Spec 022); ``search_keyword`` is added here to close
|
|
22
|
-
the POI-coverage gap captured
|
|
23
|
-
``specs/integration-verification/donga-univ-poi-bug/``.
|
|
22
|
+
the POI-coverage gap captured during historical live-location debugging.
|
|
24
23
|
|
|
25
24
|
Authentication: REST API key via ``Authorization: KakaoAK {key}`` header.
|
|
26
25
|
Key source: ``UMMAYA_KAKAO_API_KEY`` environment variable.
|