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.
Files changed (111) hide show
  1. package/README.md +2 -1
  2. package/npm-shrinkwrap.json +2 -2
  3. package/package.json +1 -1
  4. package/prompts/manifest.yaml +2 -2
  5. package/prompts/session_guidance_v1.md +3 -1
  6. package/prompts/system_v1.md +8 -7
  7. package/pyproject.toml +2 -7
  8. package/src/ummaya/context/builder.py +17 -11
  9. package/src/ummaya/engine/engine.py +27 -7
  10. package/src/ummaya/engine/query.py +20 -0
  11. package/src/ummaya/evidence/__init__.py +25 -0
  12. package/src/ummaya/evidence/__main__.py +7 -0
  13. package/src/ummaya/evidence/models.py +58 -0
  14. package/src/ummaya/evidence/runner.py +308 -0
  15. package/src/ummaya/evidence/task_registry.py +264 -0
  16. package/src/ummaya/ipc/frame_schema.py +47 -0
  17. package/src/ummaya/ipc/stdio.py +1349 -90
  18. package/src/ummaya/llm/client.py +132 -56
  19. package/src/ummaya/llm/reasoning.py +84 -0
  20. package/src/ummaya/tools/discovery_bridge.py +17 -1
  21. package/src/ummaya/tools/executor.py +32 -12
  22. package/src/ummaya/tools/geocoding/kakao_client.py +1 -2
  23. package/src/ummaya/tools/kma/apihub_catalog.py +984 -1
  24. package/src/ummaya/tools/kma/apihub_structured_adapter.py +86 -6
  25. package/src/ummaya/tools/kma/apihub_url_adapter.py +593 -0
  26. package/src/ummaya/tools/kma/apihub_url_catalog.py +296 -0
  27. package/src/ummaya/tools/location_adapters.py +8 -6
  28. package/src/ummaya/tools/manifest_metadata.py +16 -3
  29. package/src/ummaya/tools/mvp_surface.py +2 -2
  30. package/src/ummaya/tools/nmc/emergency_search.py +8 -6
  31. package/src/ummaya/tools/register_all.py +9 -0
  32. package/src/ummaya/tools/resolve_location.py +4 -4
  33. package/src/ummaya/tools/search.py +664 -18
  34. package/src/ummaya/tools/verified_data_go_kr/_manifest.py +115 -25
  35. package/src/ummaya/tools/verified_data_go_kr/airkorea_air_quality.py +109 -4
  36. package/src/ummaya/tools/verified_data_go_kr/nmc_aed_site.py +108 -2
  37. package/src/ummaya/tools/verified_data_go_kr/pps_bid_public_info.py +174 -9
  38. package/src/ummaya/tools/verified_data_go_kr/tago_bus_arrival.py +66 -3
  39. package/src/ummaya/tools/verified_data_go_kr/tago_bus_location.py +12 -2
  40. package/src/ummaya/tools/verified_data_go_kr/tago_bus_route.py +8 -2
  41. package/src/ummaya/tools/verified_data_go_kr/tago_bus_route_station.py +114 -0
  42. package/src/ummaya/tools/verified_data_go_kr/tago_bus_station.py +14 -3
  43. package/src/ummaya/tools/verify_canonical_map.py +21 -0
  44. package/tui/package.json +1 -2
  45. package/tui/src/QueryEngine.ts +4 -0
  46. package/tui/src/cli/handlers/auth.ts +1 -1
  47. package/tui/src/cli/handlers/mcp.tsx +3 -3
  48. package/tui/src/cli/print.ts +69 -18
  49. package/tui/src/cli/update.ts +13 -13
  50. package/tui/src/commands/copy/index.ts +1 -1
  51. package/tui/src/commands/cost/cost.ts +2 -2
  52. package/tui/src/commands/init-verifiers.ts +5 -5
  53. package/tui/src/commands/init.ts +30 -30
  54. package/tui/src/commands/insights.ts +43 -43
  55. package/tui/src/commands/install-github-app/install-github-app.tsx +2 -2
  56. package/tui/src/commands/install-github-app/setupGitHubActions.ts +3 -3
  57. package/tui/src/commands/install.tsx +5 -5
  58. package/tui/src/commands/mcp/addCommand.ts +5 -5
  59. package/tui/src/commands/mcp/xaaIdpCommand.ts +2 -2
  60. package/tui/src/commands/plugin/ManageMarketplaces.tsx +2 -2
  61. package/tui/src/commands/reasoning/index.ts +13 -0
  62. package/tui/src/commands/reasoning/reasoning.tsx +177 -0
  63. package/tui/src/commands/thinkback/thinkback.tsx +3 -3
  64. package/tui/src/commands.ts +2 -0
  65. package/tui/src/components/Messages.tsx +2 -1
  66. package/tui/src/components/Spinner.tsx +2 -2
  67. package/tui/src/components/design-system/LoadingState.tsx +2 -2
  68. package/tui/src/ipc/codec.ts +26 -0
  69. package/tui/src/ipc/frames.generated.ts +398 -303
  70. package/tui/src/ipc/llmClient.ts +130 -51
  71. package/tui/src/ipc/llmTypes.ts +16 -1
  72. package/tui/src/ipc/schema/frame.schema.json +1 -3475
  73. package/tui/src/main.tsx +3 -0
  74. package/tui/src/query.ts +467 -2
  75. package/tui/src/screens/REPL.tsx +3 -3
  76. package/tui/src/services/api/claude.ts +54 -25
  77. package/tui/src/services/api/client.ts +33 -12
  78. package/tui/src/services/api/ummaya.ts +70 -16
  79. package/tui/src/skills/bundled/stuck.ts +12 -12
  80. package/tui/src/state/AppStateStore.ts +7 -0
  81. package/tui/src/tools/AdapterTool/AdapterTool.ts +590 -7
  82. package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +43 -17
  83. package/tui/src/tools/LookupPrimitive/prompt.ts +7 -6
  84. package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +40 -19
  85. package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +25 -9
  86. package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +25 -9
  87. package/tui/src/tools/_shared/citizenUserText.ts +49 -0
  88. package/tui/src/tools/_shared/directPublicDataGuard.ts +362 -0
  89. package/tui/src/tools/_shared/kmaAnalysisGuard.ts +197 -0
  90. package/tui/src/tools/_shared/kmaAviationGuard.ts +70 -0
  91. package/tui/src/tools/_shared/locationInputRepair.ts +112 -0
  92. package/tui/src/tools/_shared/nmcAedGuard.ts +234 -0
  93. package/tui/src/tools/_shared/protectedCheckGuard.ts +207 -0
  94. package/tui/src/tools/_shared/rootPrimitiveInput.ts +67 -0
  95. package/tui/src/tools/_shared/textToolCallGuard.ts +91 -0
  96. package/tui/src/tools/_shared/toolChoiceRepair.ts +866 -0
  97. package/tui/src/utils/attachments.ts +1 -1
  98. package/tui/src/utils/kExaoneReasoning.ts +138 -0
  99. package/tui/src/utils/messages.ts +1 -0
  100. package/tui/src/utils/multiToolLayout.ts +13 -0
  101. package/tui/src/utils/processUserInput/processSlashCommand.tsx +2 -2
  102. package/tui/src/utils/processUserInput/processUserInput.ts +26 -0
  103. package/tui/src/utils/settings/applySettingsChange.ts +4 -0
  104. package/tui/src/utils/settings/types.ts +9 -3
  105. package/tui/src/utils/stats.ts +1 -1
  106. package/uv.lock +1 -15
  107. package/assets/copilot-gate-logo.svg +0 -58
  108. package/assets/govon-logo.svg +0 -40
  109. package/src/ummaya/eval/__init__.py +0 -5
  110. package/src/ummaya/eval/retrieval.py +0 -713
  111. package/tui/src/utils/messageStream.ts +0 -186
