feat(redstone): wiki_recipe tool — extract recipe + thumbnail in one call

Adds a new MCP tool that parses minecraft.wiki's <table data-description=
"Crafting recipes"> directly, pulling the ingredients column out of each
row (e.g. "Block of Iron + Iron Ingot" for Anvil), then chases up the
page's main item thumbnail via prop=pageimages so the bot can deliver
text + a real image in one tool round.

Why a dedicated tool: minecraft.wiki does not host standalone "recipe.png"
files — recipes are rendered live as HTML grids composed from individual
BlockSprite icons. Filtering wiki_page_images for 'recipe' / 'craft' /
'grid' returned empty for every craftable item, leaving the model with
nothing to call tg_send_photo with. Confirmed by inspecting the Anvil
page's image inventory (Anvil1-6.png + GUI + sound files, no recipe).

Tool returns {
  title, page_url,
  recipes: [{ ingredients: "…" }, …],   // strips <br>/<a>/&nbsp; cleanly
  thumbnail_url,                          // for tg_send_photo
  note?,                                  // present when no recipe rows
}

Persona prompt tightened:
- wiki_recipe is now the FIRST call for "how do I craft X" / "recipe for X"
- wiki_page_images is downgraded to "specific non-thumbnail images only"
- IMAGE DELIVERY MANDATORY clause: must end the turn with tg_send_photo
  using a real URL or say plainly that no thumbnail was found — no more
  describing-the-image-in-prose-and-stopping failures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 23:47:25 +02:00
parent 8ae89610f3
commit f1d4fb596a
2 changed files with 75 additions and 3 deletions

View File

@@ -1209,9 +1209,11 @@ function redstonePersona(user: User, chatType: string, chatId: number, srv: stri
"- Telegram chat: send messages/polls/reactions/dice/stickers/photos, edit/pin/delete your own messages, query chat info (tg_*).",
"- 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.",
"- 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.",
"- Minecraft wiki: look things up on minecraft.wiki and surface images into the chat.",
"- For 'how do I craft X?' / 'recipe for X' questions: call wiki_recipe(title=…) directly (use wiki_search first only if you're unsure of the exact page title). wiki_recipe returns the ingredient list (e.g. 'Block of Iron + Iron Ingot') AND the page's main item thumbnail URL. Then call tg_send_photo with that thumbnail_url and a caption like 'Anvil — Block of Iron ×3 + Iron Ingot' — visual + the recipe in one go. Minecraft Wiki does NOT host standalone recipe images (recipes are rendered live as HTML grids), so don't waste a turn calling wiki_page_images looking for one.",
"- For 'what is X?' / general lookup: wiki_page for the intro extract + thumbnail. Then a short text reply, optionally + tg_send_photo of the thumbnail.",
"- For specific images that aren't the page's main thumbnail (block variants, GUI screenshots, charts): wiki_page_images with an optional filename filter.",
"- IMAGE DELIVERY IS MANDATORY when the user asks for a picture / image / recipe / what something looks like. You MUST end the turn by calling tg_send_photo with a real URL — never describe the image in words and stop. If wiki_recipe / wiki_page returns no thumbnail at all, say so plainly instead of inventing one.",
"- 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:",

View File

@@ -102,6 +102,76 @@ export const minecraftWikiTools: Tool<z.ZodRawShape>[] = [
},
}),
tool({
name: "wiki_recipe",
title: "Get a crafting recipe from the Minecraft wiki",
description:
"Extracts the crafting recipe ingredient list from a wiki page (parses the live recipe table — minecraft.wiki renders recipes as HTML grids, not standalone images, so wiki_page_images is the wrong tool for recipe queries). Returns the ingredients string for each recipe variant, plus the page's main item thumbnail URL to send with tg_send_photo. Use this whenever a user asks 'how do I craft X' or 'recipe for X'.",
parameters: {
title: z.string().min(1).describe("Exact wiki page title — e.g. 'Anvil', 'Beacon', 'Crafting Table'. Use wiki_search first if unsure."),
},
handler: async ({ title }) => {
// 1. Parsed HTML — the recipe lives in a wikitable with data-description="Crafting recipes"
type ParseRes = { parse?: { title?: string; text?: string } };
const data = await wikiGet<ParseRes>({
action: "parse",
page: title,
prop: "text",
redirects: "1",
disableeditsection: "1",
});
const html = data.parse?.text ?? "";
if (!html) return { error: `no wiki page titled '${title}'` };
const resolvedTitle = data.parse?.title ?? title;
// 2. Find each crafting-recipes table and pull the first <td> (ingredients) of each data row.
const stripHtml = (s: string) =>
s.replace(/<br\s*\/?>/gi, " + ")
.replace(/<[^>]+>/g, "")
.replace(/&#160;|&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/\s*\+\s*/g, " + ")
.replace(/\s+/g, " ")
.trim();
const tableRe = /<table\b[^>]*data-description="Crafting recipes"[^>]*>([\s\S]*?)<\/table>/g;
const recipes: { ingredients: string }[] = [];
let m: RegExpExecArray | null;
while ((m = tableRe.exec(html)) !== null) {
const rowRe = /<tr\b[^>]*>([\s\S]*?)<\/tr>/g;
let r: RegExpExecArray | null;
while ((r = rowRe.exec(m[1])) !== null) {
if (/<th\b/.test(r[1])) continue;
const td = /<td\b[^>]*>([\s\S]*?)<\/td>/.exec(r[1]);
if (!td) continue;
const text = stripHtml(td[1]);
if (text) recipes.push({ ingredients: text });
}
}
// 3. Pull the page thumbnail in a second query — gives the bot a URL to send.
type ThumbRes = { query?: { pages?: { title: string; thumbnail?: { source: string } }[] } };
const meta = await wikiGet<ThumbRes>({
action: "query",
prop: "pageimages",
titles: resolvedTitle,
piprop: "thumbnail",
pithumbsize: 600,
redirects: "1",
});
const thumbnail = meta.query?.pages?.[0]?.thumbnail?.source ?? null;
return {
title: resolvedTitle,
recipes, // [{ ingredients: "Block of Iron + Iron Ingot" }, ...]
thumbnail_url: thumbnail, // page's main item image — pass to tg_send_photo
page_url: pageUrl(resolvedTitle),
note: recipes.length === 0
? "No recipe table on this page — the item may be uncraftable, found only via loot/structure, or listed under a different page."
: undefined,
};
},
}),
tool({
name: "wiki_page_images",
title: "List images on a Minecraft wiki page (with full URLs)",