Skip to content

Commit

Permalink
AI chat preview (#333)
Browse files Browse the repository at this point in the history
* Initial code for a ai chat feature

* Working chat with assistant

* Try to fix build

* Fixed api tests

* Fixed tests

* hide AI chat until preview option is selected
  • Loading branch information
jlucaspains authored Sep 7, 2024
1 parent b02c348 commit 46c2439
Show file tree
Hide file tree
Showing 17 changed files with 665 additions and 124 deletions.
12 changes: 4 additions & 8 deletions api/parse_recipe/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import logging
import json
import re
import requests

import azure.functions as func
from contextlib import suppress

from recipe_scrapers import scrape_me, _abstract
from pint import UnitRegistry
from uuid import uuid4
from time import perf_counter

from ..util import parse_recipe_ingredient, parse_recipe_instruction, parse_recipe_image

_abstract.HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/123.0"
}
from ..util import parse_recipe_ingredient, parse_recipe_instruction, get_recipe_image, get_html

ureg = UnitRegistry()

Expand All @@ -27,7 +23,7 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
download_image: bool = req_body.get("downloadImage") or False
try:
logging.info(f"processing parse request id {correlation_id} for url: {url}")
scraper = scrape_me(url, wild_mode=True)
scraper = get_html(url)

lang = scraper.language() or "en"

Expand All @@ -50,7 +46,7 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
with suppress(NotImplementedError):
result["nutrients"] = parse_nutrients(scraper.nutrients())

result["image"] = parse_recipe_image(result["image"]) if download_image else result["image"]
result["image"] = get_recipe_image(result["image"]) if download_image else result["image"]

return func.HttpResponse(json.dumps(result), status_code=200, mimetype="application/json")
except Exception as e:
Expand Down
4 changes: 2 additions & 2 deletions api/receive_recipe/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json

import azure.functions as func
from azure.cosmos import errors
from azure.cosmos import exceptions

from uuid import uuid4
from time import perf_counter
Expand Down Expand Up @@ -42,7 +42,7 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
result = json.dumps(item_clean)

return func.HttpResponse(result, status_code=200, mimetype="application/json")
except errors.CosmosResourceNotFoundError as e:
except exceptions.CosmosResourceNotFoundError as e:
logging.error(f"Failed to process share request id {correlation_id}. The id provided does not exist.")

return func.HttpResponse("Could not receive the recipe because it does not exist.", status_code=404)
Expand Down
4 changes: 2 additions & 2 deletions api/test/test_receive_recipe.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import os
import azure.functions as func
from azure.cosmos import errors
from azure.cosmos import exceptions

from unittest import mock;

Expand Down Expand Up @@ -54,7 +54,7 @@ def test_receive_recipe_not_found():
)

repository = mock.MagicMock()
repository.read_item.side_effect = mock.Mock(side_effect=errors.CosmosResourceNotFoundError(404))
repository.read_item.side_effect = mock.Mock(side_effect=exceptions.CosmosResourceNotFoundError(404))

mock_repository(repository)

Expand Down
31 changes: 27 additions & 4 deletions api/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import io
from zipfile import ZipFile
from recipe_scrapers import scrape_me
from fractions import Fraction
import re
import requests
Expand All @@ -9,6 +8,7 @@
from PIL import Image
from pint import UnitRegistry
import pillow_avif
from recipe_scrapers import scrape_html, AbstractScraper