@@ -14,6 +14,13 @@ import {
14
14
  import { getOrCreateUmmayaBridge } from '../../ipc/bridgeSingleton.js'
15
15
  import { getOrCreatePendingCallRegistry } from '../../ipc/pendingCallSingleton.js'
16
16
  import { dispatchPrimitive } from '../_shared/dispatchPrimitive.js'
17
+ import { validateKmaAviationToolChoice } from '../_shared/kmaAviationGuard.js'
18
+ import { validateKmaAnalysisToolChoice } from '../_shared/kmaAnalysisGuard.js'
19
+ import { validateNmcAedToolChoice } from '../_shared/nmcAedGuard.js'
20
+ import {
21
+ normalizeDirectPublicDataToolInput,
22
+ validateDirectPublicDataToolChoice,
23
+ } from '../_shared/directPublicDataGuard.js'
17
24
  import { LookupPrimitive } from '../LookupPrimitive/LookupPrimitive.js'
18
25
  import { ResolveLocationPrimitive } from '../ResolveLocationPrimitive/ResolveLocationPrimitive.js'
19
26
  import { SubmitPrimitive } from '../SubmitPrimitive/SubmitPrimitive.js'
@@ -24,6 +31,72 @@ type AdapterPrimitive = 'find' | 'locate' | 'send' | 'check'
24
31
  type InputSchema = z.ZodType<{ [key: string]: unknown }>
25
32
 
26
33
  const ROOT_PRIMITIVE_TOOL_NAMES = new Set(['locate', 'find', 'check', 'send'])
34
+ const KMA_URL_AIR_TOOL_NAMES = new Set([
35
+ 'kma_apihub_url_air_amos_minute',
36
+ 'kma_apihub_url_air_metar_decoded',
37
+ ])
38
+ const KMA_ANALYSIS_TOOL_NAMES = new Set([
39
+ 'kma_apihub_url_high_resolution_grid_point',
40
+ 'kma_apihub_url_aws_objective_analysis_grid',
41
+ 'kma_apihub_url_analysis_weather_chart_image',
42
+ ])
43
+ const TAGO_BUS_TOOL_NAMES = new Set([
44
+ 'tago_bus_station_search',
45
+ 'tago_bus_arrival_search',
46
+ 'tago_bus_route_search',
47
+ 'tago_bus_route_station_search',
48
+ 'tago_bus_location_search',
49
+ ])
50
+ const KMA_GIMHAE_AIRPORT_RE = /(김해공항|gimhae|rkpk)/iu
51
+ const KMA_GIMPO_AIRPORT_RE = /(김포공항|gimpo|rkss)/iu
52
+ const KMA_AIRPORT_NAME_RE = /(공항|\bairport\b|\brk[a-z]{2}\b|station\s*\d{2,3})/iu
53
+ const KMA_AIRPORT_AVIATION_RE =
54
+ /(amos|metar|speci|rvr|항공기상|공항기상|활주로|runway|aviation|비행기|항공편|비행편|이륙|착륙|결항|지연|운항|뜰\s*만|뜨나|뜰\s*수|flight|take\s*off|landing|delay|cancel)/iu
55
+ const KMA_RUNWAY_AREA_RE =
56
+ /(amos|활주로|rvr|runway|시정|visibility|공항기상관측|매분)/iu
57
+ const KMA_ANALYSIS_DATA_RE =
58
+ /(분석자료|이미\s*분석|고해상도\s*격자|객관분석|aws\s*객관|지도\s*자료|일기도|분석일기도|비구름|바람\s*흐름|synoptic|weather\s*chart|objective\s*analysis|high[-\s]?resolution|grid)/iu
59
+ const KMA_ANALYSIS_MAP_RE =
60
+ /(일기도|분석일기도|지도\s*자료|비구름|바람\s*흐름|synoptic|weather\s*chart)/iu
61
+ const KMA_ANALYSIS_POINT_RE =
62
+ /(주변|근처|특정지점|좌표|위도|경도|\blat\b|\blon\b|공항\s*주변)/iu
63
+ const KMA_LIFESTYLE_WEATHER_RE =
64
+ /(날씨|현재\s*기상|실황|관측|예보|기온|습도|풍속|지금\s*비|비\s*(와|오|올|내리)|우산|강수|소나기|산책|퇴근|current\s+weather|forecast|rain|umbrella|precipitation|temperature)/iu
65
+ const KMA_LIFESTYLE_WEATHER_TOOL_NAMES = new Set([
66
+ 'kma_current_observation',
67
+ 'kma_ultra_short_term_forecast',
68
+ 'kma_short_term_forecast',
69
+ ])
70
+ const HIRA_MEDICAL_DETAIL_RE =
71
+ /((병원|의료기관|의원).*(상세|진료과|진료과목|진료시간|주차)|(상세|진료시간|주차|응급실).*(병원|의료기관|의원)|ykiho|detail)/iu
72
+ const MOIS_EMERGENCY_CALL_BOX_RE =
73
+ /(안전\s*비상벨|비상벨|긴급\s*신고함|긴급신고함|방범벨|emergency\s+call\s+box)/iu
74
+ const GYERYONG_ASSISTIVE_CHARGER_RE =
75
+ /((전동보장구|전동\s*휠체어|보장구|장애인).*(충전|충전소|충전장소)|(충전|충전소|충전장소).*(전동보장구|전동\s*휠체어|보장구|장애인)|계룡시?.*(충전소|충전\s*장소))/iu
76
+ const MOF_OCEAN_WATER_QUALITY_RE =
77
+ /(해양\s*수질|해양수질|수질\s*자동\s*측정|용존산소|\bpH\b|water\s+quality|ocean\s+water)/iu
78
+ const PPS_SHOPPING_RE = /(종합\s*쇼핑몰|쇼핑몰|계약\s*물품|물품\s*조회|shopping\s*mall)/iu
79
+ const PPS_BID_RE = /(입찰|나라장터|조달청|\bbid\b|procurement|tender)/iu
80
+ const PROTECTED_QUERY_RE =
81
+ /(본인확인|인증|간편인증|모바일\s*(?:신분증|id)|mobile\s*id|마이데이터|mydata|증명원|소득금액증명|소득금액증명원|주민등록등본|민원|발급)/iu
82
+ const PROTECTED_MOBILE_ID_RE = /(mobile\s*id|모바일\s*(?:신분증|id)|mobile_id)/iu
83
+ const PROTECTED_SIMPLE_AUTH_RE =
84
+ /(simple_auth|간편인증|ganpyeon|소득금액증명|증명원|민원|발급)/iu
85
+ const PROTECTED_MYDATA_RE = /(mydata|마이데이터)/iu
86
+ const PROTECTED_CHECK_TOOL_NAMES = [
87
+ 'mock_verify_module_simple_auth',
88
+ 'mock_verify_ganpyeon_injeung',
89
+ 'mock_verify_mobile_id',
90
+ 'mock_verify_mydata',
91
+ ] as const
92
+ const TAGO_BUS_RE =
93
+ /(버스|시내버스|정류장|정류소|노선|도착|언제\s*와|몇\s*분|bus|route|arrival|station)/iu
94
+ const AED_REQUEST_RE = /(aed|자동심장|심장충격|제세동)/iu
95
+ const EMERGENCY_REQUEST_RE = /(응급|응급실|\ber\b|emergency)/iu
96
+ const MEDICAL_COLLAPSE_RE =
97
+ /(사람이\s*쓰러|쓰러졌|쓰러져|의식\s*잃|의식을\s*잃|심정지|호흡이\s*없|숨을\s*안|collapsed|unconscious|cardiac\s*arrest)/iu
98
+ const TRAFFIC_HAZARD_RE =
99
+ /(교통사고|사고\s*위험|사고다발|위험\s*(구간|도로|지점)|어린이보호구역|보호구역|도로\s*구간|accident|hazard|hotspot)/iu
27
100
 
28
101
  const fallbackInputSchema = z.object({}).passthrough() as InputSchema
29
102
 
@@ -212,6 +285,23 @@ function rootInputFor(entry: AdapterManifestEntry, input: Record<string, unknown
212
285
  }
213
286
  }
