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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { ToolUseContext, ValidationResult } from '../../Tool.js'
|
|
2
|
+
import { resolveAdapter } from '../../services/api/adapterManifest.js'
|
|
3
|
+
import { isNonSyntheticUserMessageText } from './citizenUserText.js'
|
|
4
|
+
import { textFromContent } from './nmcAedGuard.js'
|
|
5
|
+
|
|
6
|
+
const PPS_BID_RE = /(입찰|나라장터|조달청|공고|전기공사|bid|procurement)/iu
|
|
7
|
+
const AIRKOREA_RE =
|
|
8
|
+
/(미세먼지|초미세먼지|대기질|대기오염|마스크|pm\s*2\.?5|pm\s*10|air\s*korea|airkorea)/iu
|
|
9
|
+
const KMA_CHART_RE = /(일기도|분석일기도|지도\s*자료|비구름|바람\s*흐름|synoptic|weather\s*chart)/iu
|
|
10
|
+
const KMA_WEATHER_RE =
|
|
11
|
+
/(날씨|현재\s*기상|실황|관측|예보|기온|습도|풍속|지금\s*비|비\s*(와|오|올|내리)|우산|강수|소나기|산책|퇴근|current\s+weather|forecast|rain|umbrella|precipitation|temperature)/iu
|
|
12
|
+
const TAGO_BUS_RE =
|
|
13
|
+
/(버스|시내버스|정류장|정류소|노선|도착|언제\s*와|몇\s*분|bus|route|arrival|station)/iu
|
|
14
|
+
const TAGO_ROUTE_NO_RE = /(?:^|[^\d])(\d{1,4}(?:-\d)?)\s*번/u
|
|
15
|
+
const KOREAN_SIDO_ABBREVIATIONS: Record<string, string> = {
|
|
16
|
+
서울: '서울특별시',
|
|
17
|
+
부산: '부산광역시',
|
|
18
|
+
대구: '대구광역시',
|
|
19
|
+
인천: '인천광역시',
|
|
20
|
+
광주: '광주광역시',
|
|
21
|
+
대전: '대전광역시',
|
|
22
|
+
울산: '울산광역시',
|
|
23
|
+
세종: '세종특별자치시',
|
|
24
|
+
경기: '경기도',
|
|
25
|
+
강원: '강원특별자치도',
|
|
26
|
+
충북: '충청북도',
|
|
27
|
+
충남: '충청남도',
|
|
28
|
+
전북: '전북특별자치도',
|
|
29
|
+
전남: '전라남도',
|
|
30
|
+
경북: '경상북도',
|
|
31
|
+
경남: '경상남도',
|
|
32
|
+
제주: '제주특별자치도',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const AIRKOREA_SIDO_ALIASES: Record<string, string> = {
|
|
36
|
+
서울특별시: '서울',
|
|
37
|
+
부산광역시: '부산',
|
|
38
|
+
대구광역시: '대구',
|
|
39
|
+
인천광역시: '인천',
|
|
40
|
+
광주광역시: '광주',
|
|
41
|
+
대전광역시: '대전',
|
|
42
|
+
울산광역시: '울산',
|
|
43
|
+
세종특별자치시: '세종',
|
|
44
|
+
경기도: '경기',
|
|
45
|
+
강원특별자치도: '강원',
|
|
46
|
+
강원도: '강원',
|
|
47
|
+
충청북도: '충북',
|
|
48
|
+
충청남도: '충남',
|
|
49
|
+
전북특별자치도: '전북',
|
|
50
|
+
전라북도: '전북',
|
|
51
|
+
전라남도: '전남',
|
|
52
|
+
경상북도: '경북',
|
|
53
|
+
경상남도: '경남',
|
|
54
|
+
제주특별자치도: '제주',
|
|
55
|
+
제주도: '제주',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type DirectTarget = {
|
|
59
|
+
toolIds: readonly string[]
|
|
60
|
+
label: string
|
|
61
|
+
hint: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
65
|
+
return typeof value === 'object' && value !== null
|
|
66
|
+
? (value as Record<string, unknown>)
|
|
67
|
+
: undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function messageRecord(message: unknown): Record<string, unknown> | undefined {
|
|
71
|
+
return asRecord(asRecord(message)?.message)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function messageRole(message: unknown): string | undefined {
|
|
75
|
+
const outer = asRecord(message)
|
|
76
|
+
const inner = messageRecord(message)
|
|
77
|
+
if (typeof inner?.role === 'string') return inner.role
|
|
78
|
+
if (typeof outer?.role === 'string') return outer.role
|
|
79
|
+
return typeof outer?.type === 'string' ? outer.type : undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function messageContent(message: unknown): unknown {
|
|
83
|
+
return messageRecord(message)?.content ?? asRecord(message)?.content
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function latestUserText(context: ToolUseContext): string {
|
|
87
|
+
const messages = Array.isArray(context.messages) ? context.messages : []
|
|
88
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
89
|
+
const message = messages[idx]
|
|
90
|
+
if (messageRole(message) !== 'user') continue
|
|
91
|
+
const text = textFromContent(messageContent(message))
|
|
92
|
+
if (isNonSyntheticUserMessageText(message, text)) return text
|
|
93
|
+
}
|
|
94
|
+
return ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function routeNoFromUserText(text: string): string | undefined {
|
|
98
|
+
const match = TAGO_ROUTE_NO_RE.exec(text)
|
|
99
|
+
return match?.[1]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sidoNameFromUserText(text: string): string | undefined {
|
|
103
|
+
for (const fullName of Object.values(KOREAN_SIDO_ABBREVIATIONS)) {
|
|
104
|
+
if (text.includes(fullName)) return fullName
|
|
105
|
+
}
|
|
106
|
+
for (const [shortName, fullName] of Object.entries(KOREAN_SIDO_ABBREVIATIONS)) {
|
|
107
|
+
const pattern = new RegExp(
|
|
108
|
+
`${shortName}(?:시|도|특별시|광역시|특별자치시|특별자치도)?`,
|
|
109
|
+
'u',
|
|
110
|
+
)
|
|
111
|
+
if (pattern.test(text)) return fullName
|
|
112
|
+
}
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function airkoreaSidoName(value: unknown): string | undefined {
|
|
117
|
+
if (typeof value !== 'string') return undefined
|
|
118
|
+
const trimmed = value.trim()
|
|
119
|
+
if (!trimmed) return undefined
|
|
120
|
+
return AIRKOREA_SIDO_ALIASES[trimmed] ?? trimmed
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ppsCurrentWeekWindow(now = new Date()): {
|
|
124
|
+
start: string
|
|
125
|
+
end: string
|
|
126
|
+
} {
|
|
127
|
+
const kstNow = new Date(now.getTime() + 9 * 60 * 60 * 1000)
|
|
128
|
+
const day = kstNow.getUTCDay()
|
|
129
|
+
const mondayOffset = (day + 6) % 7
|
|
130
|
+
const weekStart = new Date(
|
|
131
|
+
Date.UTC(
|
|
132
|
+
kstNow.getUTCFullYear(),
|
|
133
|
+
kstNow.getUTCMonth(),
|
|
134
|
+
kstNow.getUTCDate() - mondayOffset,
|
|
135
|
+
0,
|
|
136
|
+
0,
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
const todayEnd = new Date(
|
|
140
|
+
Date.UTC(
|
|
141
|
+
kstNow.getUTCFullYear(),
|
|
142
|
+
kstNow.getUTCMonth(),
|
|
143
|
+
kstNow.getUTCDate(),
|
|
144
|
+
23,
|
|
145
|
+
59,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
return { start: formatPpsDateTime(weekStart), end: formatPpsDateTime(todayEnd) }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatPpsDateTime(date: Date): string {
|
|
152
|
+
const year = String(date.getUTCFullYear()).padStart(4, '0')
|
|
153
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, '0')
|
|
154
|
+
const day = String(date.getUTCDate()).padStart(2, '0')
|
|
155
|
+
const hour = String(date.getUTCHours()).padStart(2, '0')
|
|
156
|
+
const minute = String(date.getUTCMinutes()).padStart(2, '0')
|
|
157
|
+
return `${year}${month}${day}${hour}${minute}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function directTargetForUserText(text: string): DirectTarget | undefined {
|
|
161
|
+
if (KMA_CHART_RE.test(text)) {
|
|
162
|
+
return {
|
|
163
|
+
toolIds: ['kma_apihub_url_analysis_weather_chart_image'],
|
|
164
|
+
label: 'kma_apihub_url_analysis_weather_chart_image',
|
|
165
|
+
hint:
|
|
166
|
+
"params must follow the weather chart/map schema: anal_time is required as UTC YYYYMMDDHHMM. Use the latest completed official analysis slot and include minutes, for example '202605281200', not a 10-digit KST hour.",
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (PPS_BID_RE.test(text)) {
|
|
170
|
+
return {
|
|
171
|
+
toolIds: ['pps_bid_public_info'],
|
|
172
|
+
label: 'pps_bid_public_info',
|
|
173
|
+
hint:
|
|
174
|
+
"params must include inqry_bgn_dt/inqry_end_dt as valid YYYYMMDDHHMM and inqry_div:'1' unless the citizen asks for opening datetime. Keep each PPS call within a 31-day window. Copy citizen keywords into bid_ntce_nm and use region/industry filters only when the citizen supplied them; do not hard-code 전기공사 or 부산광역시 for unrelated procurement questions.",
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (AIRKOREA_RE.test(text)) {
|
|
178
|
+
return {
|
|
179
|
+
toolIds: ['airkorea_ctprvn_air_quality'],
|
|
180
|
+
label: 'airkorea_ctprvn_air_quality',
|
|
181
|
+
hint:
|
|
182
|
+
"params should include sido_name:'부산', plus optional page_no/num_of_rows/ver exactly as the adapter schema exposes.",
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (TAGO_BUS_RE.test(text)) {
|
|
186
|
+
return {
|
|
187
|
+
toolIds: [
|
|
188
|
+
'tago_bus_station_search',
|
|
189
|
+
'tago_bus_arrival_search',
|
|
190
|
+
'tago_bus_route_search',
|
|
191
|
+
'tago_bus_route_station_search',
|
|
192
|
+
'tago_bus_location_search',
|
|
193
|
+
],
|
|
194
|
+
label: 'TAGO bus adapters',
|
|
195
|
+
hint:
|
|
196
|
+
"use TAGO bus schemas for bus arrival/route requests. If a route number is named, call tago_bus_route_search for route_id, then tago_bus_route_station_search with route_id and node_nm to find passing stops. Call tago_bus_arrival_search with city_code, node_id, and route_no or route_id. If node_id is unknown, tago_bus_station_search can list nearby named stops.",
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (KMA_WEATHER_RE.test(text)) {
|
|
200
|
+
return {
|
|
201
|
+
toolIds: [
|
|
202
|
+
'kakao_keyword_search',
|
|
203
|
+
'kakao_address_search',
|
|
204
|
+
'kakao_coord_to_region',
|
|
205
|
+
'kma_current_observation',
|
|
206
|
+
'kma_ultra_short_term_forecast',
|
|
207
|
+
'kma_short_term_forecast',
|
|
208
|
+
],
|
|
209
|
+
label: 'KMA weather/location adapters',
|
|
210
|
+
hint:
|
|
211
|
+
'use a location adapter first when coordinates are missing, then KMA current-observation or forecast schemas for rain/umbrella/current-weather values. Do not use air-quality or bus adapters for ordinary weather.',
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function hasAnyRegisteredTarget(target: DirectTarget): boolean {
|
|
218
|
+
return target.toolIds.some(toolId => resolveAdapter(toolId))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const KMA_CHART_PARAM_NAMES = new Set([
|
|
222
|
+
'anal_time',
|
|
223
|
+
'is_typ',
|
|
224
|
+
'image_type',
|
|
225
|
+
'group_name',
|
|
226
|
+
'meta',
|
|
227
|
+
])
|
|
228
|
+
const TAGO_TOOL_RE = /^tago_bus_/u
|
|
229
|
+
|
|
230
|
+
function validateTargetParams(
|
|
231
|
+
target: DirectTarget,
|
|
232
|
+
toolId: string,
|
|
233
|
+
params: unknown,
|
|
234
|
+
userText: string,
|
|
235
|
+
): ValidationResult | undefined {
|
|
236
|
+
const record = asRecord(params) ?? {}
|
|
237
|
+
if (target.toolIds[0] === 'kma_apihub_url_analysis_weather_chart_image' && toolId === target.toolIds[0]) {
|
|
238
|
+
const extra = Object.keys(record).filter(key => !KMA_CHART_PARAM_NAMES.has(key))
|
|
239
|
+
const analTime = record.anal_time
|
|
240
|
+
if (extra.length > 0 || typeof analTime !== 'string' || !/^\d{12}$/u.test(analTime)) {
|
|
241
|
+
return {
|
|
242
|
+
result: false,
|
|
243
|
+
message:
|
|
244
|
+
'KMA analysis weather-chart schema mismatch: call kma_apihub_url_analysis_weather_chart_image with chart params only. ' +
|
|
245
|
+
"Use anal_time as UTC YYYYMMDDHHMM, for example {\"anal_time\":\"202605281200\"}. " +
|
|
246
|
+
`Do not add non-chart fields such as ${extra.length > 0 ? extra.join(', ') : 'org'} or append UTC text inside the value.`,
|
|
247
|
+
errorCode: 1,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (TAGO_TOOL_RE.test(toolId) && /부산|busan/iu.test(userText)) {
|
|
253
|
+
const cityCode = record.city_code
|
|
254
|
+
if (cityCode !== '21') {
|
|
255
|
+
return {
|
|
256
|
+
result: false,
|
|
257
|
+
message:
|
|
258
|
+
'TAGO city_code mismatch: the citizen request is for Busan, and the official TAGO getCtyCodeList mapping is Busan=21. ' +
|
|
259
|
+
`Re-call the TAGO bus adapter with city_code:"21"; do not use non-Busan city_code ${String(cityCode)} for 부산.`,
|
|
260
|
+
errorCode: 1,
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (toolId === 'tago_bus_arrival_search') {
|
|
266
|
+
const routeNo = routeNoFromUserText(userText)
|
|
267
|
+
const routeNoParam = record.route_no
|
|
268
|
+
const routeIdParam = record.route_id
|
|
269
|
+
if (
|
|
270
|
+
routeNo !== undefined &&
|
|
271
|
+
typeof routeNoParam !== 'string' &&
|
|
272
|
+
typeof routeIdParam !== 'string'
|
|
273
|
+
) {
|
|
274
|
+
return {
|
|
275
|
+
result: false,
|
|
276
|
+
message:
|
|
277
|
+
`TAGO arrival route filter missing: the citizen asked for route ${routeNo}. ` +
|
|
278
|
+
`Re-call tago_bus_arrival_search with route_no:"${routeNo}" or a route_id ` +
|
|
279
|
+
'from tago_bus_route_search so the result is filtered against TAGO routeno/routeid. ' +
|
|
280
|
+
'When a place is also named, use tago_bus_route_station_search to find the matching nodeid.',
|
|
281
|
+
errorCode: 1,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (toolId === 'tago_bus_station_search' && typeof record.route_id === 'string') {
|
|
287
|
+
return {
|
|
288
|
+
result: false,
|
|
289
|
+
message:
|
|
290
|
+
'TAGO station schema mismatch: route_id is for route passing-stop lookup, not stop-name search. ' +
|
|
291
|
+
'Call tago_bus_route_station_search with city_code, route_id, and node_nm to find nodeid values on a route.',
|
|
292
|
+
errorCode: 1,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return undefined
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function normalizeDirectPublicDataToolInput(
|
|
300
|
+
toolId: string,
|
|
301
|
+
context: ToolUseContext,
|
|
302
|
+
input: unknown,
|
|
303
|
+
): unknown {
|
|
304
|
+
const userText = latestUserText(context)
|
|
305
|
+
const record = asRecord(input)
|
|
306
|
+
if (!record) return input
|
|
307
|
+
if (toolId === 'airkorea_ctprvn_air_quality') {
|
|
308
|
+
const normalized: Record<string, unknown> = { ...record }
|
|
309
|
+
const fromInput = airkoreaSidoName(normalized.sido_name)
|
|
310
|
+
const fromUser = airkoreaSidoName(sidoNameFromUserText(userText))
|
|
311
|
+
const sidoName = fromInput ?? fromUser
|
|
312
|
+
if (sidoName) normalized.sido_name = sidoName
|
|
313
|
+
if (
|
|
314
|
+
typeof normalized.num_of_rows !== 'number' ||
|
|
315
|
+
normalized.num_of_rows < 100
|
|
316
|
+
) {
|
|
317
|
+
normalized.num_of_rows = 100
|
|
318
|
+
}
|
|
319
|
+
return normalized
|
|
320
|
+
}
|
|
321
|
+
if (toolId !== 'pps_bid_public_info') return input
|
|
322
|
+
if (!PPS_BID_RE.test(userText)) return input
|
|
323
|
+
const normalized: Record<string, unknown> = { ...record }
|
|
324
|
+
if (/이번\s*주/u.test(userText)) {
|
|
325
|
+
const window = ppsCurrentWeekWindow()
|
|
326
|
+
normalized.inqry_bgn_dt = window.start
|
|
327
|
+
normalized.inqry_end_dt = window.end
|
|
328
|
+
normalized.inqry_div = '1'
|
|
329
|
+
}
|
|
330
|
+
if (/전기\s*공사/iu.test(userText)) {
|
|
331
|
+
normalized.bid_ntce_nm = '전기공사'
|
|
332
|
+
normalized.indstryty_nm = '전기공사업'
|
|
333
|
+
}
|
|
334
|
+
const regionName = sidoNameFromUserText(userText)
|
|
335
|
+
if (regionName) {
|
|
336
|
+
normalized.prtcpt_lmt_rgn_nm = regionName
|
|
337
|
+
normalized.region_name = regionName
|
|
338
|
+
}
|
|
339
|
+
return normalized
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function validateDirectPublicDataToolChoice(
|
|
343
|
+
toolId: string,
|
|
344
|
+
context: ToolUseContext,
|
|
345
|
+
params?: unknown,
|
|
346
|
+
): ValidationResult | undefined {
|
|
347
|
+
const userText = latestUserText(context)
|
|
348
|
+
const target = directTargetForUserText(userText)
|
|
349
|
+
if (!target) return undefined
|
|
350
|
+
if (target.toolIds.includes(toolId)) {
|
|
351
|
+
return validateTargetParams(target, toolId, params, userText)
|
|
352
|
+
}
|
|
353
|
+
if (!hasAnyRegisteredTarget(target)) return undefined
|
|
354
|
+
return {
|
|
355
|
+
result: false,
|
|
356
|
+
message:
|
|
357
|
+
`Public-data tool-choice mismatch: the latest citizen request matches ${target.label}. ` +
|
|
358
|
+
`Call ${target.label} through the correct primitive instead of ${toolId}. ` +
|
|
359
|
+
target.hint,
|
|
360
|
+
errorCode: 1,
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { ToolUseContext, ValidationResult } from '../../Tool.js'
|
|
2
|
+
import { isNonSyntheticUserMessageText } from './citizenUserText.js'
|
|
3
|
+
|
|
4
|
+
export const KMA_ANALYSIS_CHART_TOOL_NAME = 'kma_apihub_url_analysis_weather_chart_image'
|
|
5
|
+
const KMA_ANALYSIS_POINT_TOOL_NAMES = new Set([
|
|
6
|
+
'kma_apihub_url_high_resolution_grid_point',
|
|
7
|
+
'kma_apihub_url_aws_objective_analysis_grid',
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
const KMA_ANALYSIS_MAP_RE =
|
|
11
|
+
/(일기도|분석일기도|지도\s*자료|비구름|바람\s*흐름|synoptic|weather\s*chart)/iu
|
|
12
|
+
const KMA_ANALYSIS_COMPLETION_PROMPT =
|
|
13
|
+
'KMA analyzed weather-chart evidence chain complete: kma_apihub_url_analysis_weather_chart_image has already been attempted for this 일기도/지도/비구름/바람 흐름 request. The generic tool error hint is not permission to call or print a different tool in this turn. Do not emit <tool_call> text, JSON tool-call text, JSON with name/arguments, or point-grid/current-weather substitute requests. Write the final Korean answer now from the actual chart-tool result only. If the result is approval_state=approved with content_type=image/png or raw_format=image, say the official KMA chart image lookup succeeded, but do not infer rain-cloud or wind-flow details unless the tool result contains decoded chart semantics. If the result is APIHub approval-required, 403, no-data, or another upstream failure, report that failure directly and cite the official KMA channel as the handoff path.'
|
|
14
|
+
const KMA_ANALYSIS_REPAIR_PROMPT =
|
|
15
|
+
'KMA analyzed weather-chart final-answer repair: the previous assistant message was invalid because it printed tool-call text, asked for location/tool follow-up, or inferred chart semantics after the chart tool result. Do not call or print any tool. Do not ask for coordinates, a region, or another lookup. Write one Korean prose answer only. Use the existing kma_apihub_url_analysis_weather_chart_image result: if it says approval_state=approved and content_type=image/png or raw_format=image, state that the official KMA analyzed chart image was fetched successfully, and state that this text-only adapter result does not decode the chart pixels into rain-cloud or wind-flow semantics. Do not invent meteorological interpretation from the image.'
|
|
16
|
+
const KMA_ANALYSIS_MISSING_TOOL_PROMPT =
|
|
17
|
+
'Required KMA analyzed weather-chart lookup: the citizen asked for 전국 기상도, 위성 자료, 비구름 흐름, 바람 흐름, or analyzed map evidence. This is not a place-specific weather question, so do not ask for a location or coordinates. Before final prose, call kma_apihub_url_analysis_weather_chart_image directly if it is available. If only the legacy find primitive is available, call find with tool_id:"kma_apihub_url_analysis_weather_chart_image" and schema-valid chart params, including anal_time as the latest completed UTC YYYYMMDDHHMM slot. If APIHub returns approval-required, no-data, or another upstream failure, report that failure directly.'
|
|
18
|
+
const KMA_ANALYSIS_TOOL_CALL_TEXT_RE =
|
|
19
|
+
/<tool_call>|"name"\s*:\s*"kma_apihub_url_analysis_weather_chart_image"|"arguments"\s*:\s*\{/iu
|
|
20
|
+
const KMA_ANALYSIS_INVALID_FOLLOWUP_TEXT_RE =
|
|
21
|
+
/(현재\s*위치|위치\s*정보|정확한\s*좌표|지역명|알려주시면|어떤\s*방식|카카오|도구를\s*사용|확인하실\s*수\s*있습니다)/iu
|
|
22
|
+
|
|
23
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
24
|
+
return typeof value === 'object' && value !== null
|
|
25
|
+
? (value as Record<string, unknown>)
|
|
26
|
+
: undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function messageRecord(message: unknown): Record<string, unknown> | undefined {
|
|
30
|
+
return asRecord(asRecord(message)?.message)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function messageRole(message: unknown): string | undefined {
|
|
34
|
+
const record = asRecord(message)
|
|
35
|
+
const inner = messageRecord(message)
|
|
36
|
+
if (typeof inner?.role === 'string') return inner.role
|
|
37
|
+
if (typeof record?.role === 'string') return record.role
|
|
38
|
+
return typeof record?.type === 'string' ? record.type : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function messageContent(message: unknown): unknown {
|
|
42
|
+
return messageRecord(message)?.content ?? asRecord(message)?.content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function textFromContent(content: unknown): string {
|
|
46
|
+
if (typeof content === 'string') return content
|
|
47
|
+
if (!Array.isArray(content)) return ''
|
|
48
|
+
return content
|
|
49
|
+
.map(block => {
|
|
50
|
+
if (typeof block === 'string') return block
|
|
51
|
+
if (typeof block !== 'object' || block === null) return ''
|
|
52
|
+
const record = block as Record<string, unknown>
|
|
53
|
+
return typeof record.text === 'string' ? record.text : ''
|
|
54
|
+
})
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function latestUserText(context: ToolUseContext): string {
|
|
60
|
+
const messages = Array.isArray(context.messages) ? context.messages : []
|
|
61
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
62
|
+
const message = messages[idx]
|
|
63
|
+
if (messageRole(message) !== 'user') continue
|
|
64
|
+
const text = textFromContent(messageContent(message))
|
|
65
|
+
if (isNonSyntheticUserMessageText(message, text)) return text
|
|
66
|
+
}
|
|
67
|
+
return ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function userTextFromMessages(messages: readonly unknown[]): string {
|
|
71
|
+
return messages
|
|
72
|
+
.filter(message => messageRole(message) === 'user')
|
|
73
|
+
.map(message => ({ message, text: textFromContent(messageContent(message)) }))
|
|
74
|
+
.filter(({ message, text }) => isNonSyntheticUserMessageText(message, text))
|
|
75
|
+
.map(({ text }) => text)
|
|
76
|
+
.join('\n')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasToolUse(messages: readonly unknown[], toolName: string): boolean {
|
|
80
|
+
for (const message of messages) {
|
|
81
|
+
const content = messageContent(message)
|
|
82
|
+
if (!Array.isArray(content)) continue
|
|
83
|
+
for (const block of content) {
|
|
84
|
+
const record = asRecord(block)
|
|
85
|
+
if (record?.type !== 'tool_use') continue
|
|
86
|
+
const input = asRecord(record.input)
|
|
87
|
+
const nestedToolName =
|
|
88
|
+
typeof input?.tool_id === 'string' ? input.tool_id : undefined
|
|
89
|
+
const name = nestedToolName ?? record.name
|
|
90
|
+
if (name === toolName) return true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function latestAssistantText(messages: readonly unknown[]): string {
|
|
97
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
98
|
+
const message = messages[idx]
|
|
99
|
+
if (messageRole(message) !== 'assistant') continue
|
|
100
|
+
const text = textFromContent(messageContent(message))
|
|
101
|
+
if (text.trim()) return text
|
|
102
|
+
}
|
|
103
|
+
return ''
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasRepairPrompt(messages: readonly unknown[]): boolean {
|
|
107
|
+
return messages.some(message =>
|
|
108
|
+
textFromContent(messageContent(message)).includes(
|
|
109
|
+
'KMA analyzed weather-chart final-answer repair',
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasMissingToolPrompt(messages: readonly unknown[]): boolean {
|
|
115
|
+
return messages.some(message =>
|
|
116
|
+
textFromContent(messageContent(message)).includes(
|
|
117
|
+
'Required KMA analyzed weather-chart lookup',
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isKmaAnalysisMapText(text: string): boolean {
|
|
123
|
+
return KMA_ANALYSIS_MAP_RE.test(text)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function validateKmaAnalysisToolChoice(
|
|
127
|
+
toolId: string,
|
|
128
|
+
context: ToolUseContext,
|
|
129
|
+
): ValidationResult | undefined {
|
|
130
|
+
if (!KMA_ANALYSIS_POINT_TOOL_NAMES.has(toolId)) return undefined
|
|
131
|
+
const userText = latestUserText(context)
|
|
132
|
+
if (!isKmaAnalysisMapText(userText)) return undefined
|
|
133
|
+
return {
|
|
134
|
+
result: false,
|
|
135
|
+
message:
|
|
136
|
+
'KMA analysis tool-choice mismatch: the latest citizen request asks for analyzed weather chart/map evidence. Call ' +
|
|
137
|
+
`${KMA_ANALYSIS_CHART_TOOL_NAME} for 일기도/지도/비구름/바람 흐름 requests. ` +
|
|
138
|
+
'If APIHub returns approval-required or another upstream error, report that failure directly and do not substitute point-grid data.',
|
|
139
|
+
errorCode: 1,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildKmaAnalysisCompletionPromptIfNeeded({
|
|
144
|
+
messages,
|
|
145
|
+
}: {
|
|
146
|
+
messages: readonly unknown[]
|
|
147
|
+
}): string | undefined {
|
|
148
|
+
if (!isKmaAnalysisMapText(userTextFromMessages(messages))) return undefined
|
|
149
|
+
if (!hasToolUse(messages, KMA_ANALYSIS_CHART_TOOL_NAME)) return undefined
|
|
150
|
+
return KMA_ANALYSIS_COMPLETION_PROMPT
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function buildKmaAnalysisFinalAnswerRepairPromptIfNeeded({
|
|
154
|
+
messages,
|
|
155
|
+
}: {
|
|
156
|
+
messages: readonly unknown[]
|
|
157
|
+
}): string | undefined {
|
|
158
|
+
if (!isKmaAnalysisMapText(userTextFromMessages(messages))) return undefined
|
|
159
|
+
if (!hasToolUse(messages, KMA_ANALYSIS_CHART_TOOL_NAME)) return undefined
|
|
160
|
+
if (hasRepairPrompt(messages)) return undefined
|
|
161
|
+
const assistantText = latestAssistantText(messages)
|
|
162
|
+
if (
|
|
163
|
+
!KMA_ANALYSIS_TOOL_CALL_TEXT_RE.test(assistantText) &&
|
|
164
|
+
!KMA_ANALYSIS_INVALID_FOLLOWUP_TEXT_RE.test(assistantText)
|
|
165
|
+
) {
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
return KMA_ANALYSIS_REPAIR_PROMPT
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildKmaAnalysisMissingToolPromptIfNeeded({
|
|
172
|
+
messages,
|
|
173
|
+
}: {
|
|
174
|
+
messages: readonly unknown[]
|
|
175
|
+
}): string | undefined {
|
|
176
|
+
if (!isKmaAnalysisMapText(userTextFromMessages(messages))) return undefined
|
|
177
|
+
if (hasToolUse(messages, KMA_ANALYSIS_CHART_TOOL_NAME)) return undefined
|
|
178
|
+
if (hasMissingToolPrompt(messages)) return undefined
|
|
179
|
+
const assistantText = latestAssistantText(messages)
|
|
180
|
+
if (!assistantText.trim()) return undefined
|
|
181
|
+
return KMA_ANALYSIS_MISSING_TOOL_PROMPT
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function shouldWithholdKmaAnalysisToolCallText({
|
|
185
|
+
messages,
|
|
186
|
+
candidate,
|
|
187
|
+
}: {
|
|
188
|
+
messages: readonly unknown[]
|
|
189
|
+
candidate: unknown
|
|
190
|
+
}): boolean {
|
|
191
|
+
if (hasRepairPrompt(messages)) return false
|
|
192
|
+
return (
|
|
193
|
+
buildKmaAnalysisFinalAnswerRepairPromptIfNeeded({
|
|
194
|
+
messages: [...messages, candidate],
|
|
195
|
+
}) !== undefined
|
|
196
|
+
)
|
|
197
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ToolUseContext, ValidationResult } from '../../Tool.js'
|
|
2
|
+
import { listAdapters } from '../../services/api/adapterManifest.js'
|
|
3
|
+
import { textFromContent } from './nmcAedGuard.js'
|
|
4
|
+
import { isNonSyntheticUserMessageText } from './citizenUserText.js'
|
|
5
|
+
|
|
6
|
+
const KMA_AIR_TOOL_NAMES = new Set([
|
|
7
|
+
'kma_apihub_url_air_amos_minute',
|
|
8
|
+
'kma_apihub_url_air_metar_decoded',
|
|
9
|
+
])
|
|
10
|
+
const ORDINARY_WEATHER_TOOL_NAMES = new Set([
|
|
11
|
+
'kma_current_observation',
|
|
12
|
+
'kma_ultra_short_term_forecast',
|
|
13
|
+
'kma_short_term_forecast',
|
|
14
|
+
])
|
|
15
|
+
const LOCATION_TOOL_NAMES = new Set([
|
|
16
|
+
'kakao_keyword_search',
|
|
17
|
+
'kakao_address_search',
|
|
18
|
+
'kakao_coord_to_region',
|
|
19
|
+
])
|
|
20
|
+
const AIRPORT_PLACE_RE =
|
|
21
|
+
/(김해|김포|김해공항|김포공항|gimhae|gimpo|rkpk|rkss|\bairport\b|공항)/iu
|
|
22
|
+
const AIRPORT_AVIATION_RE =
|
|
23
|
+
/(비행기|항공편|비행편|운항|이륙|착륙|결항|지연|뜰\s*만|뜨나|뜰\s*수|flight|take\s*off|landing|delay|cancel|metar|speci|amos|rvr|활주로|시정|visibility|공항기상|항공기상)/iu
|
|
24
|
+
|
|
25
|
+
function latestUserText(context: ToolUseContext): string {
|
|
26
|
+
const messages = Array.isArray(context.messages) ? context.messages : []
|
|
27
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
28
|
+
const message = messages[idx] as Record<string, unknown>
|
|
29
|
+
const inner = message.message as Record<string, unknown> | undefined
|
|
30
|
+
const role =
|
|
31
|
+
typeof inner?.role === 'string'
|
|
32
|
+
? inner.role
|
|
33
|
+
: typeof message.role === 'string'
|
|
34
|
+
? message.role
|
|
35
|
+
: message.type
|
|
36
|
+
if (role !== 'user') continue
|
|
37
|
+
const text = textFromContent(inner?.content ?? message.content)
|
|
38
|
+
if (isNonSyntheticUserMessageText(message, text)) return text
|
|
39
|
+
}
|
|
40
|
+
return ''
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isAirportAviationQuery(text: string): boolean {
|
|
44
|
+
return AIRPORT_PLACE_RE.test(text) && AIRPORT_AVIATION_RE.test(text)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasAviationAdapter(): boolean {
|
|
48
|
+
return listAdapters().some(entry => KMA_AIR_TOOL_NAMES.has(entry.tool_id))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateKmaAviationToolChoice(
|
|
52
|
+
toolId: string,
|
|
53
|
+
context: ToolUseContext,
|
|
54
|
+
): ValidationResult | undefined {
|
|
55
|
+
if (KMA_AIR_TOOL_NAMES.has(toolId)) return undefined
|
|
56
|
+
if (!LOCATION_TOOL_NAMES.has(toolId) && !ORDINARY_WEATHER_TOOL_NAMES.has(toolId)) {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
const userText = latestUserText(context)
|
|
60
|
+
if (!isAirportAviationQuery(userText)) return undefined
|
|
61
|
+
if (!hasAviationAdapter()) return undefined
|
|
62
|
+
return {
|
|
63
|
+
result: false,
|
|
64
|
+
message:
|
|
65
|
+
'KMA aviation tool-choice mismatch: the latest citizen request asks for airport METAR/AMOS aviation evidence such as flight operation, wind, runway, RVR, or visibility. ' +
|
|
66
|
+
'Call kma_apihub_url_air_metar_decoded for airport METAR/시정/풍향/풍속 evidence, or kma_apihub_url_air_amos_minute for AMOS runway-minute evidence when that airport/station is documented. ' +
|
|
67
|
+
'Do not call locate or ordinary KMA current-observation tools first because they do not provide airport METAR/AMOS visibility evidence.',
|
|
68
|
+
errorCode: 1,
|
|
69
|
+
}
|
|
70
|
+
}
|