Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/app/service/ingredient_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def parseNLPSingle(ingredient: str) -> IngredientParsingResult:
def parseLLM(
ingredients: list[str], targetLanguageCode: str | None = None
) -> list[IngredientParsingResult] | None:
if not LLM_MODEL:
return None

systemMessage = """
You are a tool that returns only JSON in the form of [{"name": name, "description": description}, ...]. Split every string from the list into these two properties. You receive recipe ingredients and fill the name field with the singular name of the ingredient and everything else is the description. Translate the response into the specified language.

Expand Down
196 changes: 196 additions & 0 deletions backend/app/service/recipe_public_scraping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import json
import re
import os

from litellm import completion
from recipe_scrapers import scrape_html
from recipe_scrapers._exceptions import SchemaOrgException
from app.service.ingredient_parsing import parseIngredients
from app.models import Recipe, Item, Household
from app.config import SUPPORTED_LANGUAGES

LLM_MODEL = os.getenv("LLM_MODEL")
LLM_API_URL = os.getenv("LLM_API_URL")


def scrapeHTML(url: str, html: str, household: Household) -> dict | None:
try:
scraper = scrape_html(html, url, supported_only=False)
except Exception:
return None
recipe = Recipe()
try:
recipe.name = scraper.title().strip()[:128]
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
return None # Unsupported if title cannot be scraped
try:
recipe.time = int(scraper.total_time())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
recipe.cook_time = int(scraper.cook_time())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
recipe.prep_time = int(scraper.prep_time())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
yields = re.search(r"\d*", scraper.yields())
if yields:
recipe.yields = int(yields.group())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
description = ""
try:
description = scraper.description() + "\n\n"
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
description = description + scraper.instructions()
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
recipe.description = description
recipe.photo = scraper.image()
recipe.source = url
items = {}
for ingredient in parseIngredients(scraper.ingredients(), household.language):
name = ingredient.name if ingredient.name else ingredient.originalText or ""
item = Item.find_name_starts_with(household.id, name)
if item:
items[ingredient.originalText] = item.obj_to_dict() | {
"description": ingredient.description,
"optional": False,
}
else:
items[ingredient.originalText] = None
return {
"recipe": recipe.obj_to_dict(),
"items": items,
}


def scrapeHTMLLLM(url: str, html: str, household: Household) -> dict | None:
if not LLM_MODEL:
return None

systemMessage = """
You are a tool that returns only JSON and nothing else. You get a html page of a recipe as an input. You return the json in the form {"name": "recipe name", "photo": "url to photo", "description": "description with instructions in markdown", "yields": "servings count", "time": integer total cooking time in minutes, "ingredients" ["list of string ingrediends including amount name and description"]}.

For example a result in English:
{
"name": "Pumpkin Soup",
"photo": "https://recipes.com/photos/1283010293.jpg",
"description": "Delicious soup.
- First wash the pumpkin
- Cut Pumpkin
- Make soup",
"yields": 4,
"time": 20,
"ingredients": [
"1 Pumpkin",
"50g red Onions"
]
}

Return only JSON and nothing else.
"""

messages = [
{
"role": "system",
"content": systemMessage,
}
]
if household.language in SUPPORTED_LANGUAGES:
messages.append(
{
"role": "user",
"content": f"Translate the response to {SUPPORTED_LANGUAGES[household.language]}. Translate the JSON content to {SUPPORTED_LANGUAGES[household.language]}. Your target language is {SUPPORTED_LANGUAGES[household.language]}. Respond in {SUPPORTED_LANGUAGES[household.language]} from the start.",
}
)

messages.append(
{
"role": "user",
"content": html,
}
)

response = completion(
model=LLM_MODEL,
api_base=LLM_API_URL,
# response_format={"type": "json_object"},
messages=messages,
)

print(response.choices[0].message.content)
try:
llmResponse = json.loads(response.choices[0].message.content)
except Exception as e:
print(e)
return None

items = {}
for ingredient in parseIngredients(llmResponse["ingredients"], household.language):
name = ingredient.name if ingredient.name else ingredient.originalText or ""
item = Item.find_name_starts_with(household.id, name)
if item:
items[ingredient.originalText] = item.obj_to_dict() | {
"description": ingredient.description,
"optional": False,
}
else:
items[ingredient.originalText] = None

return {
"recipe": llmResponse
| {
"id": None,
"public": False,
"source": url,
},
"items": items,
}
109 changes: 7 additions & 102 deletions backend/app/service/recipe_scraping.py
Original file line number Diff line number Diff line change
@@ -1,119 +1,24 @@
import re
from typing import Any
from recipe_scrapers import scrape_html
from recipe_scrapers._exceptions import SchemaOrgException
import recipe_scrapers.__version__
import requests
from app.config import FRONT_URL
from app.errors import ForbiddenRequest
from app.models.recipe import RecipeVisibility
from app.service.ingredient_parsing import parseIngredients

from app.models import Recipe, Item, Household
from app.models import Recipe, Household
from app.service.recipe_public_scraping import scrapeHTML, scrapeHTMLLLM

# taken from the recipe-scrapers library to circumvent anti-scraping measures that block requests with the default user agent
HEADERS = {
"User-Agent": f"Mozilla/5.0 (compatible; Windows NT 10.0; Win64; x64; rv:{recipe_scrapers.__version__}) recipe-scrapers/{recipe_scrapers.__version__}",
}


def scrapePublic(url: str, html: str, household: Household) -> dict[str, Any] | None:
try:
scraper = scrape_html(html, url, supported_only=False)
except Exception:
return None
recipe = Recipe()
try:
recipe.name = scraper.title().strip()[:128]
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
return None # Unsupported if title cannot be scraped
try:
recipe.time = int(scraper.total_time())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
recipe.cook_time = int(scraper.cook_time())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
recipe.prep_time = int(scraper.prep_time())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
yields = re.search(r"\d*", scraper.yields())
if yields:
recipe.yields = int(yields.group())
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
description = ""
try:
description = scraper.description() + "\n\n"
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
try:
description = description + scraper.instructions()
except (
NotImplementedError,
ValueError,
TypeError,
AttributeError,
SchemaOrgException,
):
pass
recipe.description = description
recipe.photo = scraper.image()
recipe.source = url
items = {}
for ingredient in parseIngredients(scraper.ingredients(), household.language):
name = ingredient.name if ingredient.name else ingredient.originalText or ""
item = Item.find_name_starts_with(household.id, name)
if item:
items[ingredient.originalText] = item.obj_to_dict() | {
"description": ingredient.description,
"optional": False,
}
else:
items[ingredient.originalText] = None
return {
"recipe": recipe.obj_to_dict(),
"items": items,
}
def scrapePublic(url: str, html: str, household: Household) -> dict | None:
res = scrapeHTML(url, html, household)
if not res:
res = scrapeHTMLLLM(url, html, household)
return res


def scrapeLocal(recipe_id: int, household: Household):
Expand Down
Loading