fix(recipes): normalize newlines + add update_recipe (v0.6.1)
Bug fix: literal backslash-n sequences in ingredients/instructions are now converted to real newline characters before sending to the API, so the server correctly splits ingredient lines into ingredientsList[]. New tool: update_recipe — partial update via mprecipeput with recipe.metaId; fetches current recipe in the same session to verify can_update and supply name fallback. Verified: recipe.metaId triggers update (not create). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,14 @@
|
||||
|
||||
Verified endpoints (2026-04-16 via FW_DEBUG=1):
|
||||
- Create: POST mprecipeput — params use 'recipe.' prefix (recipe.name, etc.)
|
||||
- Update: POST mprecipeput with recipe.metaId — same prefix convention
|
||||
- Read all: POST metasync with id='recipe' — response at a00.r.r.updatedCreated[]
|
||||
- Delete: POST metadelete with id=<recipe_metaId>
|
||||
|
||||
Newline normalisation (bug fix v0.6.1):
|
||||
LLM tool calls may pass literal backslash-n ('\\n', two chars) instead of a
|
||||
real newline character. _normalize_newlines() converts them before sending
|
||||
to the API so the server splits ingredients/instructions correctly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,6 +17,23 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _normalize_newlines(text: str) -> str:
|
||||
"""Replace literal backslash-n sequences with real newline characters.
|
||||
|
||||
When an LLM generates a tool call it may produce ``"line1\\nline2"``
|
||||
(two chars: backslash + n) instead of ``"line1\\nline2"`` (real newline).
|
||||
The Family Wall API splits ingredients and instructions on real newlines
|
||||
only, so we normalise before sending.
|
||||
|
||||
Args:
|
||||
text: Free-text string that may contain literal ``\\n`` sequences.
|
||||
|
||||
Returns:
|
||||
The same string with every ``\\n`` replaced by a real ``\\n``.
|
||||
"""
|
||||
return text.replace("\\n", "\n")
|
||||
|
||||
|
||||
def parse_recipe_summary(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Extract a compact recipe summary from a raw API recipe object.
|
||||
|
||||
@@ -86,11 +109,17 @@ def build_create_params(
|
||||
The Family Wall API requires the 'recipe.' prefix for all recipe fields.
|
||||
isRecipe='true' is always required.
|
||||
|
||||
Newline normalisation is applied to ingredients and instructions so that
|
||||
literal ``\\n`` sequences sent by an LLM are converted to real newlines,
|
||||
which the server uses to split the ingredient list.
|
||||
|
||||
Args:
|
||||
name: Recipe title (required).
|
||||
description: Optional description.
|
||||
ingredients: Optional free-text ingredients, lines separated by \\n.
|
||||
instructions: Optional free-text cooking instructions, lines separated by \\n.
|
||||
ingredients: Optional free-text ingredients, lines separated by ``\\n``.
|
||||
Literal backslash-n sequences are normalised to real newlines.
|
||||
instructions: Optional free-text cooking instructions, lines separated
|
||||
by ``\\n``. Literal backslash-n sequences are normalised.
|
||||
prep_time_minutes: Optional preparation time in minutes.
|
||||
cook_time_minutes: Optional cooking time in minutes.
|
||||
serves: Optional number of servings.
|
||||
@@ -106,9 +135,69 @@ def build_create_params(
|
||||
if description is not None:
|
||||
params["recipe.description"] = description
|
||||
if ingredients is not None:
|
||||
params["recipe.ingredients"] = ingredients
|
||||
params["recipe.ingredients"] = _normalize_newlines(ingredients)
|
||||
if instructions is not None:
|
||||
params["recipe.instructions"] = instructions
|
||||
params["recipe.instructions"] = _normalize_newlines(instructions)
|
||||
if prep_time_minutes is not None:
|
||||
params["recipe.prepTime"] = str(prep_time_minutes)
|
||||
if cook_time_minutes is not None:
|
||||
params["recipe.cookTime"] = str(cook_time_minutes)
|
||||
if serves is not None:
|
||||
params["recipe.serves"] = str(serves)
|
||||
if url is not None:
|
||||
params["recipe.url"] = url
|
||||
return params
|
||||
|
||||
|
||||
def build_update_params(
|
||||
recipe_id: str,
|
||||
current_name: str,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
ingredients: str | None = None,
|
||||
instructions: str | None = None,
|
||||
prep_time_minutes: int | None = None,
|
||||
cook_time_minutes: int | None = None,
|
||||
serves: int | None = None,
|
||||
url: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Build the form parameters for a mprecipeput update call.
|
||||
|
||||
Identical to :func:`build_create_params` except that ``recipe.metaId`` is
|
||||
included so the API updates the existing recipe instead of creating a new one.
|
||||
``recipe.name`` and ``recipe.isRecipe`` are always present — the name falls
|
||||
back to *current_name* when the caller does not provide a new one.
|
||||
|
||||
Newline normalisation is applied to ingredients and instructions (see
|
||||
:func:`_normalize_newlines`).
|
||||
|
||||
Args:
|
||||
recipe_id: metaId of the recipe to update (e.g. ``"recipe/123_456"``).
|
||||
current_name: The recipe's existing name, used as fallback when *name*
|
||||
is not provided. Fetched from the API before calling this function.
|
||||
name: New recipe title (omit to keep existing).
|
||||
description: New description (omit to keep existing).
|
||||
ingredients: New ingredients text (omit to keep existing).
|
||||
instructions: New instructions text (omit to keep existing).
|
||||
prep_time_minutes: New preparation time in minutes (omit to keep existing).
|
||||
cook_time_minutes: New cooking time in minutes (omit to keep existing).
|
||||
serves: New number of servings (omit to keep existing).
|
||||
url: New external URL (omit to keep existing).
|
||||
|
||||
Returns:
|
||||
Dict of form parameters ready to send to mprecipeput.
|
||||
"""
|
||||
params: dict[str, str] = {
|
||||
"recipe.metaId": recipe_id,
|
||||
"recipe.name": name if name is not None else current_name,
|
||||
"recipe.isRecipe": "true",
|
||||
}
|
||||
if description is not None:
|
||||
params["recipe.description"] = description
|
||||
if ingredients is not None:
|
||||
params["recipe.ingredients"] = _normalize_newlines(ingredients)
|
||||
if instructions is not None:
|
||||
params["recipe.instructions"] = _normalize_newlines(instructions)
|
||||
if prep_time_minutes is not None:
|
||||
params["recipe.prepTime"] = str(prep_time_minutes)
|
||||
if cook_time_minutes is not None:
|
||||
|
||||
Reference in New Issue
Block a user