nat-ad / app.py
ibombonato's picture
feat: add discount pct and change resumo for opiniao dos usuarios
c1cde73 verified
raw
history blame
15.8 kB
import gradio as gr
import os
import requests
from crewai import Agent, Task, Crew, Process, LLM
from crewai_tools import ScrapeWebsiteTool
from crewai.tools import BaseTool
from dotenv import load_dotenv
from stealth_scrape_tool import StealthScrapeTool
load_dotenv()
class ShortenerTool(BaseTool):
name: str = "URL Shortener Tool"
description: str = "Generates a short version of a given URL using an external API."
natura_api_token: str
def _run(self, original_url: str) -> str:
api_url = "https://sales-mgmt-cb-bff-apigw.prd.naturacloud.com/cb-bff-cms/cms/shortener"
headers = {"authorization": f"Bearer {self.natura_api_token}", "content-type": "application/json"}
payload = {"url": original_url}
try:
response = requests.post(api_url, headers=headers, json=payload)
response.raise_for_status()
short_url_data = response.json()
return short_url_data.get("short", original_url)
except requests.exceptions.RequestException as e:
print(f"Warning: Error generating short URL: {e}. Returning original URL.")
return original_url
except ValueError:
print("Warning: Invalid JSON response from shortener API. Returning original URL.")
return original_url
class CalculateDiscountedPriceTool(BaseTool):
"""
A tool to calculate the final price of an item after a discount is applied.
"""
name: str = "Calculate Discounted Price Tool"
description: str = "Calculates the price after applying a given discount percentage."
def _run(self, original_price: float, discount_percentage: float) -> float:
"""Calculates the discounted price and the total discount amount.
This method takes an original price and a discount percentage, validates
the inputs, and then computes the final price after the discount is
applied, as well as the amount saved.
Args:
original_price: The initial price of the item as a float or integer.
Returns:
float:
- The final discounted price, rounded to 2 decimal places.
"""
if not isinstance(original_price, (int, float)) or not isinstance(discount_percentage, (int, float)):
raise ValueError("Both original_price and discount_percentage must be numbers.")
if discount_percentage < 0 or discount_percentage > 100:
raise ValueError("Discount percentage must be between 0 and 100.")
discount_amount = original_price * (discount_percentage / 100)
discounted_price = original_price - discount_amount
return round(discounted_price, 2)
class CalculateDiscountValueTool(BaseTool):
"""
A tool to calculate the final discount value of an item after comparing the original value and the final value.
"""
name: str = "Calculate Discount Value Tool"
description: str = "Calculates the discount value after comparing two values."
def _run(self, original_price: float, final_price: float) -> float:
"""Calculates the total discounted amount give the original and final price.
This method takes an original price and a final price, validates
the inputs, and then computes the final discounted value.
Args:
original_price: The initial price of the item as a float or integer.
final_price: The final price after discount as a float or integer.
Returns:
float:
- The final discount value, rounded to 0 decimal places.
"""
if not isinstance(original_price, (int, float)) or not isinstance(final_price, (int, float)):
raise ValueError("Both original_price and final_price must be numbers.")
discount_value = original_price - final_price
discount_percentage = (discount_value / original_price) * 100
return round(discount_percentage, 0)
class SocialMediaCrew:
def __init__(self, openai_api_key: str, natura_api_token: str, openai_base_url: str, openai_model_name: str):
self.openai_api_key = openai_api_key
self.natura_api_token = natura_api_token
self.openai_base_url = openai_base_url
self.openai_model_name = openai_model_name
self.scrape_tool = StealthScrapeTool() #ScrapeWebsiteTool()
self.shortener_tool = ShortenerTool(natura_api_token=self.natura_api_token)
self.calculate_discounted_price_tool = CalculateDiscountedPriceTool()
self.calculate_discount_value_tool = CalculateDiscountValueTool()
print("Initializing SocialMediaCrew with BASE URL:", self.openai_base_url)
print("Using OpenAI Model:", self.openai_model_name)
print("Using OpenAI Key:", self.openai_api_key[:10])
llm = LLM(
api_key=self.openai_api_key,
model=self.openai_model_name,
base_url=self.openai_base_url
)
self.product_analyst = Agent(
role='Product Analyst',
goal='Analyze the provided URL and extract key product information',
backstory=("You are an expert in analyzing product pages and extracting the most important information. You can identify the product name, its main features, and the target audience."),
verbose=True,
tools=[self.scrape_tool,
self.shortener_tool,
self.calculate_discounted_price_tool,
self.calculate_discount_value_tool],
allow_delegation=False,
llm=llm,
max_retries=3
)
self.social_media_copywriter = Agent(
role='Social Media Copywriter',
goal='Create a compelling social media post in Portuguese to sell the product',
backstory=("You are a creative copywriter specialized in the beauty and fragrance market. You know how to craft posts that are engaging, persuasive, and tailored for a Portuguese-speaking audience. You are an expert in using emojis and hashtags to increase engagement."),
verbose=True,
allow_delegation=False,
llm=llm,
max_retries=3
)
def run_crew(self, product_url: str, main_cupom: str, main_cupom_discount_percentage: float, cupom_1: str, cupom_2: str) -> str:
headers = {
"accept": "*/*",
"accept-language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
"sec-ch-ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"' ,
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"' ,
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
}
try:
response = requests.get(product_url, headers=headers)
response.raise_for_status()
if '<template data-dgst="NEXT_NOT_FOUND">' in response.text:
return "INVALID_URL"
except requests.exceptions.RequestException as e:
print(f"Error checking URL: {e}")
return "INVALID_URL"
analyze_product_task = Task(
description=(f"1. Scrape the content of the URL: {product_url} using the 'scrape_tool' with css_element = '.product-detail-banner'.\n2. Identify and extract the original product price and the final discounted price if existing. IGNORE any price breakdowns like 'produto' or 'consultoria'.\n3. Extract the product name, key characteristics, and any other relevant DISCOUNT available.\n4. Use the 'Calculate Discounted Price Tool' with the extracted final best price and the provided DISCOUNT PERCENTAGE ({main_cupom_discount_percentage}) to get the CUPOM DISCOUNTED PRICE.\n4.1 Use the 'Calculate Discount Value Tool' with ORIGINAL PRICE and CUPOM DISCOUNTED PRICE to get the TOTAL DISCOUNT PERCENTAGE.\n5. Use the 'URL Shortener Tool' to generate a short URL for {product_url}. If the shortener tool returns an error, use the original URL.\n6. Provide all this information, including the product name, ORIGINAL PRICE, DISCOUNTED PRICE (the one used as the input in the tool 'Calculate Discounted Price Tool'), 2) CUPOM DISCOUNTED PRICE, and the generated short URL (or the original if the shortener failed). If any of this information cannot be extracted, you MUST return 'MISSING_PRODUCT_INFO'."),
agent=self.product_analyst,
expected_output="A concise summary of the product including its name, key features, unique selling points, ORIGINAL PRICE, DISCOUNTED PRICE (the one used as the input in the tool 'Calculate Discounted Price Tool'), CUPOM DISCOUNTED PRICE, TOTAL DISCOUNT PERCENTAGE, and the SHORT SHAREABLE URL (or the original if the shortener failed), OR 'MISSING_PRODUCT_INFO' if essential product details are not found."
)
create_post_task = Task(
description=(f"""Based on the product analysis, create a CONCISE and DIRECT social media post in Portuguese, suitable for a WhatsApp group.
If the input you receive is 'INVALID_URL' or 'MISSING_PRODUCT_INFO', you MUST stop and output only that same message.
The post should strictly follow this template:
###Template:
{{Title}}
{{Description}}
De ~~{{ORIGINAL PRICE}}~~
🔥Por {{CUPOM DISCOUNTED PRICE}} 🔥
🔥 {{TOTAL DISCOUNT PERCENTAGE}}% OFF!
🎟️ USE O CUPOM >>> {main_cupom}
🛒 Link >>> {{short_url}}
`🎟️ *Cupom válido para a primeira compra no link Minha Loja Natura, mesmo se já comprou no app ou link antigo. Demais compras ou app, use o cupom {cupom_1} ou {cupom_2} (o desconto é um pouco menor)`
`‼️ Faça login nesse link com o mesmo email e senha que já usa pra comprar Natura!`
###End Template
Ensure a URL is always present in the output. Include a clear call to action and a MAXIMUM of 2 relevant emojis. DO NOT include hashtags. Keep it short and impactful and does not forget to include the backticks around the last paragraph.
If the input you receive is 'INVALID_URL', you MUST stop and output only 'INVALID_URL'."""),
agent=self.social_media_copywriter,
expected_output="A short, direct, and impactful social media post in Portuguese for WhatsApp, strictly following the provided template, including the FINAL PRICE, any DISCOUNT, the SHORT SHAREABLE URL, a call to action, and up to 2 emojis, one in the Title and another in the Description. No hashtags should be present. A URL must always be present in the final output, OR the message 'INVALID_URL' or 'MISSING_PRODUCT_INFO' if the page was not found or product info is missing.",
context=[analyze_product_task]
)
crew = Crew(
agents=[self.product_analyst, self.social_media_copywriter],
tasks=[analyze_product_task, create_post_task],
process=Process.sequential
)
print(f"Crew is kicking off for URL: {product_url}")
result = crew.kickoff()
return result
def clean_env_vars():
os.environ.pop("OPENAI_API_KEY", None)
os.environ.pop("NATURA_API_TOKEN", None)
os.environ.pop("OPENAI_BASE_URL", None)
os.environ.pop("OPENAI_MODEL_NAME", None)
# --- Gradio Interface ---
def generate_ad(product_url: str, main_cupom: str, main_cupom_discount_percentage: float, cupom_1: str, cupom_2: str, openai_api_key: str, natura_api_token: str, openai_base_url: str, openai_model_name: str):
if not openai_api_key or not natura_api_token or not openai_model_name or not openai_base_url:
return "Please configure your API keys in the settings section below."
social_media_crew = SocialMediaCrew(openai_api_key, natura_api_token, openai_base_url, openai_model_name)
result = social_media_crew.run_crew(product_url, main_cupom, main_cupom_discount_percentage, cupom_1, cupom_2)
if result == "INVALID_URL":
return "❌ The provided URL is invalid or the product page could not be found."
elif result == "MISSING_PRODUCT_INFO":
return "⚠️ Could not extract all required product information from the URL. Please check the URL or try a different one."
else:
return result.raw
with gr.Blocks() as demo:
gr.Markdown("# 🚀 Social Media Ad Generator")
gr.Markdown("Enter a product URL to generate a social media ad.")
with gr.Tab("Generate Ad"):
url_input = gr.Textbox(label="Product URL", placeholder="Enter product URL here...")
main_cupom_input = gr.Textbox(label="Main Cupom (e.g., PRIMEIRACOMPRA)", value="PRIMEIRACOMPRA")
main_cupom_discount_percentage_input = gr.Number(label="Main Cupom Discount Percentage (e.g., 20 for 20%)", value=20, minimum=0, maximum=100)
cupom_1_input = gr.Textbox(label="Cupom 1 (e.g., AMIGO15)", placeholder="Enter first coupon code...")
cupom_2_input = gr.Textbox(label="Cupom 2 (e.g., JULHOA)", placeholder="Enter second coupon code...")
generate_button = gr.Button("Generate Ad")
ad_output = gr.Markdown(label="Your Generated Ad", show_copy_button=True)
with gr.Tab("Fragrantica"):
gr.Markdown("### 👃 Fragrantica Website Analyzer")
fragrantica_url_input = gr.Textbox(label="Fragrantica Product URL", placeholder="Enter Fragrantica product URL here...")
analyze_fragrantica_button = gr.Button("Analyze Fragrantica Product")
fragrantica_output = gr.Markdown(label="Fragrantica Analysis Report")
with gr.Tab("Settings"):
gr.Markdown("### ⚙️ API Key Settings")
gr.Markdown("Enter your API keys below. These will be used for the current session.")
openai_key_input = gr.Textbox(label="OPENAI_API_KEY", type="password", value=os.getenv("OPENAI_API_KEY", ""))
natura_token_input = gr.Textbox(label="NATURA_API_TOKEN", type="password", value=os.getenv("NATURA_API_TOKEN", ""))
openai_base_url_input = gr.Textbox(label="OPENAI_BASE_URL", value=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"))
openai_model_name_input = gr.Textbox(label="OPENAI_MODEL_NAME", value=os.getenv("OPENAI_MODEL_NAME", "gpt-4.1"))
clean_env_vars()
# No save button needed as keys are passed directly
gr.Markdown("API keys are used directly from these fields when you click 'Generate Ad'. They are not saved persistently.")
generate_button.click(generate_ad, inputs=[url_input, main_cupom_input, main_cupom_discount_percentage_input, cupom_1_input, cupom_2_input, openai_key_input, natura_token_input, openai_base_url_input, openai_model_name_input], outputs=ad_output)
# Placeholder for Fragrantica analysis function
def analyze_fragrantica_url(url, openai_api_key, natura_api_token, openai_base_url, openai_model_name):
if not openai_api_key or not openai_model_name or not openai_base_url:
return "Please configure your API keys in the settings section below."
from fragrantica_crew import FragranticaCrew
fragrantica_crew = FragranticaCrew(openai_api_key, openai_base_url, openai_model_name)
report = fragrantica_crew.kickoff(url=url)
if report == "SCRAPING_FAILED":
return "❌ Scraping failed. The website could not be accessed or parsed. Please check the URL or try again later."
return report.raw
analyze_fragrantica_button.click(analyze_fragrantica_url, inputs=[fragrantica_url_input, openai_key_input, natura_token_input, openai_base_url_input, openai_model_name_input], outputs=fragrantica_output)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)