diff --git a/core/archipelago/src/assistant/mod.rs b/core/archipelago/src/assistant/mod.rs index 7b32dadc..d241f8e8 100644 --- a/core/archipelago/src/assistant/mod.rs +++ b/core/archipelago/src/assistant/mod.rs @@ -771,18 +771,31 @@ instead of fabricating a tool call.\n\n\ That refusal rule is about ACTIONS ON THIS NODE, not about conversation. You are also the \ owner's general assistant: answer ordinary questions, explain things, and give recommendations \ (films, music, podcasts, books, reading) from your own knowledge, exactly as a capable \ -assistant would. Those answers are what the content surfaces in this app render as cards, so \ -declining to answer them leaves the owner staring at an empty panel. Only a request to CHANGE \ -or READ something on the node needs a tool — and if no tool covers it, say so plainly rather \ -than pretending. Never let a general question be refused merely because no tool matches it.\n\n\ +assistant would. Only a request to CHANGE or READ something on the node needs a tool — and if \ +no tool covers it, say so plainly rather than pretending. Never let a general question be \ +refused merely because no tool matches it.\n\n\ When the question is about what actually EXISTS here — this node's own shared files, a peer's \ catalogue, purchased items, the film catalogue — call the content tool rather than answering \ from memory or from earlier in this conversation, and call it once per place the question spans \ -(asking about \"films\" covers both the catalogue and the peers). The app renders whatever that \ -tool returns as cards beside your reply, so your text should introduce and summarise the result \ -— how many, anything notable — rather than re-listing every title, size and price in prose. A \ -long enumeration duplicates the cards the operator is already looking at. If the tool comes \ -back empty, say so plainly; do not fill the gap with remembered or invented items."; +(asking about \"films\" covers both the catalogue and the peers). The same applies to \ +RECOMMENDATIONS of a kind this node could hold: \"recommend me ten sci-fi films\" gets a \ +catalogue-and-peers check FIRST, then your own picks on top. And never end a turn by offering \ +to check (\"would you like me to look?\") — looking costs one tool call; do it, then answer \ +with what you found. The app renders whatever the tool returns as cards beside your reply, so \ +your text should introduce and summarise the result — how many, anything notable — rather than \ +re-listing every title, size and price in prose. If the tool comes back empty, say so plainly; \ +do not present remembered items as if they were found here.\n\n\ +Recommendations from your own knowledge are still wanted, and the app renders each one as a \ +rich preview card when you tag it. Put the tag on the SAME line as the title's one-line \ +reason, in EXACTLY these formats:\n\ +- [[film_ext:Title|Year|Director]] for a film, e.g. [[film_ext:Arrival|2016|Denis Villeneuve]]\n\ +- [[tv_ext:Title|Year|Creator]] for a TV series\n\ +- [[song_ext:Title|Artist|Year]] for a song\n\ +- [[book_ext:Title|Author|Year]] for a book\n\ +- [[podcast_ext:Title|Host|Year]] for a podcast\n\ +Tag only specific, real titles you are confident about — never invent a title to fill a tag, \ +and never tag a title the tool already returned (its card is already on screen). An untagged \ +recommendation renders as plain text and is easily missed."; pub fn build_system_prompt(visible_tools: &[tools::ToolDef]) -> String { let mut prompt = String::from(SYSTEM_PROMPT_PREAMBLE); @@ -1228,6 +1241,35 @@ mod tests { ); } + /// Operator report 2026-08-07: "recommend me 10 scifi films" answered + /// with a prose-only knowledge list and an OFFER to check the + /// catalogue — the model classified a recommendation as pure + /// conversation, never called the content tool, and the reply carried + /// none of the preview tags the iframe renders as cards. The preamble + /// must pin both behaviors: check-first on node-holdable + /// recommendations, and the exact tag formats for knowledge picks. + #[test] + fn system_prompt_teaches_discovery_first_and_preview_tags() { + let prompt = build_system_prompt(&[]); + assert!( + prompt.contains("catalogue-and-peers check FIRST"), + "prompt must order a catalogue/peers check before freestyle recommendations" + ); + assert!( + prompt.contains("never end a turn by offering to check"), + "prompt must ban the 'would you like me to look?' stall" + ); + for tag in [ + "[[film_ext:Title|Year|Director]]", + "[[tv_ext:Title|Year|Creator]]", + "[[song_ext:Title|Artist|Year]]", + "[[book_ext:Title|Author|Year]]", + "[[podcast_ext:Title|Host|Year]]", + ] { + assert!(prompt.contains(tag), "prompt must teach {tag}"); + } + } + /// D-16: revoking a category takes effect on the very next /// `granted_categories` resolution — not only on the next session / /// process restart.