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.
- package/bin/ummaya +76 -31
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -2
- package/src/ummaya/engine/engine.py +17 -34
- package/src/ummaya/engine/models.py +10 -4
- package/src/ummaya/engine/query.py +108 -5
- package/src/ummaya/ipc/stdio.py +576 -50
- package/src/ummaya/tools/executor.py +52 -6
- package/src/ummaya/tools/hira/hospital_search.py +15 -2
- package/src/ummaya/tools/location_adapters.py +25 -1
- package/src/ummaya/tools/mvp_surface.py +47 -46
- package/src/ummaya/tools/nmc/emergency_search.py +53 -1
- package/src/ummaya/tools/search.py +63 -2
- package/tui/package.json +1 -1
- package/tui/src/query.ts +1 -1
- package/tui/src/services/api/claude.ts +52 -3
- package/tui/src/tools/AdapterTool/AdapterTool.ts +262 -4
- package/tui/src/tools/LookupPrimitive/prompt.ts +19 -30
- package/tui/src/tools/ResolveLocationPrimitive/prompt.ts +12 -10
- package/tui/src/tools/SubmitPrimitive/prompt.ts +10 -6
- package/tui/src/tools/VerifyPrimitive/prompt.ts +10 -5
- package/uv.lock +1 -1
package/bin/ummaya
CHANGED
|
@@ -7,9 +7,27 @@ import { delimiter, dirname, join } from 'node:path'
|
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
|
|
9
9
|
const MINIMUM_BUN_VERSION = '1.3.0'
|
|
10
|
-
const
|
|
10
|
+
const PACKAGED_PRIMITIVE_TIMEOUT_MS = '90000'
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function currentLauncherPath() {
|
|
13
|
+
return realpathSync(fileURLToPath(import.meta.url))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function invokedLauncherPath() {
|
|
17
|
+
const argvPath = process.argv[1]
|
|
18
|
+
if (!argvPath) return null
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(argvPath)
|
|
21
|
+
} catch {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isDirectLaunch() {
|
|
27
|
+
return invokedLauncherPath() === currentLauncherPath()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function bunVersionAtLeast(version, minimum) {
|
|
13
31
|
const current = String(version)
|
|
14
32
|
.split('.')
|
|
15
33
|
.map((part) => Number.parseInt(part, 10))
|
|
@@ -62,7 +80,7 @@ function probeBunVersion(candidate) {
|
|
|
62
80
|
return (result.stdout || result.stderr).trim()
|
|
63
81
|
}
|
|
64
82
|
|
|
65
|
-
function launchWithCompatibleBun(currentVersion = null) {
|
|
83
|
+
function launchWithCompatibleBun(launcherPath, currentVersion = null) {
|
|
66
84
|
const checked = []
|
|
67
85
|
for (const candidate of collectBunCandidates()) {
|
|
68
86
|
const version = probeBunVersion(candidate)
|
|
@@ -94,17 +112,7 @@ function launchWithCompatibleBun(currentVersion = null) {
|
|
|
94
112
|
process.exit(1)
|
|
95
113
|
}
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
if (!runningBun) {
|
|
99
|
-
launchWithCompatibleBun()
|
|
100
|
-
}
|
|
101
|
-
if (!bunVersionAtLeast(runningBun.version, MINIMUM_BUN_VERSION)) {
|
|
102
|
-
launchWithCompatibleBun(runningBun.version)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const packageRoot = dirname(dirname(launcherPath))
|
|
106
|
-
|
|
107
|
-
function loadPackageDotenv(root) {
|
|
115
|
+
export function loadPackageDotenv(root, env = process.env) {
|
|
108
116
|
const envPath = join(root, '.env')
|
|
109
117
|
if (!existsSync(envPath)) return
|
|
110
118
|
const lines = readFileSync(envPath, 'utf8').split(/\r?\n/)
|
|
@@ -114,29 +122,66 @@ function loadPackageDotenv(root) {
|
|
|
114
122
|
const index = line.indexOf('=')
|
|
115
123
|
const key = line.slice(0, index).trim()
|
|
116
124
|
let value = line.slice(index + 1).trim()
|
|
117
|
-
if (!key ||
|
|
125
|
+
if (!key || env[key] !== undefined) continue
|
|
118
126
|
if (
|
|
119
127
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
120
128
|
(value.startsWith("'") && value.endsWith("'"))
|
|
121
129
|
) {
|
|
122
130
|
value = value.slice(1, -1)
|
|
123
131
|
}
|
|
124
|
-
|
|
132
|
+
env[key] = value
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildBackendCommand(root) {
|
|
137
|
+
const packagedPython = join(root, '.venv', 'bin', 'python')
|
|
138
|
+
if (existsSync(packagedPython)) {
|
|
139
|
+
return [packagedPython, '-m', 'ummaya.cli', '--ipc', 'stdio']
|
|
125
140
|
}
|
|
141
|
+
|
|
142
|
+
return ['uv', '--directory', root, 'run', '--frozen', '--no-dev', 'ummaya', '--ipc', 'stdio']
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function configurePackageEnv(root, env = process.env) {
|
|
146
|
+
env.UMMAYA_PACKAGE_ROOT = root
|
|
147
|
+
if (env.UMMAYA_ALLOW_BACKEND_CMD_OVERRIDE !== '1' || !env.UMMAYA_BACKEND_CMD_JSON) {
|
|
148
|
+
env.UMMAYA_BACKEND_CMD_JSON = JSON.stringify(buildBackendCommand(root))
|
|
149
|
+
}
|
|
150
|
+
env.UMMAYA_TUI_PRIMITIVE_TIMEOUT_MS ??= PACKAGED_PRIMITIVE_TIMEOUT_MS
|
|
151
|
+
return env
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function inspectLauncherAndExit() {
|
|
155
|
+
if (process.env.UMMAYA_LAUNCHER_INSPECT !== '1') return
|
|
156
|
+
process.stdout.write(
|
|
157
|
+
`${JSON.stringify({
|
|
158
|
+
packageRoot: process.env.UMMAYA_PACKAGE_ROOT,
|
|
159
|
+
backendCommand: JSON.parse(process.env.UMMAYA_BACKEND_CMD_JSON ?? '[]'),
|
|
160
|
+
primitiveTimeoutMs: process.env.UMMAYA_TUI_PRIMITIVE_TIMEOUT_MS,
|
|
161
|
+
})}\n`,
|
|
162
|
+
)
|
|
163
|
+
process.exit(0)
|
|
126
164
|
}
|
|
127
165
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
166
|
+
export async function main() {
|
|
167
|
+
const launcherPath = currentLauncherPath()
|
|
168
|
+
const runningBun = globalThis.Bun
|
|
169
|
+
if (!runningBun) {
|
|
170
|
+
launchWithCompatibleBun(launcherPath)
|
|
171
|
+
}
|
|
172
|
+
if (!bunVersionAtLeast(runningBun.version, MINIMUM_BUN_VERSION)) {
|
|
173
|
+
launchWithCompatibleBun(launcherPath, runningBun.version)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const packageRoot = dirname(dirname(launcherPath))
|
|
177
|
+
loadPackageDotenv(packageRoot)
|
|
178
|
+
configurePackageEnv(packageRoot)
|
|
179
|
+
inspectLauncherAndExit()
|
|
180
|
+
|
|
181
|
+
await import(join(packageRoot, 'tui/src/stubs/macro-preload.ts'))
|
|
182
|
+
await import(join(packageRoot, 'tui/src/entrypoints/cli.tsx'))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isDirectLaunch()) {
|
|
186
|
+
await main()
|
|
187
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ummaya",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "ummaya",
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.3",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ummaya"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "Conversational multi-agent platform for Korean public APIs"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -314,7 +314,7 @@ min_confidence = 80
|
|
|
314
314
|
|
|
315
315
|
[tool.commitizen]
|
|
316
316
|
name = "cz_conventional_commits"
|
|
317
|
-
version = "0.2.
|
|
317
|
+
version = "0.2.3"
|
|
318
318
|
tag_format = "v$version"
|
|
319
319
|
|
|
320
320
|
# PyTorch CPU-only wheel for Docker image size discipline (SC-1: ≤ 2 GB).
|
|
@@ -75,17 +75,6 @@ def _contains_location_dependent_key(value: object) -> bool:
|
|
|
75
75
|
return False
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
def _allowed_core_tools_for_available_adapters(
|
|
79
|
-
visible_primitives: set[str],
|
|
80
|
-
has_location_dependent_schema: bool,
|
|
81
|
-
) -> frozenset[str] | None:
|
|
82
|
-
"""Constrain root primitive exposure for location-independent find turns."""
|
|
83
|
-
|
|
84
|
-
if visible_primitives == {"find"} and not has_location_dependent_schema:
|
|
85
|
-
return frozenset({"find"})
|
|
86
|
-
return None
|
|
87
|
-
|
|
88
|
-
|
|
89
78
|
class QueryEngine:
|
|
90
79
|
"""Per-session orchestrator for the UMMAYA query engine.
|
|
91
80
|
|
|
@@ -216,7 +205,7 @@ class QueryEngine:
|
|
|
216
205
|
ChatMessage(role="user", content=assembled.turn_attachment.content),
|
|
217
206
|
)
|
|
218
207
|
|
|
219
|
-
dynamic_adapter_message,
|
|
208
|
+
dynamic_adapter_message, turn_tool_ids = self._build_available_adapters_context(
|
|
220
209
|
user_message
|
|
221
210
|
)
|
|
222
211
|
if dynamic_adapter_message is not None:
|
|
@@ -235,7 +224,7 @@ class QueryEngine:
|
|
|
235
224
|
tool_registry=self._tool_registry,
|
|
236
225
|
config=self._config,
|
|
237
226
|
session_context=self._permission_session,
|
|
238
|
-
|
|
227
|
+
turn_tool_ids=turn_tool_ids,
|
|
239
228
|
turn_start_message_index=turn_start_message_index,
|
|
240
229
|
)
|
|
241
230
|
|
|
@@ -281,13 +270,13 @@ class QueryEngine:
|
|
|
281
270
|
def _build_available_adapters_message(self, user_message: str) -> ChatMessage | None:
|
|
282
271
|
"""Inject BM25 adapter candidates for the current citizen utterance."""
|
|
283
272
|
|
|
284
|
-
message,
|
|
273
|
+
message, _turn_tool_ids = self._build_available_adapters_context(user_message)
|
|
285
274
|
return message
|
|
286
275
|
|
|
287
276
|
def _build_available_adapters_context(
|
|
288
277
|
self, user_message: str
|
|
289
|
-
) -> tuple[ChatMessage | None,
|
|
290
|
-
"""Build dynamic adapter context and per-turn
|
|
278
|
+
) -> tuple[ChatMessage | None, tuple[str, ...]]:
|
|
279
|
+
"""Build dynamic adapter context and per-turn concrete tool exposure."""
|
|
291
280
|
|
|
292
281
|
try:
|
|
293
282
|
from ummaya.tools.search import search # noqa: PLC0415
|
|
@@ -300,11 +289,10 @@ class QueryEngine:
|
|
|
300
289
|
)
|
|
301
290
|
except Exception: # noqa: BLE001
|
|
302
291
|
logger.exception("available_adapters auto-inject failed")
|
|
303
|
-
return None,
|
|
292
|
+
return None, ()
|
|
304
293
|
|
|
305
294
|
adapter_lines: list[str] = []
|
|
306
|
-
|
|
307
|
-
has_location_dependent_schema = False
|
|
295
|
+
selected_tool_ids: list[str] = []
|
|
308
296
|
primary_find_without_location = False
|
|
309
297
|
visible_count = 0
|
|
310
298
|
for candidate in candidates:
|
|
@@ -312,6 +300,8 @@ class QueryEngine:
|
|
|
312
300
|
tool = self._tool_registry.find(candidate.tool_id)
|
|
313
301
|
except ToolNotFoundError:
|
|
314
302
|
continue
|
|
303
|
+
if candidate.score <= 0:
|
|
304
|
+
continue
|
|
315
305
|
if tool.is_core or tool.ministry == "UMMAYA":
|
|
316
306
|
continue
|
|
317
307
|
primitive = candidate.primitive if isinstance(candidate.primitive, str) else None
|
|
@@ -324,15 +314,12 @@ class QueryEngine:
|
|
|
324
314
|
)
|
|
325
315
|
if visible_count > 0 and primary_find_without_location and requires_location:
|
|
326
316
|
continue
|
|
327
|
-
if primitive is not None:
|
|
328
|
-
visible_primitives.add(primitive)
|
|
329
|
-
if requires_location:
|
|
330
|
-
has_location_dependent_schema = True
|
|
331
317
|
schema_json = json.dumps(
|
|
332
318
|
candidate.input_schema_json,
|
|
333
319
|
ensure_ascii=False,
|
|
334
320
|
sort_keys=True,
|
|
335
321
|
)
|
|
322
|
+
selected_tool_ids.append(candidate.tool_id)
|
|
336
323
|
adapter_lines.extend(
|
|
337
324
|
[
|
|
338
325
|
f"- tool_id: {candidate.tool_id}",
|
|
@@ -340,8 +327,7 @@ class QueryEngine:
|
|
|
340
327
|
f" description: {candidate.llm_description or tool.name_ko}",
|
|
341
328
|
f" required_params: {candidate.required_params}",
|
|
342
329
|
f" input_schema_json: {schema_json}",
|
|
343
|
-
f" call_hint: {candidate.
|
|
344
|
-
f'{{"tool_id":"{candidate.tool_id}","params":{{...}}}})',
|
|
330
|
+
f" call_hint: {candidate.tool_id}({{...}})",
|
|
345
331
|
f" policy_url: {candidate.real_classification_url or ''}",
|
|
346
332
|
]
|
|
347
333
|
)
|
|
@@ -350,19 +336,16 @@ class QueryEngine:
|
|
|
350
336
|
break
|
|
351
337
|
|
|
352
338
|
if not adapter_lines:
|
|
353
|
-
return None,
|
|
354
|
-
|
|
355
|
-
allowed_core_tool_ids = _allowed_core_tools_for_available_adapters(
|
|
356
|
-
visible_primitives,
|
|
357
|
-
has_location_dependent_schema,
|
|
358
|
-
)
|
|
339
|
+
return None, ()
|
|
359
340
|
|
|
360
341
|
content = "\n".join(
|
|
361
342
|
[
|
|
362
343
|
"<available_adapters>",
|
|
363
344
|
"Use these adapter candidates for this citizen request. "
|
|
364
|
-
"
|
|
365
|
-
"
|
|
345
|
+
"Call the function named exactly as tool_id with that adapter's "
|
|
346
|
+
"schema arguments. Do not wrap adapter calls in root primitives "
|
|
347
|
+
"such as find({tool_id, params}), locate({tool_id, params}), "
|
|
348
|
+
"check({tool_id, params}), or send({tool_id, params}). "
|
|
366
349
|
"Do not call locate just because the citizen text contains a "
|
|
367
350
|
"city/province name; treat that as the dataset/filter term. "
|
|
368
351
|
"Call locate only when the selected adapter schema requires "
|
|
@@ -371,7 +354,7 @@ class QueryEngine:
|
|
|
371
354
|
"</available_adapters>",
|
|
372
355
|
]
|
|
373
356
|
)
|
|
374
|
-
return ChatMessage(role="system", content=content),
|
|
357
|
+
return ChatMessage(role="system", content=content), tuple(selected_tool_ids)
|
|
375
358
|
|
|
376
359
|
def set_permission_session(self, session: SessionContext | None) -> None:
|
|
377
360
|
"""Update the permission-pipeline session used for subsequent turns.
|
|
@@ -121,11 +121,17 @@ class QueryContext(BaseModel):
|
|
|
121
121
|
"""
|
|
122
122
|
|
|
123
123
|
allowed_core_tool_ids: frozenset[str] | None = None
|
|
124
|
-
"""
|
|
124
|
+
"""Legacy per-turn allow-list for primitive wrappers.
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
Preserved for callers that still expose the old root primitives. New turns
|
|
127
|
+
should prefer ``turn_tool_ids`` so the model sees concrete adapter schemas.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
turn_tool_ids: tuple[str, ...] = ()
|
|
131
|
+
"""Concrete adapter tool IDs selected for this citizen turn.
|
|
132
|
+
|
|
133
|
+
When populated, the query loop exports these concrete adapter schemas as the
|
|
134
|
+
provider tool surface instead of dumping the root primitive wrappers.
|
|
129
135
|
"""
|
|
130
136
|
|
|
131
137
|
turn_start_message_index: int = 0
|
|
@@ -82,7 +82,7 @@ def _assemble_tool_calls(
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def _tool_definition_name(tool_def: ToolDefinition | dict[str, object]) -> str | None:
|
|
85
|
-
"""Extract a
|
|
85
|
+
"""Extract a function name from an OpenAI tool definition."""
|
|
86
86
|
|
|
87
87
|
if isinstance(tool_def, ToolDefinition):
|
|
88
88
|
return tool_def.function.name
|
|
@@ -93,6 +93,27 @@ def _tool_definition_name(tool_def: ToolDefinition | dict[str, object]) -> str |
|
|
|
93
93
|
return name if isinstance(name, str) else None
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def _export_turn_tool_definitions(
|
|
97
|
+
tool_registry: ToolRegistry,
|
|
98
|
+
tool_ids: tuple[str, ...],
|
|
99
|
+
) -> list[dict[str, object]]:
|
|
100
|
+
"""Export selected concrete adapter schemas in ranking order."""
|
|
101
|
+
|
|
102
|
+
tool_defs: list[dict[str, object]] = []
|
|
103
|
+
seen: set[str] = set()
|
|
104
|
+
for tool_id in tool_ids:
|
|
105
|
+
if tool_id in seen:
|
|
106
|
+
continue
|
|
107
|
+
seen.add(tool_id)
|
|
108
|
+
try:
|
|
109
|
+
tool = tool_registry.find(tool_id)
|
|
110
|
+
except ToolNotFoundError:
|
|
111
|
+
logger.warning("Selected turn tool disappeared from registry: %s", tool_id)
|
|
112
|
+
continue
|
|
113
|
+
tool_defs.append(tool.to_openai_tool())
|
|
114
|
+
return tool_defs
|
|
115
|
+
|
|
116
|
+
|
|
96
117
|
def _latest_successful_tool_payload(messages: list[ChatMessage]) -> dict[str, object] | None:
|
|
97
118
|
"""Return the latest non-error tool-result JSON payload, if present."""
|
|
98
119
|
|
|
@@ -296,14 +317,38 @@ async def dispatch_tool_calls( # noqa: C901
|
|
|
296
317
|
|
|
297
318
|
async def _dispatch_one(tc: ToolCall) -> ToolResult:
|
|
298
319
|
"""Dispatch a single tool call via the executor."""
|
|
299
|
-
if tc.function.name in {"find", "locate"}:
|
|
320
|
+
if tc.function.name in {"find", "locate", "check", "send"}:
|
|
300
321
|
return await _dispatch_root_primitive(
|
|
301
322
|
tc,
|
|
302
323
|
tool_registry,
|
|
303
324
|
tool_executor,
|
|
304
325
|
session_context=session_context,
|
|
305
326
|
)
|
|
306
|
-
|
|
327
|
+
try:
|
|
328
|
+
tool = tool_registry.find(tc.function.name)
|
|
329
|
+
except ToolNotFoundError:
|
|
330
|
+
return await tool_executor.dispatch(tc.function.name, tc.function.arguments)
|
|
331
|
+
gate = tool.policy.citizen_facing_gate if tool.policy is not None else None
|
|
332
|
+
if gate in {None, "read-only"}:
|
|
333
|
+
return await tool_executor.dispatch(
|
|
334
|
+
tc.function.name,
|
|
335
|
+
tc.function.arguments,
|
|
336
|
+
tool_call_id=tc.id,
|
|
337
|
+
)
|
|
338
|
+
primitive = tool.primitive
|
|
339
|
+
if primitive is None:
|
|
340
|
+
return ToolResult(
|
|
341
|
+
tool_id=tc.function.name,
|
|
342
|
+
success=False,
|
|
343
|
+
error=f"{tc.function.name} is missing primitive metadata for gated dispatch.",
|
|
344
|
+
error_type="schema_mismatch",
|
|
345
|
+
)
|
|
346
|
+
return await _dispatch_concrete_adapter(
|
|
347
|
+
tc,
|
|
348
|
+
primitive,
|
|
349
|
+
tool_executor,
|
|
350
|
+
session_context=session_context,
|
|
351
|
+
)
|
|
307
352
|
|
|
308
353
|
async def _flush_group(items: list[tuple[int, ToolCall]], safe: bool) -> None:
|
|
309
354
|
"""Execute a group of tool calls, concurrently if safe."""
|
|
@@ -407,6 +452,59 @@ async def _dispatch_root_primitive(
|
|
|
407
452
|
return ToolResult(tool_id=primitive, success=True, data=data)
|
|
408
453
|
|
|
409
454
|
|
|
455
|
+
async def _dispatch_concrete_adapter(
|
|
456
|
+
tc: ToolCall,
|
|
457
|
+
primitive: str,
|
|
458
|
+
tool_executor: ToolExecutor,
|
|
459
|
+
*,
|
|
460
|
+
session_context: SessionContext | None,
|
|
461
|
+
) -> ToolResult:
|
|
462
|
+
"""Dispatch a directly model-facing concrete adapter call."""
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
raw_args = json.loads(tc.function.arguments)
|
|
466
|
+
except (TypeError, json.JSONDecodeError) as exc:
|
|
467
|
+
return ToolResult(
|
|
468
|
+
tool_id=tc.function.name,
|
|
469
|
+
success=False,
|
|
470
|
+
error=str(exc),
|
|
471
|
+
error_type="validation",
|
|
472
|
+
)
|
|
473
|
+
if not isinstance(raw_args, dict):
|
|
474
|
+
return ToolResult(
|
|
475
|
+
tool_id=tc.function.name,
|
|
476
|
+
success=False,
|
|
477
|
+
error=f"{tc.function.name} requires a JSON object argument.",
|
|
478
|
+
error_type="validation",
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
request_id = tc.id or f"{tc.function.name}-call"
|
|
482
|
+
if primitive == "find":
|
|
483
|
+
output = await tool_executor.invoke(
|
|
484
|
+
tc.function.name,
|
|
485
|
+
raw_args,
|
|
486
|
+
request_id=request_id,
|
|
487
|
+
session_identity=session_context,
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
output = await tool_executor.invoke_raw(
|
|
491
|
+
tc.function.name,
|
|
492
|
+
raw_args,
|
|
493
|
+
request_id=request_id,
|
|
494
|
+
session_identity=session_context,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
data = _primitive_output_dict(output)
|
|
498
|
+
if data.get("kind") == "error":
|
|
499
|
+
return ToolResult(
|
|
500
|
+
tool_id=tc.function.name,
|
|
501
|
+
success=False,
|
|
502
|
+
error=str(data.get("message") or data),
|
|
503
|
+
error_type="execution",
|
|
504
|
+
)
|
|
505
|
+
return ToolResult(tool_id=tc.function.name, success=True, data=data)
|
|
506
|
+
|
|
507
|
+
|
|
410
508
|
def _primitive_output_dict(output: object) -> dict[str, object]:
|
|
411
509
|
"""Convert primitive facade output to ToolResult data."""
|
|
412
510
|
|
|
@@ -532,8 +630,13 @@ async def _query_inner(ctx: QueryContext) -> AsyncIterator[QueryEvent]: # noqa:
|
|
|
532
630
|
tool_defs: list[ToolDefinition | dict[str, object]] | None = None
|
|
533
631
|
force_no_tools_next_turn = False
|
|
534
632
|
else:
|
|
535
|
-
raw_defs =
|
|
536
|
-
|
|
633
|
+
raw_defs = _export_turn_tool_definitions(
|
|
634
|
+
ctx.tool_registry,
|
|
635
|
+
ctx.turn_tool_ids,
|
|
636
|
+
)
|
|
637
|
+
if not raw_defs:
|
|
638
|
+
raw_defs = ctx.tool_registry.export_core_tools_openai()
|
|
639
|
+
if ctx.allowed_core_tool_ids is not None and not ctx.turn_tool_ids:
|
|
537
640
|
raw_defs = [
|
|
538
641
|
tool_def
|
|
539
642
|
for tool_def in raw_defs
|