214
287
 
288
+ function validateAdapterContractInput(
289
+ toolId: string,
290
+ input: Record<string, unknown>,
291
+ ) {
292
+ if (toolId !== 'kma_apihub_url_analysis_weather_chart_image') return undefined
293
+ const analTime = input.anal_time
294
+ if (typeof analTime === 'string' && /^\d{12}$/u.test(analTime)) return undefined
295
+ return {
296
+ result: false as const,
297
+ message:
298
+ 'KMA analysis weather-chart schema mismatch: anal_time is required as UTC YYYYMMDDHHMM. ' +
299
+ "Use a 12-digit official analysis time with minutes, for example '202605281200', not a 10-digit KST hour. " +
300
+ 'If the citizen asks for now/today, choose the latest completed official UTC analysis slot and report upstream failure directly if APIHub has no chart.',
301
+ errorCode: 1,
302
+ }
303
+ }
304
+
215
305
  export function isAdapterToolName(name: string): boolean {
216
306
  return resolveAdapter(name) !== undefined
217
307
  }
@@ -236,6 +326,7 @@ function searchTokens(text: string): string[] {
236
326
  function expandedQueryTokens(query: string): Set<string> {
237
327
  const tokens = new Set(searchTokens(query))
238
328
  const compact = query.toLowerCase()
329
+ const airportAviationQuery = isAirportAviationQuery(query)
239
330
  if (/[날씨기상비강수기온습도풍속예보관측실황]/u.test(compact)) {
240
331
  for (const token of [
241
332
  '날씨',
@@ -251,11 +342,22 @@ function expandedQueryTokens(query: string): Set<string> {
251
342
  'precipitation',
252
343
  'humidity',
253
344
  'wind',
345
+ 'kma',
346
+ '초단기실황',
347
+ '초단기예보',
348
+ '단기예보',
349
+ '우산',
350
+ 'nx',
351
+ 'ny',
352
+ 'base_date',
353
+ 'base_time',
254
354
  ]) {
255
355
  tokens.add(token)
256
356
  }
257
357
  }
258
- if (/(응급|응급실|er|emergency)/u.test(compact)) {
358
+ const medicalEmergencyQuery = isMedicalEmergencyQuery(query)
359
+ const collapseOrAedQuery = isCollapseOrAedQuery(query)
360
+ if (EMERGENCY_REQUEST_RE.test(compact) && medicalEmergencyQuery) {
259
361
  for (const token of [
260
362
  '응급',
261
363
  '응급실',
@@ -272,6 +374,30 @@ function expandedQueryTokens(query: string): Set<string> {
272
374
  tokens.add(token)
273
375
  }
274
376
  }
377
+ if (collapseOrAedQuery) {
378
+ for (const token of [
379
+ '응급',
380
+ '응급실',
381
+ '응급의료',
382
+ '응급의료센터',
383
+ 'aed',
384
+ '자동심장충격기',
385
+ '자동제세동기',
386
+ '심장충격기',
387
+ '응급처치',
388
+ '심정지',
389
+ '의식불명',
390
+ 'emergency',
391
+ 'room',
392
+ 'er',
393
+ 'defibrillator',
394
+ 'cardiac',
395
+ 'arrest',
396
+ 'nmc',
397
+ ]) {
398
+ tokens.add(token)
399
+ }
400
+ }
275
401
  if (/(병원|의료|진료|약국|hospital|clinic|medical)/u.test(compact)) {
276
402
  for (const token of [
277
403
  '병원',
@@ -288,7 +414,134 @@ function expandedQueryTokens(query: string): Set<string> {
288
414
  tokens.add(token)
289
415
  }
290
416
  }
291
- if (/(aed|자동심장|심장충격|제세동)/u.test(compact)) {
417
+ if (HIRA_MEDICAL_DETAIL_RE.test(query)) {
418
+ for (const token of [
419
+ '상세정보',
420
+ '진료과',
421
+ '진료과목',
422
+ '진료시간',
423
+ '주차',
424
+ '요양기호',
425
+ 'ykiho',
426
+ 'hira',
427
+ 'detail',
428
+ 'specialty',
429
+ ]) {
430
+ tokens.add(token)
431
+ }
432
+ }
433
+ if (MOIS_EMERGENCY_CALL_BOX_RE.test(query)) {
434
+ for (const token of [
435
+ '안전비상벨',
436
+ '비상벨',
437
+ '긴급신고함',
438
+ '방범',
439
+ '행정안전부',
440
+ 'mois',
441
+ 'emergency',
442
+ 'call',
443
+ 'box',
444
+ ]) {
445
+ tokens.add(token)
446
+ }
447
+ }
448
+ if (GYERYONG_ASSISTIVE_CHARGER_RE.test(query)) {
449
+ for (const token of [
450
+ '계룡시',
451
+ '전동보장구',
452
+ '전동휠체어',
453
+ '보장구',
454
+ '장애인',
455
+ '충전소',
456
+ '충전장소',
457
+ 'accessibility',
458
+ 'charger',
459
+ ]) {
460
+ tokens.add(token)
461
+ }
462
+ }
463
+ if (MOF_OCEAN_WATER_QUALITY_RE.test(query)) {
464
+ for (const token of [
465
+ '해양수산부',
466
+ '해양수질',
467
+ '수질자동측정망',
468
+ '관측소',
469
+ 'sea3003',
470
+ '용존산소',
471
+ 'water',
472
+ 'quality',
473
+ 'ocean',
474
+ ]) {
475
+ tokens.add(token)
476
+ }
477
+ }
478
+ if (isPpsBidQuery(query)) {
479
+ for (const token of [
480
+ '조달청',
481
+ '나라장터',
482
+ '입찰공고',
483
+ '공사입찰',
484
+ 'bidntcenm',
485
+ 'inqrybgndt',
486
+ 'inqryenddt',
487
+ 'pps',
488
+ 'bid',
489
+ 'procurement',
490
+ ]) {
491
+ tokens.add(token)
492
+ }
493
+ }
494
+ if (isProtectedCheckQuery(query)) {
495
+ for (const token of [
496
+ '본인확인',
497
+ '인증',
498
+ '간편인증',
499
+ '모바일신분증',
500
+ '모바일id',
501
+ '소득금액증명원',
502
+ '증명원',
503
+ '홈택스',
504
+ '정부24',
505
+ 'check',
506
+ 'verify',
507
+ 'identity',
508
+ 'simple',
509
+ 'auth',
510
+ 'mobile',
511
+ 'id',
512
+ 'mydata',
513
+ ]) {
514
+ tokens.add(token)
515
+ }
516
+ }
517
+ if (isTagoBusQuery(query)) {
518
+ for (const token of [
519
+ '국토교통부',
520
+ 'tago',
521
+ '버스',
522
+ '시내버스',
523
+ '버스정류소',
524
+ '정류장',
525
+ '정류소',
526
+ '노선',
527
+ '노선번호',
528
+ '버스도착',
529
+ '도착',
530
+ 'nodeid',
531
+ 'nodenm',
532
+ 'nodeno',
533
+ 'routeid',
534
+ 'routeno',
535
+ 'citycode',
536
+ 'bus',
537
+ 'station',
538
+ 'route',
539
+ 'arrival',
540
+ ]) {
541
+ tokens.add(token)
542
+ }
543
+ }
544
+ if (collapseOrAedQuery) {
292
545
  for (const token of ['aed', '자동심장충격기', '자동제세동기', '심장충격기', '위치']) {
293
546
  tokens.add(token)
294
547
  }
@@ -328,6 +581,59 @@ function expandedQueryTokens(query: string): Set<string> {
328
581
  tokens.add(token)
329
582
  }
330
583
  }
584
+ if (airportAviationQuery) {
585
+ for (const token of [
586
+ 'metar',
587
+ 'speci',
588
+ 'amos',
589
+ '항공기상',
590
+ '공항기상',
591
+ '항공',
592
+ '비행기',
593
+ '항공편',
594
+ '운항',
595
+ '이륙',
596
+ '시정',
597
+ 'rvr',
598
+ 'wind',
599
+ 'visibility',
600
+ ]) {
601
+ tokens.add(token)
602
+ }
603
+ if (KMA_GIMPO_AIRPORT_RE.test(query) && KMA_RUNWAY_AREA_RE.test(query)) {
604
+ for (const token of [
605
+ 'amos',
606
+ '공항기상관측',
607
+ '매분자료',
608
+ '활주로',
609
+ '김포공항',
610
+ 'stn110',
611
+ 'runway',
612
+ 'visibility',
613
+ ]) {
614
+ tokens.add(token)
615
+ }
616
+ }
617
+ }
618
+ if (KMA_ANALYSIS_DATA_RE.test(query)) {
619
+ for (const token of [
620
+ '분석자료',
621
+ '고해상도',
622
+ '격자자료',
623
+ '객관분석',
624
+ 'aws',
625
+ '분석일기도',
626
+ '지도',
627
+ '비구름',
628
+ '바람흐름',
629
+ 'objective',
630
+ 'analysis',
631
+ 'grid',
632
+ 'chart',
633
+ ]) {
634
+ tokens.add(token)
635
+ }
636
+ }
331
637
  if (/(교통사고|사고\s*위험|사고다발|위험\s*(구간|도로|지점)|어린이보호구역|보호구역|도로\s*구간|accident|hazard|hotspot)/u.test(compact)) {
332
638
  for (const token of [
333
639
  '교통사고',
@@ -361,7 +667,10 @@ function expandedQueryTokens(query: string): Set<string> {
361
667
  tokens.add(token)
362
668
  }
363
669
  }
364
- if (/(근처|주변|인근|가까운|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크|nearby|around)/u.test(compact)) {
670
+ if (
671
+ !airportAviationQuery &&
672
+ /(근처|주변|인근|가까운|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크|nearby|around)/u.test(compact)
673
+ ) {
365
674
  for (const token of [
366
675
  '장소',
367
676
  '키워드',
@@ -395,6 +704,80 @@ function queryTargetsKoroadHazardDataset(query: string): boolean {
395
704
  return /(사고\s*위험|위험\s*(구간|도로|지점)|도로\s*구간|어린이보호구역|보호구역|스쿨존|행정동코드|adm_cd|hazard|hotspot)/iu.test(query)
396
705
  }
397
706
 
707
+ function isAirportAviationQuery(query: string): boolean {
708
+ return KMA_AIRPORT_NAME_RE.test(query) && KMA_AIRPORT_AVIATION_RE.test(query)
709
+ }
710
+
711
+ function isMedicalEmergencyQuery(query: string): boolean {
712
+ return (
713
+ (EMERGENCY_REQUEST_RE.test(query) ||
714
+ AED_REQUEST_RE.test(query) ||
715
+ MEDICAL_COLLAPSE_RE.test(query)) &&
716
+ !MOIS_EMERGENCY_CALL_BOX_RE.test(query)
717
+ )
718
+ }
719
+
720
+ function isCollapseOrAedQuery(query: string): boolean {
721
+ return (
722
+ (AED_REQUEST_RE.test(query) || MEDICAL_COLLAPSE_RE.test(query)) &&
723
+ !MOIS_EMERGENCY_CALL_BOX_RE.test(query)
724
+ )
725
+ }
726
+
727
+ function isLocationAdapter(entry: AdapterManifestEntry): boolean {
728
+ return entry.primitive === 'locate' || ROOT_PRIMITIVE_TOOL_NAMES.has(entry.tool_id)
729
+ }
730
+
731
+ function isKmaAnalysisQuery(query: string): boolean {
732
+ return KMA_ANALYSIS_DATA_RE.test(query)
733
+ }
734
+
735
+ function isLifestyleWeatherQuery(query: string): boolean {
736
+ return (
737
+ KMA_LIFESTYLE_WEATHER_RE.test(query) &&
738
+ !isAirportAviationQuery(query) &&
739
+ !isKmaAnalysisQuery(query) &&
740
+ !isMedicalEmergencyQuery(query) &&
741
+ !TRAFFIC_HAZARD_RE.test(query) &&
742
+ !MOF_OCEAN_WATER_QUALITY_RE.test(query)
743
+ )
744
+ }
745
+
746
+ function isPpsBidQuery(query: string): boolean {
747
+ return PPS_BID_RE.test(query) && !PPS_SHOPPING_RE.test(query)
748
+ }
749
+
750
+ function isProtectedCheckQuery(query: string): boolean {
751
+ return PROTECTED_QUERY_RE.test(query)
752
+ }
753
+
754
+ function protectedCheckToolPreference(query: string): string[] {
755
+ const preferred = [
756
+ PROTECTED_MOBILE_ID_RE.test(query) ? 'mock_verify_mobile_id' : undefined,
757
+ PROTECTED_SIMPLE_AUTH_RE.test(query) ? 'mock_verify_module_simple_auth' : undefined,
758
+ PROTECTED_SIMPLE_AUTH_RE.test(query) ? 'mock_verify_ganpyeon_injeung' : undefined,
759
+ PROTECTED_MYDATA_RE.test(query) ? 'mock_verify_mydata' : undefined,
760
+ ...PROTECTED_CHECK_TOOL_NAMES,
761
+ ].filter((toolName): toolName is string => typeof toolName === 'string')
762
+ return [...new Set(preferred)]
763
+ }
764
+
765
+ function isTagoBusQuery(query: string): boolean {
766
+ return TAGO_BUS_RE.test(query)
767
+ }
768
+
769
+ function isKmaAnalysisMapQuery(query: string): boolean {
770
+ return KMA_ANALYSIS_MAP_RE.test(query)
771
+ }
772
+
773
+ function isKmaAnalysisPointQuery(query: string): boolean {
774
+ return KMA_ANALYSIS_POINT_RE.test(query) && !isKmaAnalysisMapQuery(query)
775
+ }
776
+
777
+ function queryPrefersPoiLocation(query: string): boolean {
778
+ return /(근처|주변|인근|가까운|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크|nearby|around)/iu.test(query)
779
+ }
780
+
398
781
  function scoreAdapterEntry(
399
782
  entry: AdapterManifestEntry,
400
783
  queryTokens: Set<string>,
@@ -419,6 +802,7 @@ function scoreAdapterEntry(
419
802
  if (description.includes(token)) score += 2
420
803
  if (haystack.includes(token)) score += 1
421
804
  }
805
+ if (query.toLowerCase().includes(entry.tool_id.toLowerCase())) score += 1000
422
806
  if (
423
807
  isReverseGeocodeAdapter(entry.tool_id) &&
424
808
  !queryExplicitlyMentionsCoordinates(query)
@@ -429,9 +813,183 @@ function scoreAdapterEntry(
429
813
  if (entry.tool_id === 'koroad_accident_hazard_search') score += 32
430
814
  if (entry.tool_id === 'koroad_accident_search') score = 0
431
815
  }
816
+ if (isKmaAnalysisQuery(query)) {
817
+ if (entry.tool_id === 'kma_apihub_url_analysis_weather_chart_image') {
818
+ score += isKmaAnalysisMapQuery(query) ? 900 : isKmaAnalysisPointQuery(query) ? -20 : 150
819
+ }
820
+ if (entry.tool_id === 'kma_apihub_url_high_resolution_grid_point') {
821
+ score += isKmaAnalysisPointQuery(query) ? 900 : 450
822
+ }
823
+ if (entry.tool_id === 'kma_apihub_url_aws_objective_analysis_grid') {
824
+ score += isKmaAnalysisPointQuery(query) ? 800 : 400
825
+ }
826
+ if (isKmaAnalysisPointQuery(query) && queryPrefersPoiLocation(query)) {
827
+ if (entry.tool_id === 'kakao_keyword_search') score += 30
828
+ if (entry.tool_id === 'kakao_address_search') score = Math.max(1, score - 15)
829
+ }
830
+ }
831
+ if (isLifestyleWeatherQuery(query)) {
832
+ if (entry.tool_id === 'kakao_keyword_search') score += 1100
833
+ if (entry.tool_id === 'kakao_address_search') score += 1000
834
+ if (entry.tool_id === 'kma_current_observation') score += 900
835
+ if (entry.tool_id === 'kma_ultra_short_term_forecast') score += 800
836
+ if (entry.tool_id === 'kma_short_term_forecast') score += 650
837
+ if (entry.tool_id === 'kakao_coord_to_region') score += 260
838
+ if (entry.tool_id === 'juso_adm_cd_lookup') score += 260
839
+ if (entry.tool_id === 'sgis_adm_cd_lookup') score += 260
840
+ }
841
+ if (HIRA_MEDICAL_DETAIL_RE.test(query)) {
842
+ if (entry.tool_id === 'hira_medical_institution_detail') score += 650
843
+ }
844
+ if (MOIS_EMERGENCY_CALL_BOX_RE.test(query)) {
845
+ if (entry.tool_id === 'mois_emergency_call_box_lookup') score += 1000
846
+ }
847
+ if (GYERYONG_ASSISTIVE_CHARGER_RE.test(query)) {
848
+ if (entry.tool_id === 'gyeryong_assistive_device_charging_place_locate') {
849
+ score += 1000
850
+ }
851
+ }
852
+ if (MOF_OCEAN_WATER_QUALITY_RE.test(query)) {
853
+ if (entry.tool_id === 'mof_ocean_water_quality_check') score += 1000
854
+ }
855
+ if (isPpsBidQuery(query)) {
856
+ if (entry.tool_id === 'pps_bid_public_info') score += 1000
857
+ }
858
+ if (isProtectedCheckQuery(query) && entry.primitive === 'check') {
859
+ const preference = protectedCheckToolPreference(query)
860
+ const index = preference.indexOf(entry.tool_id)
861
+ score += index >= 0 ? 1000 - index * 20 : 500
862
+ }
863
+ if (isTagoBusQuery(query)) {
864
+ if (entry.tool_id === 'tago_bus_station_search') score += 1050
865
+ if (entry.tool_id === 'tago_bus_arrival_search') score += 1000
866
+ if (entry.tool_id === 'tago_bus_route_station_search') score += 950
867
+ if (entry.tool_id === 'tago_bus_route_search') score += 850
868
+ if (entry.tool_id === 'tago_bus_location_search') score += 650
869
+ }
870
+ if (isCollapseOrAedQuery(query)) {
871
+ if (entry.tool_id === 'nmc_aed_site_locate') score += 900
872
+ if (entry.tool_id === 'nmc_emergency_search') score += 700
873
+ if (queryPrefersPoiLocation(query) && entry.tool_id === 'kakao_keyword_search') score += 120
874
+ }
875
+ if (
876
+ KMA_GIMPO_AIRPORT_RE.test(query) &&
877
+ KMA_RUNWAY_AREA_RE.test(query) &&
878
+ KMA_AIRPORT_AVIATION_RE.test(query) &&
879
+ entry.tool_id === 'kma_apihub_url_air_amos_minute'
880
+ ) {
881
+ score += 500
882
+ }
432
883
  return score
433
884
  }
434
885
 
886
+ function filterSpecialCaseRanked(
887
+ query: string,
888
+ ranked: ScoredAdapterEntry[],
889
+ ): ScoredAdapterEntry[] {
890
+ let filtered = ranked
891
+ if (isKmaAnalysisQuery(query)) {
892
+ const allowLocation = isKmaAnalysisPointQuery(query)
893
+ const preferPoiLocation = queryPrefersPoiLocation(query)
894
+ filtered = filtered
895
+ .filter(candidate => {
896
+ if (KMA_ANALYSIS_TOOL_NAMES.has(candidate.entry.tool_id)) return true
897
+ return allowLocation && isLocationAdapter(candidate.entry)
898
+ })
899
+ .map(candidate => {
900
+ if (!allowLocation || !isLocationAdapter(candidate.entry)) return candidate
901
+ let score = Math.max(1, candidate.score - 10)
902
+ if (preferPoiLocation && candidate.entry.tool_id === 'kakao_keyword_search') {
903
+ score += 30
904
+ } else if (preferPoiLocation && candidate.entry.tool_id === 'kakao_address_search') {
905
+ score = Math.max(1, score - 15)
906
+ }
907
+ return { ...candidate, score }
908
+ })
909
+ .sort((a, b) => {
910
+ if (b.score !== a.score) return b.score - a.score
911
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
912
+ })
913
+ }
914
+ if (isLifestyleWeatherQuery(query)) {
915
+ const allowed = filtered.filter(
916
+ candidate =>
917
+ KMA_LIFESTYLE_WEATHER_TOOL_NAMES.has(candidate.entry.tool_id) ||
918
+ isLocationAdapter(candidate.entry),
919
+ )
920
+ if (allowed.length > 0) {
921
+ filtered = allowed.sort((a, b) => {
922
+ if (b.score !== a.score) return b.score - a.score
923
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
924
+ })
925
+ }
926
+ }
927
+ if (isPpsBidQuery(query)) {
928
+ const allowed = filtered.filter(candidate => candidate.entry.tool_id === 'pps_bid_public_info')
929
+ if (allowed.length > 0) {
930
+ filtered = allowed.sort((a, b) => {
931
+ if (b.score !== a.score) return b.score - a.score
932
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
933
+ })
934
+ }
935
+ }
936
+ if (isProtectedCheckQuery(query)) {
937
+ const preference = protectedCheckToolPreference(query)
938
+ const allowed = filtered.filter(candidate => candidate.entry.primitive === 'check')
939
+ if (allowed.length > 0) {
940
+ filtered = allowed.sort((a, b) => {
941
+ const aIndex = preference.indexOf(a.entry.tool_id)
942
+ const bIndex = preference.indexOf(b.entry.tool_id)
943
+ const aRank = aIndex >= 0 ? aIndex : Number.MAX_SAFE_INTEGER
944
+ const bRank = bIndex >= 0 ? bIndex : Number.MAX_SAFE_INTEGER
945
+ if (aRank !== bRank) return aRank - bRank
946
+ if (b.score !== a.score) return b.score - a.score
947
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
948
+ })
949
+ }
950
+ }
951
+ if (isTagoBusQuery(query)) {
952
+ const allowed = filtered.filter(candidate => TAGO_BUS_TOOL_NAMES.has(candidate.entry.tool_id))
953
+ if (allowed.length > 0) {
954
+ filtered = allowed.sort((a, b) => {
955
+ if (b.score !== a.score) return b.score - a.score
956
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
957
+ })
958
+ }
959
+ }
960
+ if (isCollapseOrAedQuery(query)) {
961
+ const allowed = filtered.filter(candidate => {
962
+ if (candidate.entry.tool_id === 'nmc_aed_site_locate') return true
963
+ if (candidate.entry.tool_id === 'nmc_emergency_search') return true
964
+ return isLocationAdapter(candidate.entry)
965
+ })
966
+ if (allowed.some(candidate => candidate.entry.tool_id === 'nmc_aed_site_locate')) {
967
+ filtered = allowed.sort((a, b) => {
968
+ if (b.score !== a.score) return b.score - a.score
969
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
970
+ })
971
+ }
972
+ }
973
+ if (KMA_GIMHAE_AIRPORT_RE.test(query) && KMA_AIRPORT_AVIATION_RE.test(query)) {
974
+ filtered = filtered.filter(
975
+ candidate => candidate.entry.tool_id !== 'kma_apihub_url_air_amos_minute',
976
+ )
977
+ }
978
+ if (isAirportAviationQuery(query)) {
979
+ const hasAirUrlCandidate = filtered.some(candidate =>
980
+ KMA_URL_AIR_TOOL_NAMES.has(candidate.entry.tool_id),
981
+ )
982
+ if (hasAirUrlCandidate) {
983
+ filtered = filtered.filter(
984
+ candidate =>
985
+ !isLocationAdapter(candidate.entry) &&
986
+ candidate.entry.tool_id !== 'kma_current_observation',
987
+ )
988
+ }
989
+ }
990
+ return filtered
991
+ }
992
+
435
993
  export function selectTopKAdapterToolNamesForQuery(
436
994
  query: string,
437
995
  maxResults = 5,
@@ -439,7 +997,9 @@ export function selectTopKAdapterToolNamesForQuery(
439
997
  const normalizedQuery = query.trim()
440
998
  if (!normalizedQuery || maxResults <= 0) return []
441
999
  const queryTokens = expandedQueryTokens(normalizedQuery)
442
- const ranked = listAdapters()
1000
+ const ranked = filterSpecialCaseRanked(
1001
+ normalizedQuery,
1002
+ listAdapters()
443
1003
  .filter(entry => !ROOT_PRIMITIVE_TOOL_NAMES.has(entry.tool_id))
444
1004
  .map(entry => ({
445
1005
  entry,
@@ -449,7 +1009,8 @@ export function selectTopKAdapterToolNamesForQuery(
449
1009
  .sort((a, b) => {
450
1010
  if (b.score !== a.score) return b.score - a.score
451
1011
  return a.entry.tool_id.localeCompare(b.entry.tool_id)
452
- })
1012
+ }),
1013
+ )
453
1014
 
454
1015
  return pickDiverseAdapterToolNames(ranked, maxResults)
455
1016
  }
@@ -536,7 +1097,19 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
536
1097
  ].join('\n\n')
537
1098
  },
538
1099
 
539
- async validateInput(input) {
1100
+ async validateInput(input, context) {
1101
+ const directPublicDataChoice = validateDirectPublicDataToolChoice(
1102
+ entry.tool_id,
1103
+ context,
1104
+ input,
1105
+ )
1106
+ if (directPublicDataChoice) return directPublicDataChoice
1107
+ const kmaAviationChoice = validateKmaAviationToolChoice(entry.tool_id, context)
1108
+ if (kmaAviationChoice) return kmaAviationChoice
1109
+ const kmaAnalysisChoice = validateKmaAnalysisToolChoice(entry.tool_id, context)
1110
+ if (kmaAnalysisChoice) return kmaAnalysisChoice
1111
+ const nmcAedChoice = validateNmcAedToolChoice(entry.tool_id, context)
1112
+ if (nmcAedChoice) return nmcAedChoice
540
1113
  if (!resolveAdapter(entry.tool_id)) {
541
1114
  return {
542
1115
  result: false as const,
@@ -551,14 +1124,24 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
551
1124
  errorCode: 1,
552
1125
  }
553
1126
  }
1127
+ const contractInput = validateAdapterContractInput(
1128
+ entry.tool_id,
1129
+ input as Record<string, unknown>,
1130
+ )
1131
+ if (contractInput) return contractInput
554
1132
  return { result: true as const }
555
1133
  },
556
1134
 
557
1135
  async call(input, context) {
1136
+ const normalizedInput = normalizeDirectPublicDataToolInput(
1137
+ entry.tool_id,
1138
+ context,
1139
+ input,
1140
+ )
558
1141
  return dispatchPrimitive({
559
1142
  primitive,
560
1143
  toolName: entry.tool_id,
561
- args: input,
1144
+ args: normalizedInput,
562
1145
  context,
563
1146
  registry: getOrCreatePendingCallRegistry(),
564
1147
  bridge: getOrCreateUmmayaBridge(),