<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Generador de descuentos
Objetivos
Incentivar nuevas compras del cliente en el establecimiento
Fomentar el consumo de otros productos
Fomentar el consumo de productos con más margen de beneficio
Entradas y Salidas
Entrada
Step5:  Hechos
Definiremos a continuación los hechos que manejará el sistema.
Step8:  Objetivo 1
Incentivar nuevas compras del cliente en el establecimiento
Para esto no hay nada mejor que las típicas promociones 2x1, 3x2, etc.
Implementación
Step9:  Pruebas
Utilizaremos la función watch para ver qué está haciendo el motor durante la ejecución.
Step12:  Objetivo 2
Fomentar el consumo de otros productos
Para lograr este objetivo generaremos cupones con packs descuento. Ejemplo
Step13:  Pruebas
Step14:  Si compramos ambos productos de un pack no se nos debe generar la promoción, ya que en este caso el comercio perdería beneficio.
Step17:  Objetivo 3
Fomentar el consumo de productos con más margen de beneficio
El truco para cumplir este objetivo es conocer qué beneficio se obtiene por cada producto, y si existe un producto del mismo tipo con un beneficio mayor, generar un cupón de descuento para ese producto que nos permita seguir ganando más.
Implementación
Step18:  Pruebas
Step19:  El sistema no debe generar cupón si se ha comprado el producto con mayor beneficio
Step20:  Juntándolo todo
Gracias a Python podemos utilizar herencia múltiple para unir nuestros distintos motores en uno y darle un mejor interfaz de usuario.
<ASSISTANT_TASK:>
Python Code:
import re
from pyknow import *
Explanation: Generador de descuentos
Objetivos
Incentivar nuevas compras del cliente en el establecimiento
Fomentar el consumo de otros productos
Fomentar el consumo de productos con más margen de beneficio
Entradas y Salidas
Entrada: Lista de artículos que ha comprado el consumidor
Salida: Lista de cupones descuento que imprimir junto al recibo de compra
End of explanation
class Producto(Fact):
    
    Producto que ha comprado un cliente.
    >>> Producto(nombre="pepsi", tipo="refresco de cola", cantidad=1)
    
    pass
class Cupon(Fact):
    
    Cupón a generar para la próxima compra del cliente.
    >>> Cupon(tipo="2x1", producto="pepsi")
    
    
    pass
class Promo(Fact):
    
    Promoción vigente en el comercio.
    >>> Promo(tipo="2x1", **depende_de_la_promo)
    
    pass
class Beneficio(Fact):
    
    Define los beneficios que obtiene el comercio por cada producto.
    >>> Beneficio(nombre="pepsi", tipo="refresco de cola", ganancias=0.2)
    
    pass
Explanation: Hechos
Definiremos a continuación los hechos que manejará el sistema.
End of explanation
class OfertasNxM(KnowledgeEngine):
    @DefFacts()
    def carga_promociones_nxm(self):
        
        Hechos iniciales.
        
        Genera las promociones vigentes
        
        yield Promo(tipo="2x1", producto="Dodot")
        yield Promo(tipo="2x1", producto="Leche Pascual")
        yield Promo(tipo="3x2", producto="Pilas AAA")
    
    @Rule(Promo(tipo=MATCH.t & P(lambda t: re.match(r"\d+x\d+", t)),
                producto=MATCH.p),
          Producto(nombre=MATCH.p))
    def oferta_nxm(self, t, p):
        
        Sabemos que el cliente volverá para aprovechar
        la promoción, ya que hoy ha comprado el producto.
        
        self.declare(Cupon(tipo=t, producto=p))
