216 lines
8.3 KiB
Python
216 lines
8.3 KiB
Python
import json
|
|
from openai import AsyncOpenAI
|
|
from app.config import settings
|
|
|
|
SYSTEM_PROMPT = """Você é um nutricionista especialista brasileiro que analisa rótulos de alimentos.
|
|
Responda SEMPRE em JSON válido com esta estrutura exata:
|
|
{
|
|
"score": <int 0-100>,
|
|
"summary": "<resumo em 2-3 frases para leigo, em português>",
|
|
"positives": ["<ponto positivo 1>", ...],
|
|
"negatives": ["<ponto negativo 1>", ...],
|
|
"nutrition": {
|
|
"calorias": "<valor por porção, ex: 139 kcal>",
|
|
"gordura_total": "<valor, ex: 8g>",
|
|
"gordura_saturada": "<valor, ex: 3g>",
|
|
"acucar": "<valor, ex: 37g>",
|
|
"sodio": "<valor, ex: 14mg>",
|
|
"fibras": "<valor, ex: 0g>",
|
|
"proteinas": "<valor, ex: 0g>",
|
|
"carboidratos": "<valor, ex: 35g>"
|
|
},
|
|
"nutrition_verdict": "<frase curta sobre o perfil nutricional>",
|
|
"ingredients": [
|
|
{
|
|
"name": "<nome no rótulo>",
|
|
"popular_name": "<nome popular ou null>",
|
|
"explanation": "<o que é, 1 frase>",
|
|
"classification": "<good|warning|bad>",
|
|
"reason": "<motivo da classificação, 1 frase>"
|
|
}
|
|
],
|
|
"recipe": {
|
|
"title": "<nome da receita saudável>",
|
|
"description": "<descrição curta>",
|
|
"prep_time": "<tempo de preparo>",
|
|
"calories": "<calorias aproximadas>",
|
|
"ingredients_list": ["<ingrediente 1>", ...],
|
|
"steps": ["<passo 1>", ...],
|
|
"tip": "<dica nutricional>"
|
|
},
|
|
"substitutions": [
|
|
{
|
|
"name": "<nome do produto alternativo>",
|
|
"brand": "<marca sugerida ou genérica>",
|
|
"reason": "<por que é melhor, 1 frase>",
|
|
"estimated_score": <int 0-100>
|
|
}
|
|
]
|
|
}
|
|
|
|
REGRAS PARA SUBSTITUIÇÕES:
|
|
- SOMENTE inclua "substitutions" se o score for < 50
|
|
- Sugira 3 produtos REAIS brasileiros que sejam alternativas mais saudáveis
|
|
- Se score >= 50, retorne "substitutions": null
|
|
|
|
Para a receita:
|
|
- Se score > 70: sugira receita usando o produto
|
|
- Se score <= 70: sugira alternativa saudável
|
|
|
|
Critérios para o score:
|
|
- 90-100: Natural, minimamente processado
|
|
- 70-89: Bom, poucos aditivos
|
|
- 50-69: Médio, processado mas aceitável
|
|
- 30-49: Ruim, ultraprocessado
|
|
- 0-29: Péssimo, alto em açúcar/sódio/gordura trans
|
|
|
|
Use linguagem simples e direta."""
|
|
|
|
HEALTH_PROFILE_PROMPTS = {
|
|
"normal": "",
|
|
"crianca": "\n⚠️ PERFIL: CRIANÇA. Alerte sobre: excesso de açúcar, corantes artificiais, cafeína, sódio alto. Seja mais rigoroso com ultraprocessados.",
|
|
"gestante": "\n⚠️ PERFIL: GESTANTE. Alerte sobre: cafeína, adoçantes artificiais, sódio excessivo, conservantes. Priorize folato, ferro, cálcio.",
|
|
"diabetico": "\n⚠️ PERFIL: DIABÉTICO. Alerte sobre: açúcares, carboidratos refinados, índice glicêmico alto. Valorize fibras e proteínas.",
|
|
"hipertenso": "\n⚠️ PERFIL: HIPERTENSO. Alerte sobre: sódio (>400mg é ALTO), glutamato monossódico, conservantes com sódio. Limite: <2g sódio/dia.",
|
|
}
|
|
|
|
async def analyze_product(product_data: dict, user_context: dict = None) -> dict:
|
|
if not settings.OPENAI_API_KEY:
|
|
return _mock_analysis(product_data)
|
|
|
|
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
|
|
|
nutrition_info = product_data.get('nutrition', {})
|
|
nutrition_str = json.dumps(nutrition_info, ensure_ascii=False) if nutrition_info else 'Não disponível'
|
|
|
|
system = SYSTEM_PROMPT
|
|
extra = ""
|
|
if user_context:
|
|
hp = user_context.get("health_profile", "normal")
|
|
system += HEALTH_PROFILE_PROMPTS.get(hp, "")
|
|
allergies = user_context.get("allergies", [])
|
|
if allergies:
|
|
extra = f"\n\n⚠️ ALERGIAS DO USUÁRIO: {', '.join(allergies)}. Destaque QUALQUER ingrediente que possa conter esses alérgenos."
|
|
|
|
user_msg = f"""Produto: {product_data.get('name', 'Desconhecido')}
|
|
Marca: {product_data.get('brand', '')}
|
|
Categoria: {product_data.get('category', '')}
|
|
Ingredientes: {product_data.get('ingredients_text', 'Não disponível')}
|
|
Nutri-Score: {product_data.get('nutri_score', 'N/A')}
|
|
NOVA: {product_data.get('nova_group', 'N/A')}
|
|
Dados Nutricionais: {nutrition_str}{extra}
|
|
|
|
Analise este produto completo."""
|
|
|
|
try:
|
|
resp = await client.chat.completions.create(
|
|
model=settings.OPENAI_MODEL,
|
|
messages=[
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": user_msg}
|
|
],
|
|
response_format={"type": "json_object"},
|
|
temperature=0.3,
|
|
timeout=30,
|
|
)
|
|
return json.loads(resp.choices[0].message.content)
|
|
except Exception as e:
|
|
print(f"OpenAI error: {e}")
|
|
return _mock_analysis(product_data)
|
|
|
|
def _mock_analysis(product_data: dict) -> dict:
|
|
ingredients = product_data.get("ingredients_text", "")
|
|
score = 50
|
|
if any(w in ingredients.lower() for w in ["açúcar", "sugar", "xarope", "glucose"]):
|
|
score -= 15
|
|
if any(w in ingredients.lower() for w in ["hidrogenada", "trans"]):
|
|
score -= 20
|
|
if product_data.get("nova_group") == 4:
|
|
score -= 10
|
|
ns = product_data.get("nutri_score", "")
|
|
if ns == "e": score -= 10
|
|
elif ns == "d": score -= 5
|
|
elif ns == "a": score += 15
|
|
elif ns == "b": score += 10
|
|
score = max(0, min(100, score))
|
|
|
|
return {
|
|
"score": score,
|
|
"summary": f"Análise baseada em regras para {product_data.get('name', 'este produto')}. Configure OPENAI_API_KEY para análise completa com IA.",
|
|
"positives": ["Dados nutricionais disponíveis"],
|
|
"negatives": ["Análise IA indisponível - usando fallback"],
|
|
"nutrition": {},
|
|
"nutrition_verdict": "",
|
|
"ingredients": [],
|
|
"recipe": None,
|
|
"substitutions": None,
|
|
}
|
|
|
|
|
|
PHOTO_PROMPT = """Analise esta foto de um produto alimentar/suplemento. A foto pode mostrar o rótulo, tabela nutricional, ingredientes ou embalagem.
|
|
|
|
IMPORTANTE: Mesmo que a foto esteja parcial, TENTE extrair o máximo de informações. NUNCA retorne erro se conseguir identificar o produto.
|
|
|
|
Responda em JSON com este formato:
|
|
{
|
|
"product_name": "<nome>",
|
|
"brand": "<marca>",
|
|
"category": "<categoria>",
|
|
"ingredients_text": "<ingredientes>",
|
|
"nutri_score": "<a-e ou null>",
|
|
"nova_group": <1-4 ou null>,
|
|
"score": <int 0-100>,
|
|
"summary": "<resumo 2-3 frases>",
|
|
"positives": ["..."],
|
|
"negatives": ["..."],
|
|
"nutrition": {"calorias":"...","acucar":"...","gordura_total":"...","gordura_saturada":"...","sodio":"...","carboidratos":"...","fibras":"...","proteinas":"..."},
|
|
"nutrition_verdict": "<frase curta>",
|
|
"ingredients": [{"name":"...","popular_name":"...","explanation":"...","classification":"good|warning|bad","reason":"..."}],
|
|
"recipe": {"title":"...","description":"...","prep_time":"...","calories":"...","ingredients_list":["..."],"steps":["..."],"tip":"..."},
|
|
"substitutions": null
|
|
}
|
|
|
|
Se score < 50, inclua "substitutions" com 3 alternativas reais brasileiras.
|
|
SOMENTE retorne {"error": "mensagem"} se NÃO for alimento."""
|
|
|
|
async def analyze_photo(b64_image: str, user_context: dict = None) -> dict:
|
|
if not settings.OPENAI_API_KEY:
|
|
return None
|
|
|
|
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
|
|
|
|
system = "Voce eh um nutricionista brasileiro expert em analise de rotulos alimentares."
|
|
if user_context:
|
|
hp = user_context.get("health_profile", "normal")
|
|
system += HEALTH_PROFILE_PROMPTS.get(hp, "")
|
|
|
|
prompt = PHOTO_PROMPT
|
|
if user_context and user_context.get("allergies"):
|
|
prompt += f"\n\n⚠️ ALERGIAS: {', '.join(user_context['allergies'])}. Destaque alérgenos!"
|
|
|
|
for detail_level in ["low", "auto"]:
|
|
try:
|
|
resp = await client.chat.completions.create(
|
|
model="gpt-4o",
|
|
messages=[
|
|
{"role": "system", "content": system},
|
|
{"role": "user", "content": [
|
|
{"type": "text", "text": prompt},
|
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_image}", "detail": detail_level}}
|
|
]}
|
|
],
|
|
response_format={"type": "json_object"},
|
|
temperature=0.3,
|
|
timeout=30,
|
|
max_tokens=1500,
|
|
)
|
|
result = json.loads(resp.choices[0].message.content)
|
|
if result.get("error"):
|
|
continue
|
|
return result
|
|
except Exception as e:
|
|
print(f"OpenAI photo error (detail={detail_level}): {e}")
|
|
continue
|
|
|
|
return None
|