Commit 3c9d3c4
Paul C
v22.7.2: local-AI tool-calling fixes + AI bubble visibility on restart
Three urgent fixes targeting the Discord reports about local AI not
working in WolfStack despite working via curl, plus Gary's restart
bubble bug.
1. Local-model tool calling — recover from content-string emissions
Reproduced live on this box against Ollama's qwen2.5-coder:32b: the
model emits tool calls as JSON *inside* `message.content` instead
of in the structured `tool_calls` array. WolfStack's previous
parser only read `tool_calls`, so users got raw JSON as the "AI's
reply" and tool dispatch never fired. Common with smaller / not-
tool-fine-tuned local models (qwen2.5 below 7B, llama3.2 variants,
gemma tunes, FunctionGemma).
New `extract_tool_calls_from_content` in `src/ai/mod.rs` recognises
six common content-side wire formats and translates them into the
same bracket-tag pipeline the rest of the code uses:
• bare `{"name": "fn", "arguments": {…}}` object
• bare array `[{"name": "fn", "arguments": {…}}, …]`
• OpenAI-shape `{"function": {…}}` inside content
• Fenced ```json blocks
• `<tool_call>…</tool_call>` and `<function_call>…</function_call>`
XML wrappers (FunctionGemma + qwen tool-call dialect)
• Mistral's `[TOOL_CALLS]` prefix
**Critical safety property**: takes an `allowed_tool_names` param
and drops any extracted call whose name isn't on the caller's
allowlist. Without that, prose like `{"name": "nginx", "status":
"stopped"}` (a model explaining service state) would synthesise a
phantom tool call — on a sysadmin platform that's potential
command-injection-via-AI-response. The allowlist closes that.
`MAIN_AI_TOOLS` constant gates the chat path; WolfAgents passes
each agent's `allowed_tools` set.
`<tool_call>` XML stripper anchored at position 0 of the trimmed
string so a mid-prose echo can't trigger extraction. Fenced-block
close detected with proper `rfind("```")` instead of trim-end-all-
backticks.
14 unit tests across `content_tool_call_tests` covering every
wire format, the rejection cases, and two regression guards
pinned by name (`unknown_tool_name_is_dropped`,
`mid_prose_xml_wrapper_does_not_match`).
2. WolfAgents same recovery path
`src/wolfagents/agent_loop.rs::openai_tool_loop` had the same
blind spot — model's content-side tool calls returned to the
user as raw JSON. Recovery synthesises into the existing
`tool_calls_json` so the rest of the multi-round loop works
unchanged. Tool-call IDs use `call_{:016x}` (timestamp + counter
+ agent-id-length mix) so a future swap to OpenAI proper, which
validates the ID shape, doesn't reject the recovered turn's
history.
3. AI chat bubble visibility on restart (Gary KO4BSR)
`web/index.html`'s page-load visibility check only inspected
`has_claude_key || has_gemini_key` — Local / OpenAI / OpenRouter
users lost the red AI bubble every wolfstack restart and had to
re-save Settings → AI Agent to bring it back (the Save handler
force-shows it). Now checks all five provider fields.
Diagnostic logging
- `call_local`: previously-debug-only logs escalated to info/warn
so the next "local AI not responding" report has actionable
signal at default log levels.
- Empty-response error message now includes finish_reason +
body_size so context-overflow on small models is identifiable
from the UI.
- Body-size measurement at debug level (RUST_LOG=wolfstack::ai=debug
to enable) — surfaces the #1 small-model failure mode.
Independent code-reviewer pass: two BLOCKERs (mid-prose XML match,
unrestricted tool-name acceptance) + four MAJORs (synthetic ID
format, fenced-block stripping, info-log flooding) all addressed
before commit. All findings have negative-case regression tests
pinning the fix.1 parent 99cd14e commit 3c9d3c4
4 files changed
Lines changed: 440 additions & 20 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
0 commit comments