ummaya 0.2.3 → 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 +1287 -54
- 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 +48 -18
- 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
|
@@ -8,6 +8,7 @@ from datetime import UTC, datetime
|
|
|
8
8
|
from ummaya.tools.verified_data_go_kr._models import VerifiedAdapterSpec
|
|
9
9
|
|
|
10
10
|
_LAST_VERIFIED = datetime(2026, 5, 16, tzinfo=UTC)
|
|
11
|
+
_LAST_VERIFIED_2026_05_28 = datetime(2026, 5, 28, tzinfo=UTC)
|
|
11
12
|
_DATA_GO_KR_KEY = "UMMAYA_DATA_GO_KR_API_KEY"
|
|
12
13
|
|
|
13
14
|
|
|
@@ -67,9 +68,14 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
67
68
|
policy_url=_data_go_policy("15073861"),
|
|
68
69
|
policy_text="공공데이터포털 인증키 기반 한국환경공단 에어코리아 대기오염정보 조회 OpenAPI.",
|
|
69
70
|
last_verified=_LAST_VERIFIED,
|
|
70
|
-
search_hint=
|
|
71
|
+
search_hint=(
|
|
72
|
+
"15073861 AirKorea 에어코리아 대기오염 시도별 대기질 미세먼지 "
|
|
73
|
+
"sidoName 서울 부산 경기 find"
|
|
74
|
+
),
|
|
71
75
|
llm_description=(
|
|
72
|
-
"시도명(sido_name)으로 에어코리아 시도별
|
|
76
|
+
"짧은 시도명(sido_name: 서울, 부산, 경기 등)으로 에어코리아 시도별 "
|
|
77
|
+
"실시간 측정소 대기질 공개 데이터를 조회한다. 부산광역시처럼 긴 행정명은 "
|
|
78
|
+
"AirKorea 계약상 부산으로 줄여 호출한다."
|
|
73
79
|
),
|
|
74
80
|
trigger_examples=["서울 대기질 측정소 데이터 조회해줘"],
|
|
75
81
|
),
|
|
@@ -148,7 +154,9 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
148
154
|
last_verified=_LAST_VERIFIED,
|
|
149
155
|
search_hint="15098529 TAGO 버스노선 cityCode routeNo bus route find",
|
|
150
156
|
llm_description=(
|
|
151
|
-
"
|
|
157
|
+
"Search official TAGO bus route data by city_code and citizen-visible "
|
|
158
|
+
"route_no. Use this before tago_bus_location_search when route_id is "
|
|
159
|
+
"unknown."
|
|
152
160
|
),
|
|
153
161
|
),
|
|
154
162
|
VerifiedAdapterSpec(
|
|
@@ -172,9 +180,44 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
172
180
|
policy_url=_data_go_policy("15098530"),
|
|
173
181
|
policy_text="공공데이터포털 인증키 기반 국토교통부 TAGO 버스도착정보 조회 OpenAPI.",
|
|
174
182
|
last_verified=_LAST_VERIFIED,
|
|
175
|
-
search_hint="15098530 TAGO 버스도착 정류소 nodeId arrival bus find",
|
|
183
|
+
search_hint="15098530 TAGO 버스도착 정류소 nodeId routeno routeid arrival bus find",
|
|
176
184
|
llm_description=(
|
|
177
|
-
"
|
|
185
|
+
"Search official TAGO bus-arrival predictions by city_code and node_id. "
|
|
186
|
+
"If the citizen gives a stop name such as 부산역 instead of node_id, call "
|
|
187
|
+
"tago_bus_station_search first and reuse its nodeid. If the citizen names "
|
|
188
|
+
"a route such as 1001, pass route_no as an optional client-side filter "
|
|
189
|
+
"against the returned TAGO routeno field; use route_id from "
|
|
190
|
+
"tago_bus_route_search when the route number is ambiguous."
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
VerifiedAdapterSpec(
|
|
194
|
+
dataset_id="15098529",
|
|
195
|
+
tool_id="tago_bus_route_station_search",
|
|
196
|
+
module_name="tago_bus_route_station",
|
|
197
|
+
name_ko="국토교통부 TAGO 노선별 경유정류소 조회",
|
|
198
|
+
ministry="MOLIT",
|
|
199
|
+
category=["transport", "bus", "public-data"],
|
|
200
|
+
endpoint="https://apis.data.go.kr/1613000/BusRouteInfoInqireService/getRouteAcctoThrghSttnList",
|
|
201
|
+
env_var=_DATA_GO_KR_KEY,
|
|
202
|
+
auth_query_param="serviceKey",
|
|
203
|
+
response_format="xml",
|
|
204
|
+
query_param_map={
|
|
205
|
+
"city_code": "cityCode",
|
|
206
|
+
"route_id": "routeId",
|
|
207
|
+
"page_no": "pageNo",
|
|
208
|
+
"num_of_rows": "numOfRows",
|
|
209
|
+
},
|
|
210
|
+
evidence_path="docs/api/data-go-kr-candidate-docs/15098529/probes/live-2026-05-28/tago-bus-route-station.body.xml",
|
|
211
|
+
policy_url=_data_go_policy("15098529"),
|
|
212
|
+
policy_text="공공데이터포털 인증키 기반 국토교통부 TAGO 버스노선정보 조회 OpenAPI.",
|
|
213
|
+
last_verified=_LAST_VERIFIED_2026_05_28,
|
|
214
|
+
search_hint=("15098529 TAGO 노선별 경유정류소 routeId nodenm nodeid nodeord bus stop find"),
|
|
215
|
+
llm_description=(
|
|
216
|
+
"Search the official TAGO route-station list by city_code and route_id. "
|
|
217
|
+
"For a citizen query that combines a route number and place, call "
|
|
218
|
+
"tago_bus_route_search to get route_id, then call this tool with node_nm "
|
|
219
|
+
"as a client-side filter against returned nodenm values. Use the matching "
|
|
220
|
+
"nodeid with tago_bus_arrival_search and include route_no or route_id."
|
|
178
221
|
),
|
|
179
222
|
),
|
|
180
223
|
VerifiedAdapterSpec(
|
|
@@ -199,7 +242,10 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
199
242
|
policy_text="공공데이터포털 인증키 기반 국토교통부 TAGO 버스위치정보 조회 OpenAPI.",
|
|
200
243
|
last_verified=_LAST_VERIFIED,
|
|
201
244
|
search_hint="15098533 TAGO 버스위치 routeId bus location find",
|
|
202
|
-
llm_description=
|
|
245
|
+
llm_description=(
|
|
246
|
+
"Search official TAGO bus-location data by city_code and route_id. "
|
|
247
|
+
"Use tago_bus_route_search first when the citizen gives only a route number."
|
|
248
|
+
),
|
|
203
249
|
),
|
|
204
250
|
VerifiedAdapterSpec(
|
|
205
251
|
dataset_id="15098534",
|
|
@@ -225,8 +271,9 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
225
271
|
last_verified=_LAST_VERIFIED,
|
|
226
272
|
search_hint="15098534 TAGO 버스정류소 nodeNm nodeNo station find",
|
|
227
273
|
llm_description=(
|
|
228
|
-
"
|
|
229
|
-
"
|
|
274
|
+
"Search official TAGO bus-stop data by city_code, stop-name fragment "
|
|
275
|
+
"(node_nm), or stop number (node_no). For bus-arrival questions with a "
|
|
276
|
+
"named place or stop, call this before tago_bus_arrival_search to obtain nodeid."
|
|
230
277
|
),
|
|
231
278
|
),
|
|
232
279
|
VerifiedAdapterSpec(
|
|
@@ -264,27 +311,51 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
264
311
|
name_ko="조달청 나라장터 입찰공고 조회",
|
|
265
312
|
ministry="PPS",
|
|
266
313
|
category=["procurement", "bid", "public-data"],
|
|
267
|
-
endpoint="https://apis.data.go.kr/1230000/ad/BidPublicInfoService/
|
|
314
|
+
endpoint="https://apis.data.go.kr/1230000/ad/BidPublicInfoService/getBidPblancListInfoCnstwkPPSSrch",
|
|
268
315
|
env_var=_DATA_GO_KR_KEY,
|
|
269
316
|
auth_query_param="serviceKey",
|
|
270
317
|
response_format="json",
|
|
271
318
|
query_param_map={
|
|
272
|
-
"inqry_div": "inqryDiv",
|
|
273
|
-
"bid_ntce_no": "bidNtceNo",
|
|
274
319
|
"page_no": "pageNo",
|
|
275
320
|
"num_of_rows": "numOfRows",
|
|
321
|
+
"inqry_div": "inqryDiv",
|
|
322
|
+
"inqry_bgn_dt": "inqryBgnDt",
|
|
323
|
+
"inqry_end_dt": "inqryEndDt",
|
|
324
|
+
"bid_ntce_nm": "bidNtceNm",
|
|
325
|
+
"ntce_instt_nm": "ntceInsttNm",
|
|
326
|
+
"dminstt_nm": "dminsttNm",
|
|
327
|
+
"prtcpt_lmt_rgn_nm": "prtcptLmtRgnNm",
|
|
328
|
+
"indstryty_nm": "indstrytyNm",
|
|
276
329
|
},
|
|
277
330
|
static_query_params={"type": "json"},
|
|
278
|
-
evidence_path="docs/api/data-go-kr-candidate-docs/15129394/probes/live-2026-05-
|
|
331
|
+
evidence_path="docs/api/data-go-kr-candidate-docs/15129394/probes/live-2026-05-27/pps-bid-construction-search.body.json",
|
|
279
332
|
policy_url=_data_go_policy("15129394"),
|
|
280
333
|
policy_text="공공데이터포털 인증키 기반 조달청 나라장터 입찰공고정보 조회 OpenAPI.",
|
|
281
334
|
last_verified=_LAST_VERIFIED,
|
|
282
|
-
search_hint=
|
|
283
|
-
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
335
|
+
search_hint=(
|
|
336
|
+
"15129394 조달청 나라장터 입찰공고 검색조건 공사조회 전기공사 부산시 "
|
|
337
|
+
"공고게시일시 개찰일시 inqryBgnDt inqryEndDt bidNtceNm ntceInsttNm "
|
|
338
|
+
"dminsttNm prtcptLmtRgnNm cnstrtsiteRgnNm region_name indstrytyNm "
|
|
339
|
+
"bid public procurement construction find"
|
|
287
340
|
),
|
|
341
|
+
llm_description=(
|
|
342
|
+
"Wraps official PPS operation getBidPblancListInfoCnstwkPPSSrch, "
|
|
343
|
+
"the construction-bid search-condition endpoint. Use it for ordinary "
|
|
344
|
+
"citizen list searches such as '이번 주 부산시 전기공사 입찰'. "
|
|
345
|
+
"Fill inqry_bgn_dt and inqry_end_dt as YYYYMMDDHHMM. Use inqry_div='1' "
|
|
346
|
+
"for posted-this-week questions and '2' for bid-opening-date questions. "
|
|
347
|
+
"Keep each upstream call within a 31-day-or-smaller date window; split "
|
|
348
|
+
"broader citizen ranges across multiple calls instead of sending one "
|
|
349
|
+
"over-broad request. "
|
|
350
|
+
"Use bid_ntce_nm for notice keywords such as 전기공사, prtcpt_lmt_rgn_nm "
|
|
351
|
+
"for official participation-limit region restrictions such as 부산광역시, "
|
|
352
|
+
"region_name for UMMAYA client-side filtering against documented PPS "
|
|
353
|
+
"response fields such as cnstrtsiteRgnNm/ntceInsttNm/dminsttNm, and "
|
|
354
|
+
"indstryty_nm for license/industry names such as 전기공사업. Do not "
|
|
355
|
+
"invent notice-number detail inputs for list-search questions; this "
|
|
356
|
+
"adapter no longer exposes the bid-notice-number detail path."
|
|
357
|
+
),
|
|
358
|
+
trigger_examples=["이번 주 부산시 전기공사 입찰 올라온 거 있어?"],
|
|
288
359
|
),
|
|
289
360
|
VerifiedAdapterSpec(
|
|
290
361
|
dataset_id="15134761",
|
|
@@ -450,11 +521,14 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
450
521
|
),
|
|
451
522
|
last_verified=_LAST_VERIFIED,
|
|
452
523
|
search_hint=(
|
|
453
|
-
"15001699 HIRA 의료기관 상세정보
|
|
524
|
+
"15001699 HIRA 의료기관 상세정보 병원 상세 진료과 진료과목 응급실 "
|
|
525
|
+
"주차 진료시간 ykiho hospital detail specialty find"
|
|
454
526
|
),
|
|
455
527
|
llm_description=(
|
|
456
528
|
"암호화 요양기호(ykiho)로 의료기관 세부정보와 "
|
|
457
|
-
"
|
|
529
|
+
"응급실/주차/진료시간/진료과목 공개 데이터를 조회한다. "
|
|
530
|
+
"일반 병원명·지역 검색에는 hira_hospital_search를 먼저 쓰고, "
|
|
531
|
+
"상세정보나 진료과목 확인에는 이 어댑터를 이어서 쓴다."
|
|
458
532
|
),
|
|
459
533
|
),
|
|
460
534
|
VerifiedAdapterSpec(
|
|
@@ -478,9 +552,13 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
478
552
|
policy_url=_data_go_policy("15155046"),
|
|
479
553
|
policy_text="공공데이터포털 인증키 기반 행정안전부 안전비상벨 위치정보 조회 OpenAPI.",
|
|
480
554
|
last_verified=_LAST_VERIFIED,
|
|
481
|
-
search_hint=
|
|
555
|
+
search_hint=(
|
|
556
|
+
"15155046 행안부 안전비상벨 비상벨 긴급신고함 위치 경찰연계 "
|
|
557
|
+
"방범 emergency call box safety bell find"
|
|
558
|
+
),
|
|
482
559
|
llm_description=(
|
|
483
|
-
"도로명주소 조각으로
|
|
560
|
+
"도로명주소 조각으로 안전비상벨·비상벨·긴급신고함 설치 위치와 "
|
|
561
|
+
"관리기관 공개 데이터를 조회한다."
|
|
484
562
|
),
|
|
485
563
|
),
|
|
486
564
|
VerifiedAdapterSpec(
|
|
@@ -525,10 +603,12 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
525
603
|
policy_text="공공데이터포털 인증키 기반 계룡시 장애인 전동보장구 충전 장소 조회 OpenAPI.",
|
|
526
604
|
last_verified=_LAST_VERIFIED,
|
|
527
605
|
search_hint=(
|
|
528
|
-
"15096040 계룡시 전동휠체어 전동보장구 충전 장소
|
|
606
|
+
"15096040 계룡시 전동휠체어 전동보장구 보장구 충전소 충전 장소 "
|
|
607
|
+
"장애인 실내 accessibility charger find"
|
|
529
608
|
),
|
|
530
609
|
llm_description=(
|
|
531
|
-
"계룡시 장애인
|
|
610
|
+
"계룡시 장애인 전동보장구·전동휠체어 충전소/충전 장소, "
|
|
611
|
+
"위치, 이용 가능 시간 공개 데이터를 조회한다."
|
|
532
612
|
),
|
|
533
613
|
),
|
|
534
614
|
VerifiedAdapterSpec(
|
|
@@ -554,9 +634,19 @@ VERIFIED_DATA_GO_KR_ADAPTERS: tuple[VerifiedAdapterSpec, ...] = (
|
|
|
554
634
|
"공공데이터포털 인증키 기반 국립중앙의료원 전국 자동심장충격기 정보 조회 OpenAPI."
|
|
555
635
|
),
|
|
556
636
|
last_verified=_LAST_VERIFIED,
|
|
557
|
-
search_hint=
|
|
637
|
+
search_hint=(
|
|
638
|
+
"15000652 국립중앙의료원 AED 자동심장충격기 자동제세동기 "
|
|
639
|
+
"응급실 주변 시도 시군구 q0 q1 find"
|
|
640
|
+
),
|
|
558
641
|
llm_description=(
|
|
559
|
-
"시도(q0)와 시군구(q1)로 전국 AED 설치 위치와 이용 가능 시간 공개 데이터를
|
|
642
|
+
"시도(q0)와 시군구(q1)로 전국 AED 설치 위치와 이용 가능 시간 공개 데이터를 "
|
|
643
|
+
"조회한다. 시민이 '응급실이나 AED', '응급실/AED', '자동심장충격기'를 같이 "
|
|
644
|
+
"묻는 경우 nmc_emergency_search 결과만으로 AED를 답하지 말고, 이 find "
|
|
645
|
+
"어댑터를 별도 호출한다. q0/q1은 좌표가 아니라 공식 지역 필터다. "
|
|
646
|
+
"origin_lat/origin_lon은 업스트림 파라미터가 아니라 응답 WGS84 좌표를 "
|
|
647
|
+
"거리순으로 정렬하기 위한 선택 필드다. 예: "
|
|
648
|
+
"부산역 근처는 locate 후 부산광역시/동구 또는 중구 지역 필터로 본 도구를 "
|
|
649
|
+
"추가 조회한다."
|
|
560
650
|
),
|
|
561
651
|
),
|
|
562
652
|
VerifiedAdapterSpec(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
7
7
|
|
|
8
8
|
from ummaya.tools.executor import ToolExecutor
|
|
9
9
|
from ummaya.tools.models import GovAPITool
|
|
@@ -21,11 +21,29 @@ class AirKoreaAirQualityInput(BaseModel):
|
|
|
21
21
|
|
|
22
22
|
model_config = ConfigDict(extra="forbid")
|
|
23
23
|
|
|
24
|
-
sido_name: str = Field(
|
|
24
|
+
sido_name: str = Field(
|
|
25
|
+
...,
|
|
26
|
+
min_length=1,
|
|
27
|
+
description="AirKorea short province/city name, e.g. 서울, 부산, 경기.",
|
|
28
|
+
)
|
|
25
29
|
page_no: int = Field(default=1, ge=1, description="Page number.")
|
|
26
|
-
num_of_rows: int = Field(
|
|
30
|
+
num_of_rows: int = Field(
|
|
31
|
+
default=100,
|
|
32
|
+
ge=1,
|
|
33
|
+
le=100,
|
|
34
|
+
description="Rows per page; 100 captures all city/province stations in normal use.",
|
|
35
|
+
)
|
|
27
36
|
ver: str = Field(default="1.0", description="AirKorea response version.")
|
|
28
37
|
|
|
38
|
+
@field_validator("sido_name", mode="before")
|
|
39
|
+
@classmethod
|
|
40
|
+
def normalize_sido_name(cls, value: object) -> object:
|
|
41
|
+
"""AirKorea returns rows for short 시도 names, not full 행정명 names."""
|
|
42
|
+
|
|
43
|
+
if not isinstance(value, str):
|
|
44
|
+
return value
|
|
45
|
+
return _normalize_airkorea_sido_name(value)
|
|
46
|
+
|
|
29
47
|
|
|
30
48
|
SPEC = require_spec("airkorea_ctprvn_air_quality")
|
|
31
49
|
INPUT_SCHEMA = AirKoreaAirQualityInput
|
|
@@ -39,7 +57,94 @@ async def handle(
|
|
|
39
57
|
) -> dict[str, object]:
|
|
40
58
|
"""Fetch or replay AirKorea air quality rows."""
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
output = await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
|
|
61
|
+
return _enrich_air_quality_grades(output)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_AIRKOREA_GRADE_LABELS: dict[str, str] = {
|
|
65
|
+
"1": "좋음",
|
|
66
|
+
"2": "보통",
|
|
67
|
+
"3": "나쁨",
|
|
68
|
+
"4": "매우나쁨",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_AIRKOREA_ITEM_NAMES: dict[str, str] = {
|
|
72
|
+
"khai": "통합대기환경지수(CAI)",
|
|
73
|
+
"pm10": "미세먼지(PM10)",
|
|
74
|
+
"pm25": "초미세먼지(PM2.5)",
|
|
75
|
+
"o3": "오존(O3)",
|
|
76
|
+
"no2": "이산화질소(NO2)",
|
|
77
|
+
"co": "일산화탄소(CO)",
|
|
78
|
+
"so2": "아황산가스(SO2)",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_AIRKOREA_SIDO_ALIASES: dict[str, str] = {
|
|
82
|
+
"서울특별시": "서울",
|
|
83
|
+
"부산광역시": "부산",
|
|
84
|
+
"대구광역시": "대구",
|
|
85
|
+
"인천광역시": "인천",
|
|
86
|
+
"광주광역시": "광주",
|
|
87
|
+
"대전광역시": "대전",
|
|
88
|
+
"울산광역시": "울산",
|
|
89
|
+
"세종특별자치시": "세종",
|
|
90
|
+
"경기도": "경기",
|
|
91
|
+
"강원특별자치도": "강원",
|
|
92
|
+
"강원도": "강원",
|
|
93
|
+
"충청북도": "충북",
|
|
94
|
+
"충청남도": "충남",
|
|
95
|
+
"전북특별자치도": "전북",
|
|
96
|
+
"전라북도": "전북",
|
|
97
|
+
"전라남도": "전남",
|
|
98
|
+
"경상북도": "경북",
|
|
99
|
+
"경상남도": "경남",
|
|
100
|
+
"제주특별자치도": "제주",
|
|
101
|
+
"제주도": "제주",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _normalize_airkorea_sido_name(value: str) -> str:
|
|
106
|
+
normalized = value.strip()
|
|
107
|
+
return _AIRKOREA_SIDO_ALIASES.get(normalized, normalized)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _enrich_air_quality_grades(output: dict[str, object]) -> dict[str, object]:
|
|
111
|
+
items = output.get("items")
|
|
112
|
+
if not isinstance(items, list):
|
|
113
|
+
return output
|
|
114
|
+
enriched_items: list[dict[str, object]] = []
|
|
115
|
+
changed = False
|
|
116
|
+
for item in items:
|
|
117
|
+
if not isinstance(item, dict):
|
|
118
|
+
continue
|
|
119
|
+
record_raw = item.get("record")
|
|
120
|
+
if not isinstance(record_raw, dict):
|
|
121
|
+
enriched_items.append(item)
|
|
122
|
+
continue
|
|
123
|
+
record = dict(record_raw)
|
|
124
|
+
for prefix, name_ko in _AIRKOREA_ITEM_NAMES.items():
|
|
125
|
+
name_key = f"{prefix}NameKo"
|
|
126
|
+
if name_key not in record:
|
|
127
|
+
record[name_key] = name_ko
|
|
128
|
+
changed = True
|
|
129
|
+
grade_value = record.get(f"{prefix}Grade")
|
|
130
|
+
label = _airkorea_grade_label(grade_value)
|
|
131
|
+
if label is not None:
|
|
132
|
+
record[f"{prefix}GradeLabelKo"] = label
|
|
133
|
+
changed = True
|
|
134
|
+
next_item = dict(item)
|
|
135
|
+
next_item["record"] = record
|
|
136
|
+
enriched_items.append(next_item)
|
|
137
|
+
if not changed:
|
|
138
|
+
return output
|
|
139
|
+
enriched_output = dict(output)
|
|
140
|
+
enriched_output["items"] = enriched_items
|
|
141
|
+
return enriched_output
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _airkorea_grade_label(value: object) -> str | None:
|
|
145
|
+
if value is None:
|
|
146
|
+
return None
|
|
147
|
+
return _AIRKOREA_GRADE_LABELS.get(str(value).strip())
|
|
43
148
|
|
|
44
149
|
|
|
45
150
|
def register(registry: ToolRegistry, executor: ToolExecutor) -> None:
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
7
9
|
|
|
8
10
|
from ummaya.tools.executor import ToolExecutor
|
|
9
11
|
from ummaya.tools.models import GovAPITool
|
|
@@ -25,6 +27,30 @@ class NmcAedSiteInput(BaseModel):
|
|
|
25
27
|
q1: str = Field(..., min_length=1, description="District/county name.")
|
|
26
28
|
page_no: int = Field(default=1, ge=1, description="Page number.")
|
|
27
29
|
num_of_rows: int = Field(default=10, ge=1, le=100, description="Rows per page.")
|
|
30
|
+
origin_lat: float | None = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
ge=-90,
|
|
33
|
+
le=90,
|
|
34
|
+
description=(
|
|
35
|
+
"Optional original query latitude for client-side distance sorting. "
|
|
36
|
+
"Not sent to the upstream NMC AED API."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
origin_lon: float | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
ge=-180,
|
|
42
|
+
le=180,
|
|
43
|
+
description=(
|
|
44
|
+
"Optional original query longitude for client-side distance sorting. "
|
|
45
|
+
"Not sent to the upstream NMC AED API."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def _origin_pair_is_complete(self) -> NmcAedSiteInput:
|
|
51
|
+
if (self.origin_lat is None) ^ (self.origin_lon is None):
|
|
52
|
+
raise ValueError("origin_lat and origin_lon must be supplied together")
|
|
53
|
+
return self
|
|
28
54
|
|
|
29
55
|
|
|
30
56
|
SPEC = require_spec("nmc_aed_site_locate")
|
|
@@ -39,7 +65,87 @@ async def handle(
|
|
|
39
65
|
) -> dict[str, object]:
|
|
40
66
|
"""Fetch or replay NMC AED rows."""
|
|
41
67
|
|
|
42
|
-
|
|
68
|
+
output = await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
|
|
69
|
+
if input_model.origin_lat is None or input_model.origin_lon is None:
|
|
70
|
+
return output
|
|
71
|
+
_sort_items_by_origin_distance(
|
|
72
|
+
output,
|
|
73
|
+
origin_lat=input_model.origin_lat,
|
|
74
|
+
origin_lon=input_model.origin_lon,
|
|
75
|
+
)
|
|
76
|
+
return output
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sort_items_by_origin_distance(
|
|
80
|
+
output: dict[str, object],
|
|
81
|
+
*,
|
|
82
|
+
origin_lat: float,
|
|
83
|
+
origin_lon: float,
|
|
84
|
+
) -> None:
|
|
85
|
+
items = output.get("items")
|
|
86
|
+
if not isinstance(items, list):
|
|
87
|
+
return
|
|
88
|
+
for item in items:
|
|
89
|
+
if not isinstance(item, dict):
|
|
90
|
+
continue
|
|
91
|
+
record = item.get("record")
|
|
92
|
+
if not isinstance(record, dict):
|
|
93
|
+
continue
|
|
94
|
+
lat = _as_float(record.get("wgs84Lat"))
|
|
95
|
+
lon = _as_float(record.get("wgs84Lon"))
|
|
96
|
+
if lat is None or lon is None:
|
|
97
|
+
continue
|
|
98
|
+
distance_km = round(
|
|
99
|
+
_haversine_km(
|
|
100
|
+
lat1=origin_lat,
|
|
101
|
+
lon1=origin_lon,
|
|
102
|
+
lat2=lat,
|
|
103
|
+
lon2=lon,
|
|
104
|
+
),
|
|
105
|
+
3,
|
|
106
|
+
)
|
|
107
|
+
record["distance"] = distance_km
|
|
108
|
+
record["distance_km"] = distance_km
|
|
109
|
+
record["distance_unit"] = "km"
|
|
110
|
+
items.sort(key=_distance_sort_key)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _distance_sort_key(item: object) -> tuple[int, float]:
|
|
114
|
+
if not isinstance(item, dict):
|
|
115
|
+
return (1, 0.0)
|
|
116
|
+
record = item.get("record")
|
|
117
|
+
if not isinstance(record, dict):
|
|
118
|
+
return (1, 0.0)
|
|
119
|
+
distance = _as_float(record.get("distance_km"))
|
|
120
|
+
if distance is None:
|
|
121
|
+
return (1, 0.0)
|
|
122
|
+
return (0, distance)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _as_float(value: object) -> float | None:
|
|
126
|
+
if isinstance(value, bool):
|
|
127
|
+
return None
|
|
128
|
+
if isinstance(value, int | float):
|
|
129
|
+
return float(value)
|
|
130
|
+
if not isinstance(value, str):
|
|
131
|
+
return None
|
|
132
|
+
try:
|
|
133
|
+
return float(value.strip())
|
|
134
|
+
except ValueError:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _haversine_km(*, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
|
139
|
+
radius_km = 6371.0088
|
|
140
|
+
phi1 = math.radians(lat1)
|
|
141
|
+
phi2 = math.radians(lat2)
|
|
142
|
+
delta_phi = math.radians(lat2 - lat1)
|
|
143
|
+
delta_lambda = math.radians(lon2 - lon1)
|
|
144
|
+
a = (
|
|
145
|
+
math.sin(delta_phi / 2.0) ** 2
|
|
146
|
+
+ math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2.0) ** 2
|
|
147
|
+
)
|
|
148
|
+
return radius_km * 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
|
|
43
149
|
|
|
44
150
|
|
|
45
151
|
def register(registry: ToolRegistry, executor: ToolExecutor) -> None:
|