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
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import type { BetaToolChoiceTool } from '../../sdk-compat.js'
|
|
2
|
+
import type { Tools } from '../../Tool.js'
|
|
3
|
+
import type { Message } from '../../types/message.js'
|
|
4
|
+
import {
|
|
5
|
+
buildNmcAedFollowupPromptIfNeeded,
|
|
6
|
+
textFromContent,
|
|
7
|
+
} from './nmcAedGuard.js'
|
|
8
|
+
import { resolveAdapter } from '../../services/api/adapterManifest.js'
|
|
9
|
+
import {
|
|
10
|
+
isKmaAnalysisMapText,
|
|
11
|
+
KMA_ANALYSIS_CHART_TOOL_NAME,
|
|
12
|
+
} from './kmaAnalysisGuard.js'
|
|
13
|
+
import { isNonSyntheticUserMessageText } from './citizenUserText.js'
|
|
14
|
+
|
|
15
|
+
const KMA_AIR_TOOLS = [
|
|
16
|
+
'kma_apihub_url_air_metar_decoded',
|
|
17
|
+
'kma_apihub_url_air_amos_minute',
|
|
18
|
+
] as const
|
|
19
|
+
const NMC_EMERGENCY_TOOL_NAME = 'nmc_emergency_search'
|
|
20
|
+
const NMC_AED_TOOL_NAME = 'nmc_aed_site_locate'
|
|
21
|
+
const KAKAO_COORD_TO_REGION_TOOL_NAME = 'kakao_coord_to_region'
|
|
22
|
+
const TAGO_STATION_TOOL_NAME = 'tago_bus_station_search'
|
|
23
|
+
const TAGO_ROUTE_TOOL_NAME = 'tago_bus_route_search'
|
|
24
|
+
const TAGO_ROUTE_STATION_TOOL_NAME = 'tago_bus_route_station_search'
|
|
25
|
+
const TAGO_ARRIVAL_TOOL_NAME = 'tago_bus_arrival_search'
|
|
26
|
+
const PPS_BID_TOOL_NAME = 'pps_bid_public_info'
|
|
27
|
+
const AIRKOREA_TOOL_NAME = 'airkorea_ctprvn_air_quality'
|
|
28
|
+
const PROTECTED_CHECK_TOOLS = [
|
|
29
|
+
'mock_verify_module_simple_auth',
|
|
30
|
+
'mock_verify_ganpyeon_injeung',
|
|
31
|
+
'mock_verify_mobile_id',
|
|
32
|
+
'mock_verify_mydata',
|
|
33
|
+
] as const
|
|
34
|
+
|
|
35
|
+
const AIRPORT_PLACE_RE =
|
|
36
|
+
/(김해|김포|김해공항|김포공항|gimhae|gimpo|rkpk|rkss|\bairport\b|공항)/iu
|
|
37
|
+
const AIRPORT_AVIATION_RE =
|
|
38
|
+
/(비행기|항공편|비행편|운항|이륙|착륙|결항|지연|뜰\s*만|뜨나|뜰\s*수|flight|take\s*off|landing|delay|cancel|metar|speci|amos|rvr|활주로|시정|visibility|공항기상|항공기상)/iu
|
|
39
|
+
const AMOS_PREFERENCE_RE = /(amos|rvr|활주로|runway)/iu
|
|
40
|
+
const GIMPO_RE = /(김포|김포공항|gimpo|rkss)/iu
|
|
41
|
+
const PROTECTED_QUERY_RE =
|
|
42
|
+
/(본인확인|인증|간편인증|모바일\s*(?:신분증|id)|mobile\s*id|마이데이터|mydata|증명원|소득금액증명|소득금액증명원|주민등록등본|민원|발급)/iu
|
|
43
|
+
const MOBILE_ID_RE = /(mobile\s*id|모바일\s*(?:신분증|id)|mobile_id)/iu
|
|
44
|
+
const SIMPLE_AUTH_RE =
|
|
45
|
+
/(simple_auth|간편인증|ganpyeon|소득금액증명|증명원|민원|발급)/iu
|
|
46
|
+
const MYDATA_RE = /(mydata|마이데이터)/iu
|
|
47
|
+
const MEDICAL_COLLAPSE_RE =
|
|
48
|
+
/(사람이\s*쓰러|쓰러졌|쓰러짐|쓰러져|의식[을이가은는\s]*(없|잃|불명)|무의식|심정지|심폐소생|cpr|호흡\s*(없|곤란)|숨\s*(안|못)|collapse|collapsed|unconscious|cardiac\s*arrest|not\s*breathing|aed|자동심장|심장충격|제세동)/iu
|
|
49
|
+
const MEDICAL_COLLAPSE_OR_ER_RE =
|
|
50
|
+
/(사람이\s*쓰러|쓰러졌|쓰러짐|쓰러져|의식[을이가은는\s]*(없|잃|불명)|무의식|심정지|심폐소생|cpr|호흡\s*(없|곤란)|숨\s*(안|못)|collapse|collapsed|unconscious|cardiac\s*arrest|not\s*breathing|응급실|응급의료기관|응급의료센터|emergency\s*room|\ber\b)/iu
|
|
51
|
+
const NON_MEDICAL_EMERGENCY_RE =
|
|
52
|
+
/(비상벨|안심벨|emergency\s*(call\s*)?box|call\s*box)/iu
|
|
53
|
+
const TAGO_BUS_RE =
|
|
54
|
+
/(버스|시내버스|정류장|정류소|노선|도착|언제\s*와|몇\s*분|bus|route|arrival|station)/iu
|
|
55
|
+
const TAGO_ROUTE_NO_RE = /(?:^|[^\d])(\d{1,4}(?:-\d)?)\s*번/u
|
|
56
|
+
const TAGO_PLACE_RE = /([가-힣A-Za-z0-9().·\s]+?)(?:에서|근처|앞|인근)/u
|
|
57
|
+
const PPS_BID_RE = /(입찰|나라장터|조달청|공고|공사조회|전기공사|bid|procurement)/iu
|
|
58
|
+
const AIRKOREA_RE =
|
|
59
|
+
/(미세먼지|초미세먼지|대기질|대기오염|마스크|pm\s*2\.?5|pm\s*10|air\s*korea|airkorea)/iu
|
|
60
|
+
const PUBLIC_DATA_MISMATCH_CALL_RE =
|
|
61
|
+
/Public-data tool-choice mismatch:[\s\S]*?\bCall\s+([a-z][a-z0-9_]*)\s+/iu
|
|
62
|
+
const TAGO_BUS_COMPLETION_PROMPT =
|
|
63
|
+
'TAGO bus arrival evidence chain complete: the route search, route passing-stop lookup, and arrival lookup have already been attempted for this bus-arrival request. Do not call another location, public-data, or TAGO bus tool in this turn. Write the final Korean answer now from the actual TAGO results only. If the arrival result has zero items, say that no current matching arrival is shown for the checked stop/direction, and include the route/stop evidence that was found.'
|
|
64
|
+
const TAGO_BUS_REPAIR_PROMPT =
|
|
65
|
+
'TAGO bus final answer repair: the previous answer still promised another stop/route lookup after TAGO arrival evidence was already attempted. Rewrite the final Korean answer now from the actual TAGO tool_result only. Do not say 확인하겠습니다, 확인해보겠습니다, 검색해 보겠습니다, 다시 조회, or that you will check another stop. If the arrival result has zero items, say no current arrival is shown for the checked 부산역 stop and ask the citizen for a specific route number or exact stop only as a next-step option.'
|
|
66
|
+
const AIRKOREA_COMPLETION_PROMPT =
|
|
67
|
+
'AirKorea air-quality evidence complete: airkorea_ctprvn_air_quality has already returned the official result for this 미세먼지/초미세먼지 request. Do not call another location, weather, public-data, or AirKorea tool in this turn. Write the final Korean answer now from the actual tool_result only. Include stationName, dataTime, PM10 value and pm10GradeLabelKo, PM2.5 value and pm25GradeLabelKo, and CAI/khaiValue with khaiGradeLabelKo when present. This adapter returns city/province measurement rows, not a geocoded nearest-station result: say it is city/province station data, use only exact stationName rows present in tool_result, and do not infer the citizen place district, distance, nearest station, station groups, or value ranges unless those exact fields exist in tool_result. If totalCount is 0 or items are empty, say the official AirKorea API returned no rows for the checked sidoName and do not say you are still checking.'
|
|
68
|
+
const AIRKOREA_REPAIR_PROMPT =
|
|
69
|
+
'AirKorea final answer repair: the previous answer inferred nearest/direction/distance, average/range values, district groups, or changed raw values beyond the official tool_result. Rewrite the final Korean answer using only the exact AirKorea rows listed below. Do not say 가장 가까운, 인근, 동쪽, 서쪽, 남쪽, 북쪽, distance, nearest, 평균, 범위, 대부분, 해운대구 지역, or average unless those exact fields exist in tool_result. Copy PM10, PM2.5, CAI, stationName, dataTime, and grade labels literally. If the citizen named a place but the tool returned only city/province rows, say this is city/province station data rather than a nearest-station answer.'
|
|
70
|
+
const AIRKOREA_UNSUPPORTED_LOCATION_CLAIM_RE =
|
|
71
|
+
/(가장\s*가까운|인근|동쪽|서쪽|남쪽|북쪽|거리|평균|범위|대부분|전체적으로|해운대구\s*지역|명확한\s*측정소|지역에\s*해당|nearest|distance|average|range)/iu
|
|
72
|
+
const TAGO_BUS_PENDING_FINAL_RE =
|
|
73
|
+
/(확인해\s*보겠습니다|확인하겠습니다|검색해\s*보겠습니다|검색하겠습니다|다른\s*정류장|다시\s*(?:조회|확인|검색)|조회하겠습니다|will\s+check|will\s+search|will\s+look\s+up)/iu
|
|
74
|
+
const GENERIC_PENDING_FINAL_RE =
|
|
75
|
+
/(답변을?\s*제공하겠습니다|제공하겠습니다|확인해\s*보겠습니다|확인하겠습니다|조회하겠습니다|찾아보겠습니다|검색해\s*보겠습니다|검색하겠습니다|최종\s*답변은|final answer should|will\s+(?:answer|provide|check|search|look\s+up))/iu
|
|
76
|
+
const GENERIC_PENDING_FINAL_REPAIR_PROMPT =
|
|
77
|
+
'Final answer repair: successful tool_result evidence already exists, but the previous assistant message was still a plan or promise to answer later. Write the final Korean answer now from the actual tool_result values only. Do not say 제공하겠습니다, 확인하겠습니다, 조회하겠습니다, 찾아보겠습니다, 검색해 보겠습니다, or describe what you will answer next.'
|
|
78
|
+
|
|
79
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
80
|
+
return typeof value === 'object' && value !== null
|
|
81
|
+
? (value as Record<string, unknown>)
|
|
82
|
+
: undefined
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function messageRecord(message: unknown): Record<string, unknown> | undefined {
|
|
86
|
+
return asRecord(asRecord(message)?.message)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function messageRole(message: unknown): string | undefined {
|
|
90
|
+
const outer = asRecord(message)
|
|
91
|
+
const inner = messageRecord(message)
|
|
92
|
+
if (typeof inner?.role === 'string') return inner.role
|
|
93
|
+
if (typeof outer?.role === 'string') return outer.role
|
|
94
|
+
return typeof outer?.type === 'string' ? outer.type : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function messageContent(message: unknown): unknown {
|
|
98
|
+
return messageRecord(message)?.content ?? asRecord(message)?.content
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function latestUserText(messages: readonly unknown[]): string {
|
|
102
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
103
|
+
const message = messages[idx]
|
|
104
|
+
if (messageRole(message) !== 'user') continue
|
|
105
|
+
const text = textFromContent(messageContent(message))
|
|
106
|
+
if (isNonSyntheticUserMessageText(message, text)) return text
|
|
107
|
+
}
|
|
108
|
+
return ''
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function toolUseNames(messages: readonly unknown[]): Set<string> {
|
|
112
|
+
const names = new Set<string>()
|
|
113
|
+
for (const message of messages) {
|
|
114
|
+
const content = messageContent(message)
|
|
115
|
+
if (!Array.isArray(content)) continue
|
|
116
|
+
for (const block of content) {
|
|
117
|
+
const record = asRecord(block)
|
|
118
|
+
if (record?.type === 'tool_use') {
|
|
119
|
+
const input = asRecord(record.input)
|
|
120
|
+
const nestedToolName =
|
|
121
|
+
typeof input?.tool_id === 'string' ? input.tool_id : undefined
|
|
122
|
+
if (nestedToolName) names.add(nestedToolName)
|
|
123
|
+
else if (typeof record.name === 'string') names.add(record.name)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (record?.type !== 'tool_result') continue
|
|
127
|
+
if (typeof record.content !== 'string') continue
|
|
128
|
+
const parsed = parseJsonRecord(record.content)
|
|
129
|
+
const source = asRecord(asRecord(parsed?.result)?.meta)?.source
|
|
130
|
+
if (typeof source === 'string') names.add(source)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return names
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function latestToolResultText(messages: readonly unknown[]): string {
|
|
137
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
138
|
+
const content = messageContent(messages[idx])
|
|
139
|
+
if (!Array.isArray(content)) continue
|
|
140
|
+
const parts: string[] = []
|
|
141
|
+
for (const block of content) {
|
|
142
|
+
const record = asRecord(block)
|
|
143
|
+
if (record?.type !== 'tool_result') continue
|
|
144
|
+
if (typeof record.content === 'string') {
|
|
145
|
+
parts.push(record.content)
|
|
146
|
+
} else {
|
|
147
|
+
const text = textFromContent(record.content)
|
|
148
|
+
if (text.trim()) parts.push(text)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (parts.length > 0) return parts.join('\n')
|
|
152
|
+
}
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function latestAssistantText(messages: readonly unknown[]): string {
|
|
157
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
158
|
+
if (messageRole(messages[idx]) !== 'assistant') continue
|
|
159
|
+
const text = textFromContent(messageContent(messages[idx]))
|
|
160
|
+
if (text.trim()) return text
|
|
161
|
+
}
|
|
162
|
+
return ''
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasAirKoreaRepairPrompt(messages: readonly unknown[]): boolean {
|
|
166
|
+
return messages.some(message => textFromContent(messageContent(message)).includes('AirKorea final answer repair'))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasTagoBusRepairPrompt(messages: readonly unknown[]): boolean {
|
|
170
|
+
return messages.some(message => textFromContent(messageContent(message)).includes('TAGO bus final answer repair'))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasGenericPendingFinalRepairPrompt(messages: readonly unknown[]): boolean {
|
|
174
|
+
return messages.some(message => textFromContent(messageContent(message)).includes('Final answer repair: successful tool_result'))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasToolResult(messages: readonly unknown[]): boolean {
|
|
178
|
+
return messages.some(message => {
|
|
179
|
+
const content = messageContent(message)
|
|
180
|
+
if (!Array.isArray(content)) return false
|
|
181
|
+
return content.some(block => asRecord(block)?.type === 'tool_result')
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function toolResultTextsFor(
|
|
186
|
+
messages: readonly unknown[],
|
|
187
|
+
toolName: string,
|
|
188
|
+
): string[] {
|
|
189
|
+
const idToName = toolUseById(messages)
|
|
190
|
+
const texts: string[] = []
|
|
191
|
+
for (const message of messages) {
|
|
192
|
+
const content = messageContent(message)
|
|
193
|
+
if (!Array.isArray(content)) continue
|
|
194
|
+
for (const block of content) {
|
|
195
|
+
const record = asRecord(block)
|
|
196
|
+
if (record?.type !== 'tool_result') continue
|
|
197
|
+
if (typeof record.tool_use_id !== 'string') continue
|
|
198
|
+
const mappedToolName = idToName.get(record.tool_use_id)
|
|
199
|
+
if (mappedToolName === toolName) {
|
|
200
|
+
if (typeof record.content === 'string') texts.push(record.content)
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
if (typeof record.content !== 'string') continue
|
|
204
|
+
const parsed = parseJsonRecord(record.content)
|
|
205
|
+
const source = asRecord(asRecord(parsed?.result)?.meta)?.source
|
|
206
|
+
if (source === toolName) texts.push(record.content)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return texts
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function toolUseById(messages: readonly unknown[]): Map<string, string> {
|
|
213
|
+
const ids = new Map<string, string>()
|
|
214
|
+
for (const message of messages) {
|
|
215
|
+
const content = messageContent(message)
|
|
216
|
+
if (!Array.isArray(content)) continue
|
|
217
|
+
for (const block of content) {
|
|
218
|
+
const record = asRecord(block)
|
|
219
|
+
if (record?.type !== 'tool_use') continue
|
|
220
|
+
if (typeof record.id !== 'string' || typeof record.name !== 'string') {
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
const input = asRecord(record.input)
|
|
224
|
+
const nestedToolName =
|
|
225
|
+
typeof input?.tool_id === 'string' ? input.tool_id : undefined
|
|
226
|
+
ids.set(record.id, nestedToolName ?? record.name)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return ids
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseJsonRecord(value: string): Record<string, unknown> | undefined {
|
|
233
|
+
try {
|
|
234
|
+
return asRecord(JSON.parse(value))
|
|
235
|
+
} catch {
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function stringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
241
|
+
const value = record[key]
|
|
242
|
+
if (value === null || value === undefined) return undefined
|
|
243
|
+
const text = String(value).trim()
|
|
244
|
+
return text === '' ? undefined : text
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function airKoreaExactRowsSummary(messages: readonly unknown[]): string | undefined {
|
|
248
|
+
const rows: string[] = []
|
|
249
|
+
for (const text of toolResultTextsFor(messages, AIRKOREA_TOOL_NAME)) {
|
|
250
|
+
const parsed = parseJsonRecord(text)
|
|
251
|
+
const result = asRecord(parsed?.result)
|
|
252
|
+
const items = Array.isArray(result?.items) ? result.items : []
|
|
253
|
+
for (const item of items) {
|
|
254
|
+
const record = asRecord(asRecord(item)?.record)
|
|
255
|
+
if (!record) continue
|
|
256
|
+
const stationName = stringField(record, 'stationName')
|
|
257
|
+
const dataTime = stringField(record, 'dataTime')
|
|
258
|
+
const pm10Value = stringField(record, 'pm10Value')
|
|
259
|
+
const pm10Grade = stringField(record, 'pm10GradeLabelKo')
|
|
260
|
+
const pm25Value = stringField(record, 'pm25Value')
|
|
261
|
+
const pm25Grade = stringField(record, 'pm25GradeLabelKo')
|
|
262
|
+
const khaiValue = stringField(record, 'khaiValue')
|
|
263
|
+
const khaiGrade = stringField(record, 'khaiGradeLabelKo')
|
|
264
|
+
if (!stationName) continue
|
|
265
|
+
rows.push(
|
|
266
|
+
`- ${stationName}: dataTime=${dataTime ?? '없음'}, PM10=${pm10Value ?? '없음'}${pm10Grade ? `(${pm10Grade})` : ''}, PM2.5=${pm25Value ?? '없음'}${pm25Grade ? `(${pm25Grade})` : ''}, CAI=${khaiValue ?? '없음'}${khaiGrade ? `(${khaiGrade})` : ''}`,
|
|
267
|
+
)
|
|
268
|
+
if (rows.length >= 8) break
|
|
269
|
+
}
|
|
270
|
+
if (rows.length >= 8) break
|
|
271
|
+
}
|
|
272
|
+
if (rows.length === 0) return undefined
|
|
273
|
+
return `\n\nExact AirKorea rows you may cite verbatim:\n${rows.join('\n')}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function hasSuccessfulRegionLocateResult(messages: readonly unknown[]): boolean {
|
|
277
|
+
const idToName = toolUseById(messages)
|
|
278
|
+
for (const message of messages) {
|
|
279
|
+
const content = messageContent(message)
|
|
280
|
+
if (!Array.isArray(content)) continue
|
|
281
|
+
for (const block of content) {
|
|
282
|
+
const record = asRecord(block)
|
|
283
|
+
if (record?.type !== 'tool_result') continue
|
|
284
|
+
if (record.is_error === true) continue
|
|
285
|
+
if (typeof record.tool_use_id !== 'string') continue
|
|
286
|
+
const toolName = idToName.get(record.tool_use_id)
|
|
287
|
+
if (toolName !== 'locate' && !toolName?.startsWith('kakao_')) continue
|
|
288
|
+
if (typeof record.content !== 'string') continue
|
|
289
|
+
const parsed = parseJsonRecord(record.content)
|
|
290
|
+
if (parsed?.ok === false) continue
|
|
291
|
+
const result = asRecord(parsed?.result ?? parsed)
|
|
292
|
+
if (result?.kind !== 'region') continue
|
|
293
|
+
if (
|
|
294
|
+
typeof result.region_1depth_name === 'string' &&
|
|
295
|
+
typeof result.region_2depth_name === 'string'
|
|
296
|
+
) {
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function hasSuccessfulPoiLocateResult(messages: readonly unknown[]): boolean {
|
|
305
|
+
const idToName = toolUseById(messages)
|
|
306
|
+
for (const message of messages) {
|
|
307
|
+
const content = messageContent(message)
|
|
308
|
+
if (!Array.isArray(content)) continue
|
|
309
|
+
for (const block of content) {
|
|
310
|
+
const record = asRecord(block)
|
|
311
|
+
if (record?.type !== 'tool_result') continue
|
|
312
|
+
if (record.is_error === true) continue
|
|
313
|
+
if (typeof record.tool_use_id !== 'string') continue
|
|
314
|
+
const toolName = idToName.get(record.tool_use_id)
|
|
315
|
+
if (toolName !== 'locate' && !toolName?.startsWith('kakao_')) continue
|
|
316
|
+
if (typeof record.content !== 'string') continue
|
|
317
|
+
const parsed = parseJsonRecord(record.content)
|
|
318
|
+
if (parsed?.ok === false) continue
|
|
319
|
+
const result = asRecord(parsed?.result ?? parsed)
|
|
320
|
+
if (result?.kind !== 'poi') continue
|
|
321
|
+
if (typeof result.lat === 'number' && typeof result.lon === 'number') {
|
|
322
|
+
return true
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function availableToolNamesFromTools(tools: Tools): Set<string> {
|
|
330
|
+
return new Set(tools.map(tool => tool.name))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isAvailableOrSyncedAdapter(
|
|
334
|
+
available: Set<string>,
|
|
335
|
+
toolName: string,
|
|
336
|
+
): boolean {
|
|
337
|
+
return available.has(toolName) || resolveAdapter(toolName) !== undefined
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function chooseAvailableOrSyncedAdapter(
|
|
341
|
+
available: Set<string>,
|
|
342
|
+
candidates: readonly string[],
|
|
343
|
+
): string | undefined {
|
|
344
|
+
return candidates.find(candidate => isAvailableOrSyncedAdapter(available, candidate))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isAirportAviationQuery(text: string): boolean {
|
|
348
|
+
return AIRPORT_PLACE_RE.test(text) && AIRPORT_AVIATION_RE.test(text)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function isMedicalCollapseQuery(text: string): boolean {
|
|
352
|
+
return MEDICAL_COLLAPSE_RE.test(text) && !NON_MEDICAL_EMERGENCY_RE.test(text)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function routeNoFromUserText(text: string): string | undefined {
|
|
356
|
+
return TAGO_ROUTE_NO_RE.exec(text)?.[1]
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function placeNameFromUserText(text: string): string | undefined {
|
|
360
|
+
const match = TAGO_PLACE_RE.exec(text)
|
|
361
|
+
const place = match?.[1]?.trim()
|
|
362
|
+
if (!place) return undefined
|
|
363
|
+
return place.replace(/\s+/gu, ' ')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isTagoRoutePlaceQuery(text: string): boolean {
|
|
367
|
+
return TAGO_BUS_RE.test(text) && routeNoFromUserText(text) !== undefined
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function isTagoBusQuery(text: string): boolean {
|
|
371
|
+
return TAGO_BUS_RE.test(text)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isTagoBusOriginDestinationQuery(text: string): boolean {
|
|
375
|
+
if (!isTagoBusQuery(text) || routeNoFromUserText(text) !== undefined) return false
|
|
376
|
+
return /에서[\s\S]{1,50}(?:까지|으로|로|가는|가려고|가야|가려면|이동|방향|행)/u.test(
|
|
377
|
+
text,
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function hasSuccessfulStationSearch(messages: readonly unknown[]): boolean {
|
|
382
|
+
return toolResultTextsFor(messages, TAGO_STATION_TOOL_NAME).some(
|
|
383
|
+
text => text.includes('"ok":true') && text.includes('"nodeid"'),
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function hasSuccessfulRouteSearch(messages: readonly unknown[]): boolean {
|
|
388
|
+
return toolResultTextsFor(messages, TAGO_ROUTE_TOOL_NAME).some(
|
|
389
|
+
text => text.includes('"ok":true') && text.includes('"routeid"'),
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function hasSuccessfulRouteStationSearch(messages: readonly unknown[]): boolean {
|
|
394
|
+
return toolResultTextsFor(messages, TAGO_ROUTE_STATION_TOOL_NAME).some(
|
|
395
|
+
text => text.includes('"ok":true') && text.includes('"nodeid"'),
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function selectTagoBusToolChoice(
|
|
400
|
+
userText: string,
|
|
401
|
+
available: Set<string>,
|
|
402
|
+
usedToolNames: Set<string>,
|
|
403
|
+
messages: readonly unknown[],
|
|
404
|
+
): string | undefined {
|
|
405
|
+
if (!isTagoBusQuery(userText)) return undefined
|
|
406
|
+
if (routeNoFromUserText(userText) === undefined) {
|
|
407
|
+
if (
|
|
408
|
+
!usedToolNames.has(TAGO_STATION_TOOL_NAME) &&
|
|
409
|
+
isAvailableOrSyncedAdapter(available, TAGO_STATION_TOOL_NAME)
|
|
410
|
+
) {
|
|
411
|
+
return TAGO_STATION_TOOL_NAME
|
|
412
|
+
}
|
|
413
|
+
if (isTagoBusOriginDestinationQuery(userText)) {
|
|
414
|
+
return undefined
|
|
415
|
+
}
|
|
416
|
+
if (
|
|
417
|
+
hasSuccessfulStationSearch(messages) &&
|
|
418
|
+
!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME) &&
|
|
419
|
+
isAvailableOrSyncedAdapter(available, TAGO_ARRIVAL_TOOL_NAME)
|
|
420
|
+
) {
|
|
421
|
+
return TAGO_ARRIVAL_TOOL_NAME
|
|
422
|
+
}
|
|
423
|
+
return undefined
|
|
424
|
+
}
|
|
425
|
+
if (
|
|
426
|
+
!usedToolNames.has(TAGO_ROUTE_TOOL_NAME) &&
|
|
427
|
+
isAvailableOrSyncedAdapter(available, TAGO_ROUTE_TOOL_NAME)
|
|
428
|
+
) {
|
|
429
|
+
return TAGO_ROUTE_TOOL_NAME
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
hasSuccessfulRouteSearch(messages) &&
|
|
433
|
+
!usedToolNames.has(TAGO_ROUTE_STATION_TOOL_NAME) &&
|
|
434
|
+
isAvailableOrSyncedAdapter(available, TAGO_ROUTE_STATION_TOOL_NAME)
|
|
435
|
+
) {
|
|
436
|
+
return TAGO_ROUTE_STATION_TOOL_NAME
|
|
437
|
+
}
|
|
438
|
+
if (
|
|
439
|
+
hasSuccessfulRouteStationSearch(messages) &&
|
|
440
|
+
!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME) &&
|
|
441
|
+
isAvailableOrSyncedAdapter(available, TAGO_ARRIVAL_TOOL_NAME)
|
|
442
|
+
) {
|
|
443
|
+
return TAGO_ARRIVAL_TOOL_NAME
|
|
444
|
+
}
|
|
445
|
+
return undefined
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function buildTagoBusFollowupPromptIfNeeded({
|
|
449
|
+
messages,
|
|
450
|
+
availableToolNames,
|
|
451
|
+
}: {
|
|
452
|
+
messages: readonly unknown[]
|
|
453
|
+
availableToolNames: Iterable<string>
|
|
454
|
+
}): string | undefined {
|
|
455
|
+
const userText = latestUserText(messages)
|
|
456
|
+
if (!isTagoBusQuery(userText)) return undefined
|
|
457
|
+
const available = new Set(availableToolNames)
|
|
458
|
+
const usedToolNames = toolUseNames(messages)
|
|
459
|
+
const routeNo = routeNoFromUserText(userText)
|
|
460
|
+
const placeName = placeNameFromUserText(userText) ?? 'the citizen-named place'
|
|
461
|
+
if (
|
|
462
|
+
routeNo === undefined &&
|
|
463
|
+
isTagoBusOriginDestinationQuery(userText) &&
|
|
464
|
+
hasSuccessfulStationSearch(messages) &&
|
|
465
|
+
!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME)
|
|
466
|
+
) {
|
|
467
|
+
return (
|
|
468
|
+
'TAGO bus origin-destination limitation: the citizen asked for an origin-to-destination trip without a route number. ' +
|
|
469
|
+
'The available TAGO adapters expose stop search, route-number search, route passing stops, and stop-based current arrivals; ' +
|
|
470
|
+
'they do not prove an origin-destination route from stop arrivals alone. Do not call tago_bus_arrival_search for this OD request. ' +
|
|
471
|
+
'Write the final Korean answer now from the station evidence, explain that current TAGO tools need a route number or exact stop/route to check arrivals, ' +
|
|
472
|
+
'and do not claim that a bus route to the destination was found.'
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
if (
|
|
476
|
+
routeNo === undefined &&
|
|
477
|
+
hasSuccessfulStationSearch(messages) &&
|
|
478
|
+
!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME) &&
|
|
479
|
+
available.has(TAGO_ARRIVAL_TOOL_NAME)
|
|
480
|
+
) {
|
|
481
|
+
return (
|
|
482
|
+
'Required follow-up for this TAGO bus chain: the station search has returned nodeid. ' +
|
|
483
|
+
`Before any final answer, call ${TAGO_ARRIVAL_TOOL_NAME} with city_code:"21" ` +
|
|
484
|
+
'and the best matching node_id from the station result to get current arrivals. ' +
|
|
485
|
+
'Do not switch to route search unless the citizen named a route number.'
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
routeNo !== undefined &&
|
|
490
|
+
hasSuccessfulRouteSearch(messages) &&
|
|
491
|
+
!usedToolNames.has(TAGO_ROUTE_STATION_TOOL_NAME) &&
|
|
492
|
+
available.has(TAGO_ROUTE_STATION_TOOL_NAME)
|
|
493
|
+
) {
|
|
494
|
+
return (
|
|
495
|
+
'Required follow-up for this TAGO bus chain: the route search has returned route_id. ' +
|
|
496
|
+
`Before any final answer, call ${TAGO_ROUTE_STATION_TOOL_NAME} with city_code:"21", ` +
|
|
497
|
+
`that route_id, and node_nm:"${placeName}" to find the route's passing stop nodeid.`
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
if (
|
|
501
|
+
routeNo !== undefined &&
|
|
502
|
+
hasSuccessfulRouteStationSearch(messages) &&
|
|
503
|
+
!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME) &&
|
|
504
|
+
available.has(TAGO_ARRIVAL_TOOL_NAME)
|
|
505
|
+
) {
|
|
506
|
+
return (
|
|
507
|
+
'Required follow-up for this TAGO bus chain: route passing-stop evidence has returned nodeid. ' +
|
|
508
|
+
`Before any final answer, call ${TAGO_ARRIVAL_TOOL_NAME} with city_code:"21", ` +
|
|
509
|
+
`a matching node_id, and route_no:"${routeNo}" or route_id to get current arrivals. ` +
|
|
510
|
+
'If the first matching direction returns no arrivals, try the other matching nodeid before final prose.'
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
return undefined
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function buildTagoBusCompletionPromptIfNeeded({
|
|
517
|
+
messages,
|
|
518
|
+
}: {
|
|
519
|
+
messages: readonly unknown[]
|
|
520
|
+
}): string | undefined {
|
|
521
|
+
const userText = latestUserText(messages)
|
|
522
|
+
if (!isTagoBusQuery(userText)) return undefined
|
|
523
|
+
const usedToolNames = toolUseNames(messages)
|
|
524
|
+
if (routeNoFromUserText(userText) === undefined) {
|
|
525
|
+
return usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME)
|
|
526
|
+
? TAGO_BUS_COMPLETION_PROMPT
|
|
527
|
+
: undefined
|
|
528
|
+
}
|
|
529
|
+
if (
|
|
530
|
+
!usedToolNames.has(TAGO_ROUTE_STATION_TOOL_NAME) ||
|
|
531
|
+
!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME)
|
|
532
|
+
) {
|
|
533
|
+
return undefined
|
|
534
|
+
}
|
|
535
|
+
return TAGO_BUS_COMPLETION_PROMPT
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function buildTagoBusFinalAnswerRepairPromptIfNeeded({
|
|
539
|
+
messages,
|
|
540
|
+
}: {
|
|
541
|
+
messages: readonly unknown[]
|
|
542
|
+
}): string | undefined {
|
|
543
|
+
const userText = latestUserText(messages)
|
|
544
|
+
if (!isTagoBusQuery(userText)) return undefined
|
|
545
|
+
const usedToolNames = toolUseNames(messages)
|
|
546
|
+
if (!usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME)) return undefined
|
|
547
|
+
if (hasTagoBusRepairPrompt(messages)) return undefined
|
|
548
|
+
const assistantText = latestAssistantText(messages)
|
|
549
|
+
if (!TAGO_BUS_PENDING_FINAL_RE.test(assistantText)) return undefined
|
|
550
|
+
return TAGO_BUS_REPAIR_PROMPT
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function shouldWithholdTagoBusFinalAnswer({
|
|
554
|
+
messages,
|
|
555
|
+
candidate,
|
|
556
|
+
}: {
|
|
557
|
+
messages: readonly unknown[]
|
|
558
|
+
candidate: unknown
|
|
559
|
+
}): boolean {
|
|
560
|
+
if (hasTagoBusRepairPrompt(messages)) return false
|
|
561
|
+
return (
|
|
562
|
+
buildTagoBusFinalAnswerRepairPromptIfNeeded({
|
|
563
|
+
messages: [...messages, candidate],
|
|
564
|
+
}) !== undefined
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function buildAirKoreaCompletionPromptIfNeeded({
|
|
569
|
+
messages,
|
|
570
|
+
}: {
|
|
571
|
+
messages: readonly unknown[]
|
|
572
|
+
}): string | undefined {
|
|
573
|
+
const userText = latestUserText(messages)
|
|
574
|
+
if (!AIRKOREA_RE.test(userText)) return undefined
|
|
575
|
+
const usedToolNames = toolUseNames(messages)
|
|
576
|
+
if (!usedToolNames.has(AIRKOREA_TOOL_NAME)) return undefined
|
|
577
|
+
return AIRKOREA_COMPLETION_PROMPT + (airKoreaExactRowsSummary(messages) ?? '')
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function buildAirKoreaFinalAnswerRepairPromptIfNeeded({
|
|
581
|
+
messages,
|
|
582
|
+
}: {
|
|
583
|
+
messages: readonly unknown[]
|
|
584
|
+
}): string | undefined {
|
|
585
|
+
const userText = latestUserText(messages)
|
|
586
|
+
if (!AIRKOREA_RE.test(userText)) return undefined
|
|
587
|
+
const usedToolNames = toolUseNames(messages)
|
|
588
|
+
if (!usedToolNames.has(AIRKOREA_TOOL_NAME)) return undefined
|
|
589
|
+
if (hasAirKoreaRepairPrompt(messages)) return undefined
|
|
590
|
+
const assistantText = latestAssistantText(messages)
|
|
591
|
+
if (!AIRKOREA_UNSUPPORTED_LOCATION_CLAIM_RE.test(assistantText)) return undefined
|
|
592
|
+
return AIRKOREA_REPAIR_PROMPT + (airKoreaExactRowsSummary(messages) ?? '')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function shouldWithholdAirKoreaFinalAnswer({
|
|
596
|
+
messages,
|
|
597
|
+
candidate,
|
|
598
|
+
}: {
|
|
599
|
+
messages: readonly unknown[]
|
|
600
|
+
candidate: unknown
|
|
601
|
+
}): boolean {
|
|
602
|
+
if (hasAirKoreaRepairPrompt(messages)) return false
|
|
603
|
+
return (
|
|
604
|
+
buildAirKoreaFinalAnswerRepairPromptIfNeeded({
|
|
605
|
+
messages: [...messages, candidate],
|
|
606
|
+
}) !== undefined
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function buildGenericPendingFinalAnswerRepairPromptIfNeeded({
|
|
611
|
+
messages,
|
|
612
|
+
}: {
|
|
613
|
+
messages: readonly unknown[]
|
|
614
|
+
}): string | undefined {
|
|
615
|
+
if (!hasToolResult(messages)) return undefined
|
|
616
|
+
if (hasGenericPendingFinalRepairPrompt(messages)) return undefined
|
|
617
|
+
const assistantText = latestAssistantText(messages)
|
|
618
|
+
if (!GENERIC_PENDING_FINAL_RE.test(assistantText)) return undefined
|
|
619
|
+
return GENERIC_PENDING_FINAL_REPAIR_PROMPT
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function shouldWithholdGenericPendingFinalAnswer({
|
|
623
|
+
messages,
|
|
624
|
+
candidate,
|
|
625
|
+
}: {
|
|
626
|
+
messages: readonly unknown[]
|
|
627
|
+
candidate: unknown
|
|
628
|
+
}): boolean {
|
|
629
|
+
if (hasGenericPendingFinalRepairPrompt(messages)) return false
|
|
630
|
+
return (
|
|
631
|
+
buildGenericPendingFinalAnswerRepairPromptIfNeeded({
|
|
632
|
+
messages: [...messages, candidate],
|
|
633
|
+
}) !== undefined
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function needsEmergencyRoomSearch(text: string): boolean {
|
|
638
|
+
return MEDICAL_COLLAPSE_OR_ER_RE.test(text) && !NON_MEDICAL_EMERGENCY_RE.test(text)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function selectKmaAviationTool(
|
|
642
|
+
userText: string,
|
|
643
|
+
available: Set<string>,
|
|
644
|
+
usedToolNames: Set<string>,
|
|
645
|
+
preferUnused: boolean,
|
|
646
|
+
): string | undefined {
|
|
647
|
+
const preferAmos = AMOS_PREFERENCE_RE.test(userText) && GIMPO_RE.test(userText)
|
|
648
|
+
const candidates = preferAmos
|
|
649
|
+
? ['kma_apihub_url_air_amos_minute', 'kma_apihub_url_air_metar_decoded']
|
|
650
|
+
: ['kma_apihub_url_air_metar_decoded', 'kma_apihub_url_air_amos_minute']
|
|
651
|
+
if (preferUnused) {
|
|
652
|
+
const unused = candidates.filter(candidate => !usedToolNames.has(candidate))
|
|
653
|
+
const unusedChoice = chooseAvailableOrSyncedAdapter(available, unused)
|
|
654
|
+
if (unusedChoice) return unusedChoice
|
|
655
|
+
}
|
|
656
|
+
return chooseAvailableOrSyncedAdapter(available, candidates)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function selectProtectedCheckTool(
|
|
660
|
+
userText: string,
|
|
661
|
+
available: Set<string>,
|
|
662
|
+
): string | undefined {
|
|
663
|
+
const candidates = [
|
|
664
|
+
MOBILE_ID_RE.test(userText) ? 'mock_verify_mobile_id' : undefined,
|
|
665
|
+
SIMPLE_AUTH_RE.test(userText) ? 'mock_verify_module_simple_auth' : undefined,
|
|
666
|
+
SIMPLE_AUTH_RE.test(userText) ? 'mock_verify_ganpyeon_injeung' : undefined,
|
|
667
|
+
MYDATA_RE.test(userText) ? 'mock_verify_mydata' : undefined,
|
|
668
|
+
...PROTECTED_CHECK_TOOLS,
|
|
669
|
+
].filter((toolName): toolName is string => typeof toolName === 'string')
|
|
670
|
+
return chooseAvailableOrSyncedAdapter(available, candidates)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function hasAnyToolUse(usedToolNames: Set<string>, candidates: readonly string[]): boolean {
|
|
674
|
+
return candidates.some(candidate => usedToolNames.has(candidate))
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function requiredKmaAviationTools(
|
|
678
|
+
userText: string,
|
|
679
|
+
available: Set<string>,
|
|
680
|
+
): string[] {
|
|
681
|
+
const candidates = GIMPO_RE.test(userText)
|
|
682
|
+
? ['kma_apihub_url_air_metar_decoded', 'kma_apihub_url_air_amos_minute']
|
|
683
|
+
: ['kma_apihub_url_air_metar_decoded']
|
|
684
|
+
return candidates.filter(candidate => isAvailableOrSyncedAdapter(available, candidate))
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function shouldSuppressUmmayaToolCallsForAnswerSynthesis({
|
|
688
|
+
messages,
|
|
689
|
+
tools,
|
|
690
|
+
}: {
|
|
691
|
+
messages: readonly Message[]
|
|
692
|
+
tools: Tools
|
|
693
|
+
}): boolean {
|
|
694
|
+
const available = availableToolNamesFromTools(tools)
|
|
695
|
+
const userText = latestUserText(messages)
|
|
696
|
+
const usedToolNames = toolUseNames(messages)
|
|
697
|
+
if (
|
|
698
|
+
isMedicalCollapseQuery(userText) &&
|
|
699
|
+
usedToolNames.has(NMC_AED_TOOL_NAME)
|
|
700
|
+
) {
|
|
701
|
+
return true
|
|
702
|
+
}
|
|
703
|
+
if (
|
|
704
|
+
PROTECTED_QUERY_RE.test(userText) &&
|
|
705
|
+
hasAnyToolUse(usedToolNames, PROTECTED_CHECK_TOOLS)
|
|
706
|
+
) {
|
|
707
|
+
return true
|
|
708
|
+
}
|
|
709
|
+
if (
|
|
710
|
+
isKmaAnalysisMapText(userText) &&
|
|
711
|
+
usedToolNames.has(KMA_ANALYSIS_CHART_TOOL_NAME)
|
|
712
|
+
) {
|
|
713
|
+
return true
|
|
714
|
+
}
|
|
715
|
+
if (AIRKOREA_RE.test(userText) && usedToolNames.has(AIRKOREA_TOOL_NAME)) {
|
|
716
|
+
return true
|
|
717
|
+
}
|
|
718
|
+
if (
|
|
719
|
+
isTagoBusQuery(userText) &&
|
|
720
|
+
usedToolNames.has(TAGO_ARRIVAL_TOOL_NAME)
|
|
721
|
+
) {
|
|
722
|
+
return true
|
|
723
|
+
}
|
|
724
|
+
if (!isAirportAviationQuery(userText)) return false
|
|
725
|
+
|
|
726
|
+
const requiredTools = requiredKmaAviationTools(userText, available)
|
|
727
|
+
if (requiredTools.length === 0) return false
|
|
728
|
+
|
|
729
|
+
return requiredTools.every(toolName => usedToolNames.has(toolName))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function publicDataMismatchTargetTool(
|
|
733
|
+
latestToolResult: string,
|
|
734
|
+
): string | undefined {
|
|
735
|
+
const candidate = PUBLIC_DATA_MISMATCH_CALL_RE.exec(latestToolResult)?.[1]
|
|
736
|
+
if (!candidate) return undefined
|
|
737
|
+
return candidate
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function selectUmmayaToolChoiceOverride({
|
|
741
|
+
messages,
|
|
742
|
+
tools,
|
|
743
|
+
}: {
|
|
744
|
+
messages: readonly Message[]
|
|
745
|
+
tools: Tools
|
|
746
|
+
}): BetaToolChoiceTool | undefined {
|
|
747
|
+
const available = availableToolNamesFromTools(tools)
|
|
748
|
+
const userText = latestUserText(messages)
|
|
749
|
+
const latestToolResult = latestToolResultText(messages)
|
|
750
|
+
const usedToolNames = toolUseNames(messages)
|
|
751
|
+
const airportAviationQuery = isAirportAviationQuery(userText)
|
|
752
|
+
const usedAviationTool = hasAnyToolUse(usedToolNames, KMA_AIR_TOOLS)
|
|
753
|
+
const publicDataTargetTool = publicDataMismatchTargetTool(latestToolResult)
|
|
754
|
+
if (
|
|
755
|
+
shouldSuppressUmmayaToolCallsForAnswerSynthesis({
|
|
756
|
+
messages,
|
|
757
|
+
tools,
|
|
758
|
+
})
|
|
759
|
+
) {
|
|
760
|
+
return undefined
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (
|
|
764
|
+
publicDataTargetTool &&
|
|
765
|
+
!usedToolNames.has(publicDataTargetTool) &&
|
|
766
|
+
isAvailableOrSyncedAdapter(available, publicDataTargetTool)
|
|
767
|
+
) {
|
|
768
|
+
return { type: 'tool', name: publicDataTargetTool }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (
|
|
772
|
+
AIRKOREA_RE.test(userText) &&
|
|
773
|
+
!usedToolNames.has(AIRKOREA_TOOL_NAME) &&
|
|
774
|
+
isAvailableOrSyncedAdapter(available, AIRKOREA_TOOL_NAME)
|
|
775
|
+
) {
|
|
776
|
+
return { type: 'tool', name: AIRKOREA_TOOL_NAME }
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (
|
|
780
|
+
PPS_BID_RE.test(userText) &&
|
|
781
|
+
!usedToolNames.has(PPS_BID_TOOL_NAME) &&
|
|
782
|
+
isAvailableOrSyncedAdapter(available, PPS_BID_TOOL_NAME)
|
|
783
|
+
) {
|
|
784
|
+
return { type: 'tool', name: PPS_BID_TOOL_NAME }
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (
|
|
788
|
+
needsEmergencyRoomSearch(userText) &&
|
|
789
|
+
available.has(KAKAO_COORD_TO_REGION_TOOL_NAME) &&
|
|
790
|
+
!usedToolNames.has(KAKAO_COORD_TO_REGION_TOOL_NAME) &&
|
|
791
|
+
!hasSuccessfulRegionLocateResult(messages) &&
|
|
792
|
+
hasSuccessfulPoiLocateResult(messages)
|
|
793
|
+
) {
|
|
794
|
+
return { type: 'tool', name: KAKAO_COORD_TO_REGION_TOOL_NAME }
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (
|
|
798
|
+
needsEmergencyRoomSearch(userText) &&
|
|
799
|
+
available.has(NMC_EMERGENCY_TOOL_NAME) &&
|
|
800
|
+
!usedToolNames.has(NMC_EMERGENCY_TOOL_NAME) &&
|
|
801
|
+
hasSuccessfulRegionLocateResult(messages)
|
|
802
|
+
) {
|
|
803
|
+
return { type: 'tool', name: NMC_EMERGENCY_TOOL_NAME }
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const needsGimpoAviationFollowup =
|
|
807
|
+
airportAviationQuery &&
|
|
808
|
+
GIMPO_RE.test(userText) &&
|
|
809
|
+
usedToolNames.has('kma_apihub_url_air_metar_decoded') &&
|
|
810
|
+
!usedToolNames.has('kma_apihub_url_air_amos_minute') &&
|
|
811
|
+
isAvailableOrSyncedAdapter(available, 'kma_apihub_url_air_amos_minute')
|
|
812
|
+
|
|
813
|
+
const shouldForceAviation =
|
|
814
|
+
latestToolResult.includes('KMA aviation tool-choice mismatch') ||
|
|
815
|
+
(airportAviationQuery && !usedAviationTool) ||
|
|
816
|
+
needsGimpoAviationFollowup
|
|
817
|
+
if (shouldForceAviation) {
|
|
818
|
+
const name = selectKmaAviationTool(
|
|
819
|
+
userText,
|
|
820
|
+
available,
|
|
821
|
+
usedToolNames,
|
|
822
|
+
usedAviationTool,
|
|
823
|
+
)
|
|
824
|
+
if (name) return { type: 'tool', name }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const shouldForceKmaAnalysis =
|
|
828
|
+
latestToolResult.includes('KMA analysis tool-choice mismatch') ||
|
|
829
|
+
(isKmaAnalysisMapText(userText) &&
|
|
830
|
+
!usedToolNames.has(KMA_ANALYSIS_CHART_TOOL_NAME))
|
|
831
|
+
if (
|
|
832
|
+
shouldForceKmaAnalysis &&
|
|
833
|
+
isAvailableOrSyncedAdapter(available, KMA_ANALYSIS_CHART_TOOL_NAME)
|
|
834
|
+
) {
|
|
835
|
+
return { type: 'tool', name: KMA_ANALYSIS_CHART_TOOL_NAME }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const shouldForceAed = buildNmcAedFollowupPromptIfNeeded({
|
|
839
|
+
messages,
|
|
840
|
+
availableToolNames: available,
|
|
841
|
+
})
|
|
842
|
+
if (shouldForceAed && available.has(NMC_AED_TOOL_NAME)) {
|
|
843
|
+
return { type: 'tool', name: NMC_AED_TOOL_NAME }
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const tagoToolName = selectTagoBusToolChoice(
|
|
847
|
+
userText,
|
|
848
|
+
available,
|
|
849
|
+
usedToolNames,
|
|
850
|
+
messages,
|
|
851
|
+
)
|
|
852
|
+
if (tagoToolName) {
|
|
853
|
+
return { type: 'tool', name: tagoToolName }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const shouldForceProtected =
|
|
857
|
+
latestToolResult.includes('Protected-domain tool-choice mismatch') ||
|
|
858
|
+
(PROTECTED_QUERY_RE.test(userText) &&
|
|
859
|
+
!hasAnyToolUse(usedToolNames, PROTECTED_CHECK_TOOLS))
|
|
860
|
+
if (shouldForceProtected) {
|
|
861
|
+
const name = selectProtectedCheckTool(`${userText}\n${latestToolResult}`, available)
|
|
862
|
+
if (name) return { type: 'tool', name }
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return undefined
|
|
866
|
+
}
|