fix(redstone): force image delivery + raise tool-loop ceiling + log tool calls
Reproduced: "send me the recipe for an anvil" got a TEXT reply describing
the recipe but no image. Three changes to fix this class of problem:
1) Iteration cap 4 → 6. The wiki+photo flow is naturally
wiki_search → wiki_page → wiki_page_images → tg_send_photo → final
text — five tool calls + one text response. With only 4 iterations
the model often ran out before reaching tg_send_photo or before
emitting closing text, returning {text:null, ranTool:true} which
the handler swallows silently.
2) Persona prompt: image delivery is now MANDATORY when the user asks
for a picture / image / recipe / "what it looks like". Added an
explicit fallback chain: filter='recipe' → 'craft' → 'grid' → the
wiki_page thumbnail_url. The previous phrasing was suggestive only,
which let the model answer in pure text.
3) Tool-call tracing. Both dispatchers now log `[redstone:or|gem]
tool -> name args` before each call and `tool <- name ok|err msg`
after. Without this, silent "model called tools but no image
showed up" failures had no log line at all and we had to guess.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
12
bot/bot.ts
12
bot/bot.ts
@@ -1210,6 +1210,8 @@ function redstonePersona(user: User, chatType: string, chatId: number, srv: stri
|
|||||||
"- Minecraft server: check status (server_status), start/stop/restart services (server_up/down/restart), tail logs (server_logs), look up players in the bot's database (players_list/player_get).",
|
"- Minecraft server: check status (server_status), start/stop/restart services (server_up/down/restart), tail logs (server_logs), look up players in the bot's database (players_list/player_get).",
|
||||||
"- Admin-only tools (the caller's role is shown in the context block): run RCON commands (rcon), trigger a world backup (backup), and modify player records (player_upsert/set_*/remove, seen_list). If a non-admin asks for one, politely refuse and don't call it.",
|
"- Admin-only tools (the caller's role is shown in the context block): run RCON commands (rcon), trigger a world backup (backup), and modify player records (player_upsert/set_*/remove, seen_list). If a non-admin asks for one, politely refuse and don't call it.",
|
||||||
"- Minecraft wiki: look things up on minecraft.wiki and surface images into the chat. Usual flow for a 'how do I craft X?' / 'what is X?' question: wiki_search to pick the exact page title → wiki_page for a one-line summary + thumbnail → wiki_page_images with filter='recipe' or 'craft' or 'grid' to find a crafting-grid image, then tg_send_photo to send the image URL straight into the chat (caption it briefly).",
|
"- Minecraft wiki: look things up on minecraft.wiki and surface images into the chat. Usual flow for a 'how do I craft X?' / 'what is X?' question: wiki_search to pick the exact page title → wiki_page for a one-line summary + thumbnail → wiki_page_images with filter='recipe' or 'craft' or 'grid' to find a crafting-grid image, then tg_send_photo to send the image URL straight into the chat (caption it briefly).",
|
||||||
|
"- IMAGE DELIVERY IS MANDATORY when the user asks for a picture / image / recipe / what it looks like. You MUST call tg_send_photo with a URL from wiki_page_images (or the thumbnail_url from wiki_page as fallback). Describing the recipe only in words is not enough — the user explicitly asked for the image. After tg_send_photo succeeds, you can stop; the photo IS the reply.",
|
||||||
|
"- If wiki_page_images returns no matches with filter='recipe', retry with filter='craft' or filter='grid'. If still empty, fall back to wiki_page's thumbnail_url. Always send SOMETHING visual when an image was requested.",
|
||||||
"- When you use a tool, you usually don't need a separate text reply unless you're adding context — the tool's effect IS the response. After a server_status / players_list lookup, summarize the answer in one sentence.",
|
"- When you use a tool, you usually don't need a separate text reply unless you're adding context — the tool's effect IS the response. After a server_status / players_list lookup, summarize the answer in one sentence.",
|
||||||
"",
|
"",
|
||||||
"Stickers — drop emojis inline and the bot does the rest:",
|
"Stickers — drop emojis inline and the bot does the rest:",
|
||||||
@@ -1371,7 +1373,7 @@ async function runRedstoneTurnViaOpenRouter(
|
|||||||
{ role: "user", content: userText },
|
{ role: "user", content: userText },
|
||||||
];
|
];
|
||||||
let ranTool = false;
|
let ranTool = false;
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const data = await callOpenRouterOnce(messages);
|
const data = await callOpenRouterOnce(messages);
|
||||||
if (!data) return null; // hard failure — caller falls back to Gemini
|
if (!data) return null; // hard failure — caller falls back to Gemini
|
||||||
const msg = data.choices?.[0]?.message;
|
const msg = data.choices?.[0]?.message;
|
||||||
@@ -1399,11 +1401,14 @@ async function runRedstoneTurnViaOpenRouter(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
console.info("[redstone:or] tool ->", tc.function.name, JSON.stringify(args).slice(0, 200));
|
||||||
const result = await withSlowToolIndicator(chatId, tc.function.name, () => tool.handler(args, toolCtx));
|
const result = await withSlowToolIndicator(chatId, tc.function.name, () => tool.handler(args, toolCtx));
|
||||||
response = { result };
|
response = { result };
|
||||||
ranTool = true;
|
ranTool = true;
|
||||||
|
console.info("[redstone:or] tool <-", tc.function.name, "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response = { error: (e as Error).message };
|
response = { error: (e as Error).message };
|
||||||
|
console.warn("[redstone:or] tool <-", tc.function.name, "err:", (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
messages.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(response) });
|
messages.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(response) });
|
||||||
@@ -1417,7 +1422,7 @@ async function runRedstoneTurnViaGemini(
|
|||||||
): Promise<TurnResult> {
|
): Promise<TurnResult> {
|
||||||
const contents: GeminiContent[] = [...history, { role: "user", parts: [{ text: userText }] }];
|
const contents: GeminiContent[] = [...history, { role: "user", parts: [{ text: userText }] }];
|
||||||
let ranTool = false;
|
let ranTool = false;
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const data = await callGeminiOnce(contents, systemPrompt);
|
const data = await callGeminiOnce(contents, systemPrompt);
|
||||||
if (!data) return { text: null, ranTool };
|
if (!data) return { text: null, ranTool };
|
||||||
const parts = data.candidates?.[0]?.content?.parts ?? [];
|
const parts = data.candidates?.[0]?.content?.parts ?? [];
|
||||||
@@ -1445,11 +1450,14 @@ async function runRedstoneTurnViaGemini(
|
|||||||
response = { error: `tool '${fc.functionCall.name}' requires admin role; the calling user is '${callerRole}'. Inform the user, do not retry.` };
|
response = { error: `tool '${fc.functionCall.name}' requires admin role; the calling user is '${callerRole}'. Inform the user, do not retry.` };
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
console.info("[redstone:gem] tool ->", fc.functionCall.name, JSON.stringify(fc.functionCall.args).slice(0, 200));
|
||||||
const result = await withSlowToolIndicator(chatId, fc.functionCall.name, () => tool.handler(fc.functionCall.args, toolCtx));
|
const result = await withSlowToolIndicator(chatId, fc.functionCall.name, () => tool.handler(fc.functionCall.args, toolCtx));
|
||||||
response = { result };
|
response = { result };
|
||||||
ranTool = true;
|
ranTool = true;
|
||||||
|
console.info("[redstone:gem] tool <-", fc.functionCall.name, "ok");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response = { error: (e as Error).message };
|
response = { error: (e as Error).message };
|
||||||
|
console.warn("[redstone:gem] tool <-", fc.functionCall.name, "err:", (e as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responseParts.push({ functionResponse: { name: fc.functionCall.name, response } });
|
responseParts.push({ functionResponse: { name: fc.functionCall.name, response } });
|
||||||
|
|||||||
Reference in New Issue
Block a user