def parse_recipe_ingredients(text: str, ureg: UnitRegistry):
"""Parses a recipe collection of ingredientes that are formatted in a single string separated by \n
Expand Down Expand Up @@ -154,13 +154,36 @@ def parse_recipe_instruction(text: str, lang: str):

return { "raw": text, "minutes": minutes }

def parse_recipe_image(image_url: str):
request_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Priority": "u=0;i",
"Sec-Fetch-Ua": '"Microsoft Edge";v="129", "Not=A?Brand";v="8", "Chromium";v="129"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": "Linux",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Upgrade-Insecure-Requests": "1"
}

def get_recipe_image(image_url: str):
"""Pulls an image from a web server and formats the result in URI and base64
Args:
image_url (str): URL of the image to pull
Returns:
str: URI in base64
"""
response = requests.get(image_url)
response = requests.get(image_url, headers=request_headers)
parsedImage = parse_image(response.url, response.content, False, response.headers['Content-Type'])
return parsedImage
return parsedImage


def get_html(url: str) -> AbstractScraper:
html = requests.get(url, headers=request_headers).content

return scrape_html(html, url, wild_mode=True)
26 changes: 21 additions & 5 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,23 @@
"storageStats": "Storage",
"storageNotTraceable": "Storage cannot be tracked on this device",
"storageDescription": "Space used {{used}} of {{remaining}} available.",
"enableRecipeHighlighting": "Enable recipe highlighting",
"enableRecipeHighlightingDescription": "Highlight ingredient quantities and UOMs and step details",
"enableNutritionFacts": "Enable nutrition labels (preview)",
"previewFeatures": "Preview features",
"previewFeaturesDescription": "Review and enable features that are still in development and may not work as expected."
},
"preview-features": {
"title": "Preview Features",
"enableNutritionFacts": "Enable nutrition labels",
"enableNutritionFactsDescription": "Allows for recipes imported or inputted with nutrition facts to display a nutrition facts label.",
"enableRecipeLanguageSwitcher": "Enable recipe language switcher",
"enableRecipeLanguageSwitcherDescription": "Allows for selecting which language a recipe is written in. This directly influences the parsing of recipe ingredients and steps."
"enableRecipeLanguageSwitcherDescription": "Allows for selecting which language a recipe is written in. This directly influences the parsing of recipe ingredients and steps.",
"enableAiChat": "Enable AI Chat",
"enableAiChatDescription": "Allows for interacting with an AI assistant to get recipe insights and other cooking related information.",
"aiChatBaseUrl": "AI Chat Base URL",
"aiChatBaseUrlDescription": "The base URL for the AI Chat service. Must support Open AI style HTTP API.",
"aiAuthorizationHeader": "AI Chat Authorization Header",
"aiAuthorizationHeaderDescription": "The authorization header for the AI Chat service. Typically: Bearer <token>",
"aiModelName": "AI Model Name",
"aiModelNameDescription": "The name of the OpenAI model to use for the AI Chat service."
},
"recipe": {
"id": {
Expand Down Expand Up @@ -166,7 +177,8 @@
"multiplierTooltip": "Set multiplier",
"startTimeTooltip": "Set start time",
"printTooltip": "Print",
"noNutritionFacts": "This recipe does not have nutrition facts available."
"noNutritionFacts": "This recipe does not have nutrition facts available.",
"chatWithAssistant": "Chat with Assistant"
},
"print": {
"action": "Print"
Expand Down Expand Up @@ -229,6 +241,10 @@
"tipContent": "Ensure to separate individual ingredients and steps with an empty line.",
"addContentTo": "Add content to:"
}
},
"chat": {
"abortError": "Ok. I won't answer that question.",
"unableToAnswer": "I'm sorry, I'm unable to answer that question right now."
}
},
"initialRecipes": [
Expand Down
20 changes: 17 additions & 3 deletions public/locales/pt/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,22 @@
"storageStats": "Armazenamento",
"storageNotTraceable": "O armazenamento não pode ser rastreado neste dispositivo.",
"storageDescription": "Espaço usado {{used}} de {{remaining}} disponível.",
"enableRecipeHighlighting": "Ativar o realce da receita",
"enableRecipeHighlightingDescription": "Realce quantidades de ingredientes, unidades de medida e detalhes da etapa",
"previewFeatures": "Recursos de visualização",
"previewFeaturesDescription": "Revisar e habilitar recursos que ainda estão em desenvolvimento e podem não funcionar como esperado."
},
"preview-features": {
"enableNutritionFacts": "Ativar informações nutricionais (preview)",
"enableNutritionFactsDescription": "Permite que receitas importadas ou inseridas com informações nutricionais exibam um rótulo de informações nutricionais.",
"enableRecipeLanguageSwitcher": "Ativar alternador de idioma da receita",
"enableRecipeLanguageSwitcherDescription": "Permite selecionar em qual idioma a receita está escrita. Isso influencia diretamente a análise dos ingredientes e passos da receita."
"enableRecipeLanguageSwitcherDescription": "Permite selecionar em qual idioma a receita está escrita. Isso influencia diretamente a análise dos ingredientes e passos da receita.",
"enableAiChat": "Ativar Chat de IA",
"enableAiChatDescription": "Permite interagir com um assistente de IA para obter insights de receitas e outras informações relacionadas à culinária.",
"aiChatBaseUrl": "URL Base do Chat de IA",
"aiChatBaseUrlDescription": "A URL base para o serviço de Chat de IA. Deve suportar a API HTTP no estilo Open AI.",
"aiAuthorizationHeader": "Cabeçalho de Autorização do Chat de IA",
"aiAuthorizationHeaderDescription": "O cabeçalho de autorização para o serviço de Chat de IA. Normalmente: Bearer <token>",
"aiModelName": "Nome do Modelo de IA",
"aiModelNameDescription": "O nome do modelo OpenAI a ser usado para o serviço de Chat de IA."
},
"recipe": {
"id": {
Expand Down Expand Up @@ -229,6 +239,10 @@
"tipContent": "Garanta que pelo menos uma linha em branco existe entre os ingredientes e passos.",
"addContentTo": "Adicionar contento em:"
}
},
"chat": {
"abortError": "Ok. Eu não vou responder essa pergunta.",
"unableToAnswer": "Desculpe, eu não consigo responder sua pergunta no momento."
}
},
"initialRecipes": [
Expand Down
8 changes: 4 additions & 4 deletions public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/logo2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 7 additions & 6 deletions src/components/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ function goBack() {
<div class="px-1 py-1">
<MenuItem v-slot="{ active }" v-for="child in menuOption.children">
<button @click="child.action" :class="[
active
? 'bg-theme-secondary text-white'
: 'text-gray-900',
'group flex w-full items-center rounded-md px-2 py-2 text-sm',
]">
active
? 'bg-theme-secondary text-white'
: 'text-gray-900',
'group flex w-full items-center rounded-md px-2 py-2 text-sm',
]">
<svg v-if="child.svg" class="h-6 w-6 text-white" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
v-html="child.svg"></svg>
Expand Down Expand Up @@ -187,4 +187,5 @@ function goBack() {
100% {
transform: translate(-50px);
}
}</style>
}
</style>
12 changes: 12 additions & 0 deletions src/helpers/shareHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { t } from "i18next";
import { Recipe } from "../services/recipe";

export function recipeAsText(item: Recipe) {
return `${item.title}
${t("pages.recipe.id.index.ingredients")}:
${item.ingredients.join("\r\n")}
${t("pages.recipe.id.index.instructions")}:
${item.steps.join("\r\n")}`;
}
Loading

0 comments on commit 46c2439

Please sign in to comment.