Skip to content

Commit 34eda36

Browse files
michalharakalclaude
andcommitted
feat: add tool calling smoke tests and single-shot demo mode
Add tool calling test phase to smoke-test.sh that runs --demo with a prompt for models with toolCalling config. Add Qwen3-8B-Q4 to smoke test config. - Add ToolCallingDemo.runSingleShot() for non-interactive tool calling - Wire --demo with positional prompt to single-shot mode in kllama and skainet CLIs - Add tool calling section to smoke-test.sh with [Tool Call] detection - Add skainet runner to smoke-test.sh runner_args - Increase kllama-cli memory to -Xmx42g -XX:MaxDirectMemorySize=64g - Add Qwen3-8B-Q4 and toolCalling config to smoke-models.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8af88c8 commit 34eda36

7 files changed

Lines changed: 202 additions & 23 deletions

File tree

llm-apps/kllama-cli/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ tasks.withType<Test>().configureEach {
3434
}
3535

3636
tasks.withType<JavaExec>().configureEach {
37-
jvmArgs("--enable-preview", "--add-modules", "jdk.incubator.vector", "-Xmx12g")
37+
jvmArgs("--enable-preview", "--add-modules", "jdk.incubator.vector", "-Xmx42g", "-XX:MaxDirectMemorySize=64g")
3838
}

llm-apps/skainet-cli/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,5 @@ tasks.withType<Test>().configureEach {
5050
}
5151

5252
tasks.withType<JavaExec>().configureEach {
53-
jvmArgs("--enable-preview", "--add-modules", "jdk.incubator.vector", "-Xmx12g", "-XX:MaxDirectMemorySize=64g")
53+
jvmArgs("--enable-preview", "--add-modules", "jdk.incubator.vector", "-Xmx48g", "-XX:MaxDirectMemorySize=64g")
5454
}

