fix: catch the more common tool-skip case — short reply, no tool call (v0.57.2)

The previous fix's looksLikeUnstructuredRecipe only fires when the model
spells the whole recipe out as a numbered/bulleted list — it does nothing
for a short reply that just *claims* a draft exists ("Voici une recette
de Bœuf Bourguignon... brouillon à vérifier ci-dessous") without ever
calling the tool, which is what the user actually hit.

Detecting the model's claimed compliance is fragile (varies by phrasing/
model), so instead detect intent from the user's own message — the same
noun+verb pattern ("recipe"+create/make/... or "recette"+créer/fais/...)
the system prompt's own createRecipe examples already describe. Forces
the retry whenever there's no tool call AND either signal fires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-20 19:46:52 +02:00
parent 6ec9b65c24
commit f6214e60df
5 changed files with 41 additions and 7 deletions
+5
View File
@@ -2,6 +2,11 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
## 0.57.2 — 2026-07-20 20:15
### Fixed
- The previous chatbot fix only caught the model spelling a recipe out as prose. The more common case: a short reply that claims a draft exists ("here's a draft, check below") without ever calling the tool, so nothing renders. Now also detects recipe-creation intent from the user's own message and forces the tool call in that case too.
## 0.57.1 — 2026-07-20 20:00
### Fixed
+26 -4
View File
@@ -37,6 +37,24 @@ function looksLikeUnstructuredRecipe(text: string): boolean {
return listLines.length >= 3;
}
// The above only catches the model spelling the whole recipe out. A second,
// more common failure with weaker models: it gives a short reply that even
// *claims* to have drafted something ("here's a draft, check below") without
// ever calling the tool — so nothing renders below it. Rather than guess from
// that claim (fragile across models/phrasing), detect intent from the user's
// own message instead: same noun+verb pattern the system prompt itself uses
// as its createRecipe examples, checked in whichever of the two supported
// locales the request is in.
const RECIPE_INTENT: Record<string, { noun: RegExp; verb: RegExp }> = {
en: { noun: /\brecipe\b/i, verb: /\b(create|make|generate|give|write|save|invent)\b/i },
fr: { noun: /\brecettes?\b/i, verb: /\b(cr[ée]e?r?|fais|donne|[ée]cri[st]|note[rz]?|sauvegard\w*|enregistr\w*|invente\w*|g[ée]n[èe]re\w*)\b/i },
};
function looksLikeRecipeCreationRequest(question: string, locale: string): boolean {
const pattern = RECIPE_INTENT[locale] ?? RECIPE_INTENT["en"]!;
return pattern.noun.test(question) && pattern.verb.test(question);
}
export async function POST(req: NextRequest) {
const { session, response } = await requireSessionOrApiKey(req);
if (response) return response;
@@ -77,10 +95,14 @@ export async function POST(req: NextRequest) {
let final = result.data;
// Weaker/free-tier models sometimes ignore the MUST-call-the-tool rule
// above and just write the recipe out in prose instead. Rather than
// surface that (or fail the request), force the tool call on one extra
// pass — same system+prompt, just no longer letting the model opt out.
if (final.toolCalls.length === 0 && looksLikeUnstructuredRecipe(final.text)) {
// above — either by spelling the recipe out in prose, or (more common,
// and easy to miss) giving a short reply that *claims* a draft exists
// without ever calling the tool, so nothing actually renders below it.
// Catch both: the prose-list shape, or the user's own message clearly
// asking for a recipe in the first place. Rather than surface either
// failure, force the tool call on one extra pass — same system+prompt,
// just no longer letting the model opt out.
if (final.toolCalls.length === 0 && (looksLikeUnstructuredRecipe(final.text) || looksLikeRecipeCreationRequest(parsed.data.question, locale))) {
try {
const forced = await generateText({
model,
+8 -1
View File
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
export const APP_VERSION = "0.57.1";
export const APP_VERSION = "0.57.2";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
{
version: "0.57.2",
date: "2026-07-20 20:15",
fixed: [
"The previous chatbot fix only caught the model spelling a recipe out as prose. The more common case: a short reply that claims a draft exists (\"here's a draft, check below\") without ever calling the tool, so nothing renders. Now also detects recipe-creation intent from the user's own message and forces the tool call in that case too.",
],
},
{
version: "0.57.1",
date: "2026-07-20 20:00",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
"version": "0.57.1",
"version": "0.57.2",
"private": true,
"scripts": {
"dev": "next dev",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "epicure",
"version": "0.57.1",
"version": "0.57.2",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",