ummaya 0.2.1 → 0.2.3

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.
@@ -23,6 +23,8 @@ type AdapterPrimitive = 'find' | 'locate' | 'send' | 'check'
23
23
 
24
24
  type InputSchema = z.ZodType<{ [key: string]: unknown }>
25
25
 
26
+ const ROOT_PRIMITIVE_TOOL_NAMES = new Set(['locate', 'find', 'check', 'send'])
27
+
26
28
  const fallbackInputSchema = z.object({}).passthrough() as InputSchema
27
29
 
28
30
  type JsonObject = Record<string, unknown>
@@ -214,6 +216,10 @@ export function isAdapterToolName(name: string): boolean {
214
216
  return resolveAdapter(name) !== undefined
215
217
  }
216
218
 
219
+ export function isRootPrimitiveToolName(name: string): boolean {
220
+ return ROOT_PRIMITIVE_TOOL_NAMES.has(name)
221
+ }
222
+
217
223
  export function getAdapterToolByName(name: string): Tool | undefined {
218
224
  const entry = resolveAdapter(name)
219
225
  return entry ? buildAdapterTool(entry) : undefined
@@ -223,6 +229,258 @@ export function getAdapterTools(): Tools {
223
229
  return listAdapters().map(buildAdapterTool)
224
230
  }
225
231
 
232
+ function searchTokens(text: string): string[] {
233
+ return text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? []
234
+ }
235
+
236
+ function expandedQueryTokens(query: string): Set<string> {
237
+ const tokens = new Set(searchTokens(query))
238
+ const compact = query.toLowerCase()
239
+ if (/[날씨기상비강수기온습도풍속예보관측실황]/u.test(compact)) {
240
+ for (const token of [
241
+ '날씨',
242
+ '기상',
243
+ '현재',
244
+ '관측',
245
+ '실황',
246
+ 'weather',
247
+ 'current',
248
+ 'observation',
249
+ 'forecast',
250
+ 'temperature',
251
+ 'precipitation',
252
+ 'humidity',
253
+ 'wind',
254
+ ]) {
255
+ tokens.add(token)
256
+ }
257
+ }
258
+ if (/(응급|응급실|er|emergency)/u.test(compact)) {
259
+ for (const token of [
260
+ '응급',
261
+ '응급실',
262
+ '응급의료',
263
+ '응급의료센터',
264
+ '실시간',
265
+ '병상',
266
+ '야간',
267
+ 'emergency',
268
+ 'room',
269
+ 'er',
270
+ 'nmc',
271
+ ]) {
272
+ tokens.add(token)
273
+ }
274
+ }
275
+ if (/(병원|의료|진료|약국|hospital|clinic|medical)/u.test(compact)) {
276
+ for (const token of [
277
+ '병원',
278
+ '의료기관',
279
+ '진료',
280
+ '진료과목',
281
+ '야간',
282
+ '약국',
283
+ 'hospital',
284
+ 'clinic',
285
+ 'medical',
286
+ 'nearby',
287
+ ]) {
288
+ tokens.add(token)
289
+ }
290
+ }
291
+ if (/(aed|자동심장|심장충격|제세동)/u.test(compact)) {
292
+ for (const token of ['aed', '자동심장충격기', '자동제세동기', '심장충격기', '위치']) {
293
+ tokens.add(token)
294
+ }
295
+ }
296
+ if (/(미세먼지|초미세|대기질|공기질|airquality|air quality)/u.test(compact)) {
297
+ for (const token of ['미세먼지', '대기질', '대기오염', 'airkorea', 'air', 'quality']) {
298
+ tokens.add(token)
299
+ }
300
+ }
301
+ if (/(법률|변호사|무료상담|상담)/u.test(compact)) {
302
+ for (const token of ['법률', '변호사', '마을변호사', '상담', 'legal', 'lawyer']) {
303
+ tokens.add(token)
304
+ }
305
+ }
306
+ if (/(장례|화장|봉안|장사|funeral)/u.test(compact)) {
307
+ for (const token of ['장례', '장례식장', '시설사용료', 'funeral', 'fee']) {
308
+ tokens.add(token)
309
+ }
310
+ }
311
+ if (/(취업|채용|공고|공무원|job|recruit)/u.test(compact)) {
312
+ for (const token of ['취업', '채용', '공고', '공무원', 'public', 'job']) {
313
+ tokens.add(token)
314
+ }
315
+ }
316
+ if (/(대학|등록금|유학생|tuition|university)/u.test(compact)) {
317
+ for (const token of ['대학', '등록금', '유학생', '대학알리미', 'tuition', 'university']) {
318
+ tokens.add(token)
319
+ }
320
+ }
321
+ if (/(전력|전기|한전|계약종별|power|kepco)/u.test(compact)) {
322
+ for (const token of ['전력', '전기사용량', '계약종별', '한전', 'kepco', 'power', 'usage']) {
323
+ tokens.add(token)
324
+ }
325
+ }
326
+ if (/(특보|예비특보|경보|주의보|태풍|warning|alert)/u.test(compact)) {
327
+ for (const token of ['특보', '예비특보', '경보', '주의보', '기상청', 'weather', 'alert']) {
328
+ tokens.add(token)
329
+ }
330
+ }
331
+ if (/(교통사고|사고\s*위험|사고다발|위험\s*(구간|도로|지점)|어린이보호구역|보호구역|도로\s*구간|accident|hazard|hotspot)/u.test(compact)) {
332
+ for (const token of [
333
+ '교통사고',
334
+ '사고',
335
+ '위험',
336
+ '위험지점',
337
+ '사고다발',
338
+ '사고다발구역',
339
+ '어린이보호구역',
340
+ '행정동코드',
341
+ 'koroad',
342
+ 'accident',
343
+ 'hazard',
344
+ 'hotspot',
345
+ ]) {
346
+ tokens.add(token)
347
+ }
348
+ }
349
+ if (/(주소|위치|좌표|행정|[가-힣]+(시|군|구|동|읍|면|로|길))/u.test(compact)) {
350
+ for (const token of [
351
+ 'locate',
352
+ '위치',
353
+ '주소',
354
+ '좌표',
355
+ '행정동',
356
+ '법정동',
357
+ 'geocode',
358
+ 'address',
359
+ 'kakao',
360
+ ]) {
361
+ tokens.add(token)
362
+ }
363
+ }
364
+ if (/(근처|주변|인근|가까운|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크|nearby|around)/u.test(compact)) {
365
+ for (const token of [
366
+ '장소',
367
+ '키워드',
368
+ 'poi',
369
+ '랜드마크',
370
+ '역',
371
+ 'keyword',
372
+ 'station',
373
+ 'place',
374
+ ]) {
375
+ tokens.add(token)
376
+ }
377
+ }
378
+ return tokens
379
+ }
380
+
381
+ type ScoredAdapterEntry = {
382
+ entry: AdapterManifestEntry
383
+ score: number
384
+ }
385
+
386
+ function queryExplicitlyMentionsCoordinates(query: string): boolean {
387
+ return /좌표|위도|경도|\blat\b|\blon\b|\blongitude\b|\blatitude\b|wgs84|coord|coord2region|reverse geocode|q0|q1/i.test(query)
388
+ }
389
+
390
+ function isReverseGeocodeAdapter(toolId: string): boolean {
391
+ return toolId === 'kakao_coord_to_region' || toolId === 'sgis_adm_cd_lookup'
392
+ }
393
+
394
+ function queryTargetsKoroadHazardDataset(query: string): boolean {
395
+ return /(사고\s*위험|위험\s*(구간|도로|지점)|도로\s*구간|어린이보호구역|보호구역|스쿨존|행정동코드|adm_cd|hazard|hotspot)/iu.test(query)
396
+ }
397
+
398
+ function scoreAdapterEntry(
399
+ entry: AdapterManifestEntry,
400
+ queryTokens: Set<string>,
401
+ query: string,
402
+ ): number {
403
+ const searchHint = entry.search_hint.toLowerCase()
404
+ const description = (entry.llm_description ?? '').toLowerCase()
405
+ const haystack = [
406
+ entry.tool_id,
407
+ entry.name,
408
+ entry.primitive,
409
+ searchHint,
410
+ description,
411
+ ].join(' ').toLowerCase()
412
+ const hintTokens = new Set(searchTokens(searchHint))
413
+ let score = 0
414
+ for (const token of queryTokens) {
415
+ if (!token) continue
416
+ if (entry.tool_id.toLowerCase().includes(token)) score += 12
417
+ if (hintTokens.has(token)) score += 8
418
+ else if (searchHint.includes(token)) score += 4
419
+ if (description.includes(token)) score += 2
420
+ if (haystack.includes(token)) score += 1
421
+ }
422
+ if (
423
+ isReverseGeocodeAdapter(entry.tool_id) &&
424
+ !queryExplicitlyMentionsCoordinates(query)
425
+ ) {
426
+ score = Math.max(0, score - 24)
427
+ }
428
+ if (queryTargetsKoroadHazardDataset(query)) {
429
+ if (entry.tool_id === 'koroad_accident_hazard_search') score += 32
430
+ if (entry.tool_id === 'koroad_accident_search') score = 0
431
+ }
432
+ return score
433
+ }
434
+
435
+ export function selectTopKAdapterToolNamesForQuery(
436
+ query: string,
437
+ maxResults = 5,
438
+ ): string[] {
439
+ const normalizedQuery = query.trim()
440
+ if (!normalizedQuery || maxResults <= 0) return []
441
+ const queryTokens = expandedQueryTokens(normalizedQuery)
442
+ const ranked = listAdapters()
443
+ .filter(entry => !ROOT_PRIMITIVE_TOOL_NAMES.has(entry.tool_id))
444
+ .map(entry => ({
445
+ entry,
446
+ score: scoreAdapterEntry(entry, queryTokens, normalizedQuery),
447
+ }))
448
+ .filter(candidate => candidate.score > 0)
449
+ .sort((a, b) => {
450
+ if (b.score !== a.score) return b.score - a.score
451
+ return a.entry.tool_id.localeCompare(b.entry.tool_id)
452
+ })
453
+
454
+ return pickDiverseAdapterToolNames(ranked, maxResults)
455
+ }
456
+
457
+ function pickDiverseAdapterToolNames(
458
+ ranked: ScoredAdapterEntry[],
459
+ maxResults: number,
460
+ ): string[] {
461
+ const selected: string[] = []
462
+ const seen = new Set<string>()
463
+ const add = (candidate: ScoredAdapterEntry): void => {
464
+ if (selected.length >= maxResults || seen.has(candidate.entry.tool_id)) return
465
+ selected.push(candidate.entry.tool_id)
466
+ seen.add(candidate.entry.tool_id)
467
+ }
468
+
469
+ const locateCandidates = ranked.filter(candidate => candidate.entry.primitive === 'locate')
470
+ const actionCandidates = ranked.filter(candidate => candidate.entry.primitive !== 'locate')
471
+
472
+ if (actionCandidates.length > 0) {
473
+ const locateBudget = Math.min(2, Math.max(1, maxResults - 1))
474
+ for (const candidate of locateCandidates.slice(0, locateBudget)) add(candidate)
475
+ for (const candidate of actionCandidates) add(candidate)
476
+ } else {
477
+ for (const candidate of ranked) add(candidate)
478
+ }
479
+
480
+ for (const candidate of ranked) add(candidate)
481
+ return selected
482
+ }
483
+
226
484
  function buildAdapterTool(entry: AdapterManifestEntry): Tool {
227
485
  const primitive = primitiveFor(entry)
228
486
  const primitiveTool = primitiveToolFor(primitive)
@@ -231,10 +489,10 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
231
489
 
232
490
  return buildTool({
233
491
  name: entry.tool_id,
234
- // K-EXAONE runs through FriendliAI's OpenAI-compatible function calling,
235
- // not Anthropic tool_reference. Keep CC's Tool object shape, but make the
236
- // concrete public-service adapter schemas visible on the first turn.
237
- alwaysLoad: true,
492
+ // Keep concrete public-service adapters in CC's Tool object shape, but do
493
+ // not inline every synced adapter schema into the first LLM request. The
494
+ // ToolSearch path loads the few adapters relevant to the citizen request.
495
+ shouldDefer: true,
238
496
  searchHint: [entry.search_hint, entry.name, entry.tool_id, primitive]
239
497
  .filter(Boolean)
240
498
  .join(' '),
@@ -1,44 +1,33 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // UMMAYA-original — Epic #1634 P3 · FindPrimitive prompt strings.
3
- // Spec 2521 (2026-05-01) fetch-only surface; BM25 adapter discovery is a
4
- // backend-internal mechanism (auto-injected into the system prompt's
5
- // <available_adapters> dynamic suffix), not an LLM-callable mode. Older
6
- // "search/fetch two-mode" copy was the source of phantom tool-UI noise the
7
- // user surfaced via Layer 5 frame capture (specs/2521 frames/raw.cast).
3
+ // 2026 migration note: root primitives are lightweight category descriptors
4
+ // and legacy transcript compatibility wrappers. Concrete adapter functions are
5
+ // loaded through CC ToolSearch/deferred schema expansion or backend top-K
6
+ // retrieval, then called directly with adapter schema arguments.
8
7
  // Contract: specs/1634-tool-system-wiring/contracts/primitive-envelope.md § 2
9
8
 
10
9
  export const FIND_TOOL_NAME = 'find'
11
10
 
12
11
  /** Citizen-facing English description shown to the LLM (<= 240 chars). */
13
12
  export const DESCRIPTION =
14
- 'Invoke one concrete Korean public-service adapter. Use the function named find, but set tool_id to a listed adapter id from <available_adapters>, never to find/locate/check/send.'
13
+ 'Discover Korean public-service lookup adapters. Prefer concrete adapter functions loaded by ToolSearch or backend retrieval; find is a legacy wrapper only.'
15
14
 
16
15
  /** Extended prompt included in the system-prompt tool-use section. */
17
- export const FIND_TOOL_PROMPT = `Invoke Korean public-service adapters registered in the UMMAYA tool registry.
16
+ export const FIND_TOOL_PROMPT = `Discover Korean public-service lookup adapters registered in the UMMAYA tool registry.
18
17
 
19
- Single mode (Spec 2521 fetch-only):
18
+ Preferred path:
19
+ - Call concrete adapter functions directly after their schemas are loaded.
20
+ - Example: kma_current_observation({ base_date: "YYYYMMDD", base_time: "HH00", nx: 97, ny: 74 })
21
+ - Adapter schemas are progressively disclosed by ToolSearch or by backend top-K retrieval for the current citizen request.
22
+ - Only top candidates should be loaded; do not expect every adapter schema in the prompt.
20
23
 
21
- Input: { tool_id: string, params: object }
22
- Output: { tool_id: string, result: object }
23
-
24
- Adapter discovery
25
- ─────────────────
26
- Adapter discovery is a BACKEND-INTERNAL function — NOT a callable mode.
27
- For every citizen turn the backend runs BM25 against the registry and
28
- injects the top-K candidates into the system prompt's
29
- <available_adapters> dynamic suffix. The LLM picks a tool_id from that
30
- block and calls find directly.
24
+ Legacy root wrapper:
25
+ - If a concrete adapter function is not loaded and only the root primitive is available, find accepts { tool_id, params } for old transcripts and compatibility paths.
26
+ - tool_id must be a concrete adapter id from <available_adapters>, never "find", "locate", "check", or "send".
27
+ - Invalid: find({ tool_id: "find", params: {...} })
28
+ - Compatibility-only: find({ tool_id: "kma_current_observation", params: { base_date: "YYYYMMDD", base_time: "HH00", nx: 97, ny: 74 } })
31
29
 
32
30
  Rules:
33
- - The function name is find. The tool_id argument is NOT the function name.
34
- - tool_id must be a concrete adapter id listed in <available_adapters>, for example "kma_current_observation" or "kma_forecast_fetch".
35
- - Never set tool_id to a root primitive name: "find", "locate", "check", or "send".
36
- - Invalid: find({ tool_id: "find", params: {...} })
37
- - Valid: find({ tool_id: "kma_current_observation", params: {...} })
38
- - Pick tool_id only from <available_adapters>. Never guess an id.
39
- - Do NOT call find with mode='search' / query — those payloads are
40
- rejected with LookupErrorReason.invalid_params (Spec 2521).
41
- - Do NOT call the same tool_id twice in a single turn — answer with the
42
- result you already have, or pick a different tool_id from the list.
43
- - params shape mirrors the adapter's Pydantic input_schema (see the
44
- <available_adapters> hint for required keys).`
31
+ - Do not call find with mode='search' or query; discovery is handled outside the primitive call.
32
+ - Do not call the same adapter twice in a single turn unless a validation error requires corrected arguments.
33
+ - Use the concrete adapter schema fields exactly; never invent required keys.`
@@ -5,21 +5,23 @@ export const LOCATE_TOOL_NAME = 'locate'
5
5
 
6
6
  /** Citizen-facing English description shown to the LLM. */
7
7
  export const DESCRIPTION =
8
- 'Invoke one concrete Korean location adapter. Use the function named locate, but set tool_id to a listed locate adapter id from <available_adapters>, never to locate/find/check/send.'
8
+ 'Discover Korean location adapters. Prefer concrete location adapter functions loaded by ToolSearch or backend retrieval; locate is a legacy wrapper only.'
9
9
 
10
10
  /** Extended prompt included in the system-prompt tool-use section. */
11
- export const LOCATE_TOOL_PROMPT = `Resolve a Korean location phrase into structured location identifiers.
11
+ export const LOCATE_TOOL_PROMPT = `Resolve Korean location phrases with concrete location adapters.
12
12
 
13
- Input:
14
- { tool_id: string, params: object }
13
+ Preferred path:
14
+ - Call concrete adapter functions directly after their schemas are loaded.
15
+ - Examples: kakao_keyword_search({ query: "부산 사하구 다대1동" }) or kakao_coord_to_region({ lat: 35.115446, lon: 128.967669 })
16
+ - Adapter schemas are progressively disclosed by ToolSearch or by backend top-K retrieval for the current citizen request.
15
17
 
16
- Rules:
17
- - The function name is locate. The tool_id argument is NOT the function name.
18
- - tool_id must be a concrete locate adapter id listed in <available_adapters>, for example "kakao_address_search" or "kakao_coord_to_region".
19
- - Never set tool_id to a root primitive name: "locate", "find", "check", or "send".
18
+ Legacy root wrapper:
19
+ - If a concrete adapter function is not loaded and only the root primitive is available, locate accepts { tool_id, params } for old transcripts and compatibility paths.
20
+ - tool_id must be a concrete locate adapter id from <available_adapters>, never "locate", "find", "check", or "send".
20
21
  - Invalid: locate({ tool_id: "locate", params: {...} })
21
- - Valid: locate({ tool_id: "kakao_address_search", params: { query: "부산 사하구 다대1동" } })
22
- - Pick tool_id only from <available_adapters> entries whose primitive is locate.
22
+ - Compatibility-only: locate({ tool_id: "kakao_address_search", params: { query: "부산 사하구 다대1동" } })
23
+
24
+ Rules:
23
25
  - Use kakao_keyword_search for named places, campuses, stations, landmarks, hospitals, and POIs.
24
26
  - Coordinate-producing locate results may include KMA nx/ny; pass those exact values to KMA weather adapters that require nx and ny.
25
27
  - Use kakao_address_search or juso_adm_cd_lookup for structured road/jibun addresses and district text.
@@ -6,14 +6,19 @@ export const SEND_TOOL_NAME = 'send'
6
6
 
7
7
  /** One-line English description (<= 240 chars). */
8
8
  export const DESCRIPTION =
9
- 'Send a citizen action such as an application, report, or submission to a public-service adapter. This can have side effects; choose an adapter from <available_adapters>.'
9
+ 'Discover side-effecting public-service send adapters. Prefer concrete adapter functions loaded by retrieval; send is a permission-gated legacy wrapper.'
10
10
 
11
11
  /** Extended prompt included in the system-prompt tool-use section. */
12
- export const SEND_TOOL_PROMPT = `Send a side-effecting citizen action to a registered UMMAYA adapter.
12
+ export const SEND_TOOL_PROMPT = `Send a side-effecting citizen action through a concrete UMMAYA adapter.
13
13
 
14
- Input: { tool_id: string, params: object }
15
- - tool_id: the adapter identifier from <available_adapters>
16
- - params: adapter-defined Pydantic-validated parameter body
14
+ Preferred path:
15
+ - Call concrete adapter functions directly after their schemas are loaded.
16
+ - Adapter schemas are progressively disclosed by ToolSearch or backend top-K retrieval for the current citizen request.
17
+ - Use the adapter's exact schema fields and cite the resulting receipt in the citizen-facing answer.
18
+
19
+ Legacy root wrapper:
20
+ - If a concrete adapter function is not loaded and only the root primitive is available, send accepts { tool_id, params } for old transcripts and compatibility paths.
21
+ - tool_id must be a registered send adapter id, not "send", "find", "locate", or "check".
17
22
 
18
23
  Output: { transaction_id: string, status: string, adapter_receipt: object }
19
24
  - transaction_id: deterministically derived identifier for idempotency reasoning
@@ -21,7 +26,6 @@ Output: { transaction_id: string, status: string, adapter_receipt: object }
21
26
  - adapter_receipt: adapter-specific confirmation payload
22
27
 
23
28
  Rules:
24
- - Pick the send adapter from <available_adapters>; BM25 discovery is backend-internal.
25
29
  - send is IRREVERSIBLE — confirm intent clearly before calling.
26
30
  - The permission gauntlet (Layer 2 orange ⓶) executes before adapter dispatch.
27
31
  - Use transaction_id to reason about idempotency (same input → same ID).
@@ -7,14 +7,19 @@ export const CHECK_TOOL_NAME = 'check'
7
7
 
8
8
  /** One-line citizen-facing English description shown to the LLM (<= 240 chars). */
9
9
  export const DESCRIPTION =
10
- 'Delegate credential verification to an auth adapter. Use tool_id for registered methods such as certificates, simple auth, or mobile ID; UMMAYA never mints or stores credentials.'
10
+ 'Discover credential-check adapters. Prefer concrete adapter functions loaded by retrieval; check never mints or stores credentials.'
11
11
 
12
12
  /** Extended prompt included in the system-prompt tool-use section. */
13
- export const CHECK_TOOL_PROMPT = `Delegate credential checking to a registered UMMAYA auth adapter.
13
+ export const CHECK_TOOL_PROMPT = `Delegate credential checking to a concrete UMMAYA auth adapter.
14
14
 
15
- Input: { tool_id: string, params: object }
16
- - tool_id: the auth adapter identifier (e.g. "gongdong_injeungseo", "mobile_id")
17
- - params: adapter-defined credential parameter body
15
+ Preferred path:
16
+ - Call concrete adapter functions directly after their schemas are loaded.
17
+ - Adapter schemas are progressively disclosed by ToolSearch or backend top-K retrieval for the current citizen request.
18
+ - Use the adapter's exact schema fields for scope, purpose, and session-bound evidence.
19
+
20
+ Legacy root wrapper:
21
+ - If a concrete adapter function is not loaded and only the root primitive is available, check accepts { tool_id, params } for old transcripts and compatibility paths.
22
+ - tool_id must be a registered check adapter id, not "check", "find", "locate", or "send".
18
23
 
19
24
  Output (discriminated by auth_family):
20
25
  - auth_family: "gongdong_injeungseo" | "geumyung_injeungseo" | "ganpyeon_injeung" | "digital_onepass" | "mobile_id" | "mydata"
package/uv.lock CHANGED
@@ -2725,7 +2725,7 @@ wheels = [
2725
2725
 
2726
2726
  [[package]]
2727
2727
  name = "ummaya"
2728
- version = "0.2.1"
2728
+ version = "0.2.3"
2729
2729
  source = { editable = "." }
2730
2730
  dependencies = [
2731
2731
  { name = "httpx" },