llm-apps/skainet-cli/src/main/kotlin/sk/ainet/apps/skainet/cli/Main.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ fun main(args: Array<String>) {
209209
when {
210210
cliArgs.demoMode -> {
211211
val demo = ToolCallingDemo(runtime, tokenizer, cliArgs.templateName, metadata)
212-
demo.run(maxTokens = cliArgs.steps, temperature = cliArgs.temperature)
212+
if (cliArgs.prompt != null) {
213+
demo.runSingleShot(cliArgs.prompt, maxTokens = cliArgs.steps, temperature = cliArgs.temperature)
214+
} else {
215+
demo.run(maxTokens = cliArgs.steps, temperature = cliArgs.temperature)
216+
}
213217
}
214218
cliArgs.agentMode -> {
215219
val agentCli = AgentCli(runtime, tokenizer, cliArgs.templateName, metadata)

llm-runtime/kllama/src/jvmMain/kotlin/sk/ainet/apps/kllama/cli/Main.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,11 @@ fun main(args: Array<String>) {
496496
when {
497497
cliArgs.demoMode -> {
498498
val demo = ToolCallingDemo(runtime, tokenizer, cliArgs.templateName, metadata)
499-
demo.run(maxTokens = cliArgs.steps, temperature = cliArgs.temperature)
499+
if (cliArgs.prompt != null) {
500+
demo.runSingleShot(cliArgs.prompt, maxTokens = cliArgs.steps, temperature = cliArgs.temperature)
501+
} else {
502+
demo.run(maxTokens = cliArgs.steps, temperature = cliArgs.temperature)
503+
}
500504
}
501505
cliArgs.agentMode -> {
502506
val agentCli = AgentCli(runtime, tokenizer, cliArgs.templateName, metadata)

llm-runtime/kllama/src/jvmMain/kotlin/sk/ainet/apps/kllama/cli/ToolCallingDemo.kt

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,70 @@ public class ToolCallingDemo<T : DType>(
4646
/**
4747
* Run the tool-calling demo with `list_files` and `calculator` tools.
4848
*/
49+
/**
50+
* Run a single non-interactive tool calling round. Used by smoke tests.
51+
*
52+
* @param prompt The user prompt.
53+
* @param maxTokens Maximum tokens per round.
54+
* @param temperature Sampling temperature.
55+
*/
56+
public fun runSingleShot(
57+
prompt: String,
58+
maxTokens: Int = 256,
59+
temperature: Float = 0.7f
60+
) {
61+
val registry = ToolRegistry()
62+
registry.register(ListFilesTool())
63+
registry.register(CalculatorTool())
64+
65+
println("Tool Calling Smoke Test")
66+
println("Available tools: ${registry.definitions().joinToString { it.name }}")
67+
println("Prompt: \"$prompt\"")
68+
println("---")
69+
70+
val agentLoop = session.createAgentLoop(registry, maxTokens, temperature)
71+
72+
val systemPrompt = """You are a helpful assistant with access to tools.
73+
When the user asks about files or directories, use the list_files tool to look up the actual contents.
74+
When the user asks to calculate something, use the calculator tool.
75+
Always use a tool when one is relevant — do not guess file listings."""
76+
77+
val messages = mutableListOf(
78+
ChatMessage(role = ChatRole.SYSTEM, content = systemPrompt),
79+
ChatMessage(role = ChatRole.USER, content = prompt)
80+
)
81+
82+
val listener = object : AgentListener {
83+
override fun onToken(token: String) {
84+
print(token)
85+
System.out.flush()
86+
}
87+
override fun onAssistantMessage(text: String) { println() }
88+
override fun onToolCalls(calls: List<ToolCall>) {
89+
for (call in calls) println("[Tool Call] ${call.name}(${call.arguments})")
90+
}
91+
override fun onToolResult(call: ToolCall, result: String) {
92+
println("[Tool Result] ${call.name} -> $result")
93+
print("Assistant: ")
94+
System.out.flush()
95+
}
96+
override fun onComplete(finalResponse: String) {}
97+
}
98+
99+
print("Assistant: ")
100+
System.out.flush()
101+
102+
agentLoop.runWithEncoder(
103+
messages = messages,
104+
encode = { text -> tokenizer.encode(text) },
105+
listener = listener
106+
)
107+
println()
108+
}
109+
110+
/**
111+
* Run the interactive tool-calling demo.
112+
*/
49113
public fun run(
50114
maxTokens: Int = 512,
51115
temperature: Float = 0.7f

tests/smoke/smoke-models.json

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,30 @@
66
},
77
"models": [
88
{
9-
"name": "Llama-3.2-1B-Q8",
9+
"name": "TinyLlama-1.1B-Q8",
1010
"runner": "kllama",
11-
"model": "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/tinyllama-1.1b-chat-v1.0.Q8_0.gguf",
11+
"model": "tinyllama-1.1b-chat-v1.0.Q8_0.gguf",
1212
"format": "gguf"
1313
},
1414
{
15-
"name": "Gemma-2B-SafeTensors",
16-
"runner": "kgemma",
17-
"model": "unsloth/gemma-3-270m-it-GGUF/gemma-3-270m-it-Q8_0.gguf",
15+
"name": "Qwen3-1.7B-Q8",
16+
"runner": "kqwen",
17+
"model": "Qwen3-1.7B-Q8_0.gguf",
1818
"format": "gguf",
19-
"steps": 16
19+
"toolCalling": {
20+
"prompt": "What is 2 + 2?",
21+
"steps": 256
22+
}
2023
},
2124
{
22-
"name": "Qwen3-1.7B",
23-
"runner": "qwen",
24-
"model": "Qwen3-1.7B-GGUF/Qwen3-1.7B-Q8_0.gguf",
25-
"format": "safetensors",
26-
"prompt": "Hello world",
27-
},
28-
{
29-
"name": "BERT-MiniLM",
30-
"runner": "kbert",
31-
"model": "~/.cache/huggingface/models/all-MiniLM-L6-v2",
32-
"format": "safetensors",
33-
"prompt": "Hello world"
25+
"name": "Qwen3-8B-Q4",
26+
"runner": "kllama",
27+
"model": "Qwen3-8B-Q4_K_M.gguf",
28+
"format": "gguf",
29+
"toolCalling": {
30+
"prompt": "What is 2 + 2?",
31+
"steps": 256
32+
}
3433
}
3534
]
3635
}

tests/smoke/smoke-test.sh

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ runner_args() {
6969
local runner="$1" model="$2" prompt="$3" steps="$4" temp="$5" doc="${6:-}" output="${7:-}"
7070

7171
case "$runner" in
72+
skainet) echo "-m ${model} -s ${steps} -k ${temp} \"${prompt}\"" ;;
7273
kllama) echo "-m ${model} -s ${steps} -k ${temp} \"${prompt}\"" ;;
7374
kgemma) echo "${model} \"${prompt}\" ${steps} ${temp}" ;;
7475
kqwen) echo "${model} \"${prompt}\" ${steps} ${temp}" ;;
@@ -254,9 +255,93 @@ print(f'M_OUTPUT={repr(m.get(\"output\", \"\"))}')
254255
separator
255256
done
256257

258+
# ── Tool Calling Tests ───────────────────────────────────────────
259+
TC_COUNT=$(python3 -c "
260+
import json
261+
cfg = json.load(open('${CONFIG_FILE}'))
262+
print(sum(1 for m in cfg['models'] if m.get('toolCalling')))
263+
")
264+
265+
declare -a tc_results=()
266+
tc_pass=0
267+
tc_fail=0
268+
269+
if [[ "$TC_COUNT" -gt 0 ]]; then
270+
echo ""
271+
echo -e "${BOLD}Tool Calling Tests${RESET} ($TC_COUNT models)"
272+
separator
273+
274+
kllama_task=$(runner_task "kllama")
275+
276+
for i in $(seq 0 $((MODEL_COUNT - 1))); do
277+
eval "$(python3 -c "
278+
import json
279+
cfg = json.load(open('${CONFIG_FILE}'))
280+
m = cfg['models'][$i]
281+
tc = m.get('toolCalling')
282+
if tc is None:
283+
print('TC_ENABLED=false')
284+
else:
285+
print('TC_ENABLED=true')
286+
print(f'TC_PROMPT={repr(tc.get(\"prompt\", \"What is 2 + 2?\"))}')
287+
print(f'TC_STEPS={tc.get(\"steps\", 256)}')
288+
print(f'M_NAME={repr(m[\"name\"])}')
289+
print(f'M_MODEL={repr(m[\"model\"])}')
290+
")"
291+
292+
[[ "$TC_ENABLED" != "true" ]] && continue
293+
294+
M_MODEL=$(expand_path "$M_MODEL")
295+
296+
echo -e "\n${BOLD}Model:${RESET} $M_NAME (tool calling)"
297+
echo -e "${BOLD}Prompt:${RESET} \"$TC_PROMPT\""
298+
299+
if [[ ! -e "$M_MODEL" ]]; then
300+
echo -e " ${RED}FAIL${RESET} (model path not found)"
301+
tc_fail=$((tc_fail + 1))
302+
tc_results+=("FAIL|$M_NAME|not found|-")
303+
separator
304+
continue
305+
fi
306+
307+
start_ts=$(python3 -c 'import time; print(time.time())')
308+
output_file=$(mktemp)
309+
exit_code=0
310+
311+
$GRADLE "$kllama_task" --quiet \
312+
--args="-m ${M_MODEL} --demo -s ${TC_STEPS} -k 0.7 \"${TC_PROMPT}\"" \
313+
> "$output_file" 2>&1 || exit_code=$?
314+
315+
end_ts=$(python3 -c 'import time; print(time.time())')
316+
wall_sec=$(python3 -c "print(f'{$end_ts - $start_ts:.1f}')")
317+
318+
if [[ $exit_code -ne 0 ]]; then
319+
echo -e " ${RED}FAIL${RESET} (exit $exit_code, wall ${wall_sec}s)"
320+
tail -5 "$output_file" | sed 's/^/ │ /'
321+
tc_fail=$((tc_fail + 1))
322+
tc_results+=("FAIL|$M_NAME|exit $exit_code|${wall_sec}s")
323+
elif grep -q '\[Tool Call\]' "$output_file"; then
324+
tool_name=$(grep -oE '\[Tool Call\] [a-z_]+' "$output_file" | head -1 | sed 's/\[Tool Call\] //')
325+
echo -e " ${GREEN}OK${RESET} tool called: ${CYAN}${tool_name}${RESET} wall: ${wall_sec}s"
326+
grep '\[Tool Call\]' "$output_file" | head -2 | sed 's/^/ │ /'
327+
grep '\[Tool Result\]' "$output_file" | head -2 | sed 's/^/ │ /'
328+
tc_pass=$((tc_pass + 1))
329+
tc_results+=("OK|$M_NAME|$tool_name|${wall_sec}s")
330+
else
331+
echo -e " ${YELLOW}WARN${RESET} (no tool call detected, wall ${wall_sec}s)"
332+
tail -5 "$output_file" | sed 's/^/ │ /'
333+
tc_fail=$((tc_fail + 1))
334+
tc_results+=("WARN|$M_NAME|no tool call|${wall_sec}s")
335+
fi
336+
337+
rm -f "$output_file"
338+
separator
339+
done
340+
fi
341+
257342
# ── Summary ──────────────────────────────────────────────────────
258343
echo ""
259-
echo -e "${BOLD}Summary${RESET}"
344+
echo -e "${BOLD}Summary — Generation${RESET}"
260345
separator
261346
printf " %-6s %-30s %-8s %8s %10s %8s\n" "Status" "Model" "Runner" "Size" "tok/s" "Wall"
262347
separator
@@ -272,6 +357,29 @@ print(f'M_OUTPUT={repr(m.get(\"output\", \"\"))}')
272357
done
273358
separator
274359
echo -e " ${GREEN}Pass: $pass${RESET} ${RED}Fail: $fail${RESET} Total: ${MODEL_COUNT}"
360+
361+
if [[ "$TC_COUNT" -gt 0 ]]; then
362+
echo ""
363+
echo -e "${BOLD}Summary — Tool Calling${RESET}"
364+
separator
365+
printf " %-6s %-30s %-15s %8s\n" "Status" "Model" "Tool" "Wall"
366+
separator
367+
for r in "${tc_results[@]}"; do
368+
IFS='|' read -r status name tool wall <<< "$r"
369+
if [[ "$status" == "OK" ]]; then
370+
color="$GREEN"
371+
elif [[ "$status" == "WARN" ]]; then
372+
color="$YELLOW"
373+
else
374+
color="$RED"
375+
fi
376+
printf " ${color}%-6s${RESET} %-30s %-15s %8s\n" \
377+
"$status" "${name:0:30}" "$tool" "$wall"
378+
done
379+
separator
380+
echo -e " ${GREEN}Pass: $tc_pass${RESET} ${RED}Fail: $tc_fail${RESET} Total: ${TC_COUNT}"
381+
fi
382+
275383
echo ""
276384
exit 0
277385
fi

0 commit comments

Comments
 (0)