Explanation: Objetivo 1
Incentivar nuevas compras del cliente en el establecimiento
Para esto no hay nada mejor que las típicas promociones 2x1, 3x2, etc.
Implementación
End of explanation
watch('RULES', 'FACTS')
nxm = OfertasNxM()
nxm.reset()
nxm.declare(Producto(nombre="Dodot"))
nxm.declare(Producto(nombre="Agua Mineral"))
nxm.declare(Producto(nombre="Pilas AAA"))
nxm.run()
nxm.facts
Explanation: Pruebas
Utilizaremos la función watch para ver qué está haciendo el motor durante la ejecución.
End of explanation
class OfertasPACK(KnowledgeEngine):
    @DefFacts()
    def carga_promociones_pack(self):
        Genera las promociones vigentes
        yield Promo(tipo="PACK", producto1="Fregona ACME", producto2="Mopa ACME", descuento="25%")
        yield Promo(tipo="PACK", producto1="Pasta Gallo", producto2="Tomate Frito", descuento="10%")
    @Rule(Promo(tipo="PACK", producto1=MATCH.p1, producto2=MATCH.p2, descuento=MATCH.d),
          OR(
              AND(
                  NOT(Producto(nombre=MATCH.p1)),
                  Producto(nombre=MATCH.p2)
              ),
              AND(
                  Producto(nombre=MATCH.p1),
                  NOT(Producto(nombre=MATCH.p2))
              )
          )
    )
    def pack(self, p1, p2, d):
        
        El cliente querrá comprar un producto adicional en su próxima visita.
        
        self.declare(Cupon(tipo="PACK", producto1=p1, producto2=p2, descuento=d))
Explanation: Objetivo 2
Fomentar el consumo de otros productos
Para lograr este objetivo generaremos cupones con packs descuento. Ejemplo:
Si compras una fregona y una mopa a la vez, tienes un 25% de descuento en ambos productos
Implementación
End of explanation
pack = OfertasPACK()
pack.reset()
pack.declare(Producto(nombre="Tomate Frito"))
pack.declare(Producto(nombre="Fregona ACME"))
pack.run()
Explanation: Pruebas
End of explanation
pack.reset()
pack.declare(Producto(nombre="Fregona ACME"))
pack.declare(Producto(nombre="Mopa ACME"))
pack.run()
Explanation: Si compramos ambos productos de un pack no se nos debe generar la promoción, ya que en este caso el comercio perdería beneficio.
End of explanation
class OfertasDescuento(KnowledgeEngine):
    @DefFacts()
    def carga_beneficios(self):
        
        Define las beneficios por producto.
        
        yield Beneficio(nombre="Mahou", tipo="Cerveza", ganancias=0.5)
        yield Beneficio(nombre="Cerveza Hacendado", tipo="Cerveza", ganancias=0.9)
        yield Beneficio(nombre="Pilas AAA Duracell", tipo="Pilas AAA", ganancias=1.5)
        yield Beneficio(nombre="Pilas AAA Hacendado", tipo="Pilas AAA", ganancias=2)
        
    @Rule(Producto(nombre=MATCH.p1),
          Beneficio(nombre=MATCH.p1, tipo=MATCH.t, ganancias=MATCH.g1),
          Beneficio(nombre=MATCH.p2, tipo=MATCH.t, ganancias=MATCH.g2),
          TEST(lambda g1, g2: g2 > g1)
    )
    def descuento_producto_con_mayor_beneficio(self, p2, g1, g2, **_):
        
        
        diferencia_ganancia = g2 - g1
        self.declare(Cupon(tipo="DESCUENTO",
                           producto=p2,
                           cantidad=diferencia_ganancia / 2))
Explanation: Objetivo 3
Fomentar el consumo de productos con más margen de beneficio
El truco para cumplir este objetivo es conocer qué beneficio se obtiene por cada producto, y si existe un producto del mismo tipo con un beneficio mayor, generar un cupón de descuento para ese producto que nos permita seguir ganando más.
Implementación
End of explanation
descuento = OfertasDescuento()
descuento.reset()
descuento.declare(Producto(nombre="Mahou"))
descuento.run()
Explanation: Pruebas
End of explanation
descuento.reset()
descuento.declare(Producto(nombre="Pilas AAA Hacendado"))
descuento.run()
Explanation: El sistema no debe generar cupón si se ha comprado el producto con mayor beneficio
End of explanation
class GeneradorCupones(OfertasNxM, OfertasPACK, OfertasDescuento):
    def generar_cupones(self, *nombre_productos):
        # Reiniciamos el motor
        self.reset()
        # Declaramos los productos que ha comprado el cliente
        for nombre in nombre_productos:
            self.declare(Producto(nombre=nombre))
        # Ejecutamos el motor
        self.run()
        
        # Extraemos las promociones generadas
        for fact in self.facts.values():
            if isinstance(fact, Cupon):
                yield fact
ke = GeneradorCupones()
[cupon for cupon in ke.generar_cupones("Pilas AAA", "Mahou", "Tomate Frito")]
Explanation: Juntándolo todo
Gracias a Python podemos utilizar herencia múltiple para unir nuestros distintos motores en uno y darle un mejor interfaz de usuario.
End of explanation
<END_TASK>