Transformar datos en soluciones: crear una aplicación inteligente con Python e IA | de Vianney Mixtur | enero de 2025

En esta sección, compartiré algunos detalles de implementación de Baker. Nuevamente es de código abierto, así que invito a mis lectores técnicos a verificar el código en GitHub. Es posible que algunos lectores quieran saltar a la siguiente sección.

La aplicación es minimalista con una arquitectura simple de 3 niveles y está construida casi en su totalidad en Python.

Un diagrama de arquitectura de la aplicación Baker.
Foto del autor

Está hecho de los siguientes componentes:

  1. Interfaz: A iluminado La interfaz proporciona una plataforma intuitiva para que los usuarios interactúen con el sistema, consulten recetas y reciban recomendaciones.
  2. backend: Construido con API rápidael backend sirve como interfaz para manejar las consultas de los usuarios y entregar recomendaciones.
  3. Motor: El motor contiene la lógica central para buscar y filtrar recetas, aprovechando mestizo como generador de consultas.
  4. Base de datos: Las recetas se guardan en un MongoDB base de datos que procesa los canales de agregación generados por el motor.

Configuración de back-end

El backend se inicializa en app.pydonde se definen los puntos finales de FastAPI. Por ejemplo:

from fastapi import FastAPI
from baker.engine.core import find_recipes
from baker.models.ingredient import Ingredient

app = FastAPI()
@app.get("/")
def welcome():
return {"message": "Welcome to the Baker API!"}
@app.post("/recipes")
def _find_recipes(ingredients: list[Ingredient], serving_size: int = 1) -> list[dict]:
return find_recipes(ingredients, serving_size)

El /recipes El punto final acepta una lista de ingredientes y un tamaño de porción y luego delega el procesamiento al motor.

Lógica del motor de recetas

El corazón de la aplicación reside en core.py dentro del engine directorio. Gestiona conexiones de bases de datos y canales de consultas. A continuación se muestra un ejemplo de la find_recipes función:

# Imports and the get_recipes_collection function are not included

def find_recipes(ingredients, serving_size=1):
# Get the recipes collection
recipes = get_recipes_collection()

# Create the pipeline
pipeline = Pipeline()
pipeline = include_normalization_steps(pipeline, serving_size)
query = generate_match_query(ingredients, serving_size)
print(query)
pipeline.match(query=query).project(
include=[
"id",
"title",
"preparation_time",
"cooking_time",
"original_serving_size",
"serving_size",
"ingredients",
"steps",
],
exclude="_id",
)

# Find the recipes
result = recipes.aggregate(pipeline.export()).to_list(length=None)

return result

def generate_match_query(ingredients: list[Ingredient], serving_size: int = 1) -> dict:
"""Generate the match query."""

operands = []
for ingredient in ingredients:
operand = {
"ingredients.name": ingredient.name,
"ingredients.unit": ingredient.unit,
"ingredients.quantity": {"$gte": ingredient.quantity / serving_size},
}
operands.append(operand)

query = {"$and": operands}

return query

def include_normalization_steps(pipeline: Pipeline, serving_size: int = 1):
"""Adds steps in a pipeline to normalize the ingredients quantity in the db

The steps below normalize the quantities of the ingredients in the recipes in the DB by the recipe serving size.

"""

# Unwind the ingredients
pipeline.unwind(path="$ingredients")

pipeline.add_fields({"original_serving_size": "$serving_size"})
# Add the normalized quantity
pipeline.add_fields(
{
# "orignal_serving_size": "$serving_size",
"serving_size": serving_size,
"ingredients.quantity": S.multiply(
S.field("ingredients.quantity"),
S.divide(serving_size, S.max([S.field("serving_size"), 1])),
),
}
)

# Group the results
pipeline.group(
by="_id",
query={
"id": {"$first": "$id"},
"title": {"$first": "$title"},
"original_serving_size": {"$first": "$original_serving_size"},
"serving_size": {"$first": "$serving_size"},
"preparation_time": {"$first": "$preparation_time"},
"cooking_time": {"$first": "$cooking_time"},
# "directions_source_text": {"$first": "$directions_source_text"},
"ingredients": {"$addToSet": "$ingredients"},
"steps": {"$first": "$steps"},
},
)
return pipeline

La lógica central de Panadero reside en el find_recipes función.

Esta función crea una canalización de agregación de MongoDB gracias a monggregate. Este proceso de agregación incluye varios pasos.

Los primeros pasos son generados por el include_normalization_steps función que actualizará dinámicamente las cantidades de los ingredientes en la base de datos para garantizar que estemos comparando manzanas con manzanas. Esto se hace actualizando las cantidades de ingredientes en la base de datos según la porción deseada por el usuario.

Entonces la lógica de coincidencia real es creada por el generate_match_query función. Aquí nos aseguramos de que las recetas no requieran más de lo que el usuario tiene para los ingredientes en cuestión.

Finalmente, una proyección filtra los campos que no necesitamos devolver.

Panadero te ayuda a descubrir un mejor destino para tus ingredientes al encontrar recetas que coincidan con lo que ya tienes en casa.

La aplicación presenta una interfaz sencilla basada en formularios. Ingresa los ingredientes que tienes, especifica sus cantidades y selecciona la unidad de medida entre las opciones disponibles.

Una captura de pantalla que muestra la interfaz de Baker para ingresar ingredientes.
Foto del autor

En el ejemplo anterior, estoy buscando una receta para dos porciones utilizar 4 tomates y 2 zanahorias que han estado en mi cocina durante demasiado tiempo.

Panadero ¡Encontré dos recetas! Al hacer clic en una receta podrá ver todos los detalles.

Una captura de pantalla de la interfaz de Baker que muestra cómo se muestran las recetas.
Foto del autor

Panadero adapta las cantidades de la receta para que coincidan con el tamaño de la porción que hayas establecido. Por ejemplo, si ajusta el tamaño de la porción de dos a cuatro personas, la aplicación vuelve a calcular las cantidades de ingredientes en consecuencia.

Actualizar el tamaño de la porción también puede cambiar las recetas que aparecen. Panadero garantiza que las recetas sugeridas coincidan no solo con el tamaño de la porción sino también con los ingredientes y las cantidades que tiene a mano. Por ejemplo, si sólo tienes 4 tomates y 2 zanahorias para dos personas, Panadero Evitará recomendar recetas que requieran 4 tomates y 4 zanahorias.