🏗️ Código fonte completo: PostgreSQL, nutrição, receita, score labels, PWA fixes
This commit is contained in:
@@ -9,6 +9,17 @@ Responda SEMPRE em JSON válido com esta estrutura exata:
|
||||
"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, ex: Alto em açúcar, zero fibras>",
|
||||
"ingredients": [
|
||||
{
|
||||
"name": "<nome no rótulo>",
|
||||
@@ -17,9 +28,23 @@ Responda SEMPRE em JSON válido com esta estrutura exata:
|
||||
"classification": "<good|warning|bad>",
|
||||
"reason": "<motivo da classificação, 1 frase>"
|
||||
}
|
||||
]
|
||||
],
|
||||
"recipe": {
|
||||
"title": "<nome da receita saudável>",
|
||||
"description": "<descrição curta, 1-2 frases>",
|
||||
"prep_time": "<tempo de preparo>",
|
||||
"calories": "<calorias aproximadas da receita>",
|
||||
"ingredients_list": ["<ingrediente 1>", "<ingrediente 2>", ...],
|
||||
"steps": ["<passo 1>", "<passo 2>", ...],
|
||||
"tip": "<dica nutricional relacionada>"
|
||||
}
|
||||
}
|
||||
|
||||
Para a receita:
|
||||
- Se o produto for SAUDÁVEL (score > 70): sugira uma receita usando o produto
|
||||
- Se o produto for RUIM (score <= 70): sugira uma alternativa saudável que substitua o produto
|
||||
- A receita deve ser simples, rápida e brasileira quando possível
|
||||
|
||||
Critérios para o score:
|
||||
- 90-100: Alimento natural, minimamente processado, sem aditivos
|
||||
- 70-89: Bom, com poucos aditivos ou processamento leve
|
||||
@@ -27,6 +52,7 @@ Critérios para o score:
|
||||
- 30-49: Ruim, ultraprocessado com vários aditivos
|
||||
- 0-29: Péssimo, alto em açúcar/sódio/gordura trans, muitos aditivos
|
||||
|
||||
Use os dados nutricionais fornecidos quando disponíveis. Estime quando não disponíveis.
|
||||
Considere Nutri-Score, classificação NOVA, e ingredientes problemáticos.
|
||||
Seja direto e honesto. Use linguagem simples."""
|
||||
|
||||
@@ -36,14 +62,18 @@ async def analyze_product(product_data: dict) -> dict:
|
||||
|
||||
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'
|
||||
|
||||
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}
|
||||
|
||||
Analise este produto."""
|
||||
Analise este produto com informações nutricionais detalhadas e sugira uma receita."""
|
||||
|
||||
try:
|
||||
resp = await client.chat.completions.create(
|
||||
@@ -54,7 +84,7 @@ Analise este produto."""
|
||||
],
|
||||
response_format={"type": "json_object"},
|
||||
temperature=0.3,
|
||||
timeout=15,
|
||||
timeout=30,
|
||||
)
|
||||
return json.loads(resp.choices[0].message.content)
|
||||
except Exception as e:
|
||||
@@ -82,5 +112,8 @@ def _mock_analysis(product_data: dict) -> dict:
|
||||
"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"],
|
||||
"ingredients": []
|
||||
"nutrition": {},
|
||||
"nutrition_verdict": "",
|
||||
"ingredients": [],
|
||||
"recipe": None
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ class Product(Base):
|
||||
nova_group = Column(Integer)
|
||||
nutrition_json = Column(Text) # JSON string
|
||||
image_url = Column(String)
|
||||
updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -12,4 +12,4 @@ class Scan(Base):
|
||||
score = Column(Integer)
|
||||
summary = Column(Text)
|
||||
analysis_json = Column(Text) # Full AI analysis as JSON
|
||||
scanned_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
scanned_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -9,4 +9,4 @@ class User(Base):
|
||||
name = Column(String, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
is_premium = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -91,6 +91,9 @@ async def scan_product(req: ScanRequest, user: User = Depends(get_current_user),
|
||||
ingredients=analysis.get("ingredients", []),
|
||||
nutri_score=product_data.get("nutri_score"),
|
||||
nova_group=product_data.get("nova_group"),
|
||||
nutrition=analysis.get("nutrition"),
|
||||
nutrition_verdict=analysis.get("nutrition_verdict"),
|
||||
recipe=analysis.get("recipe"),
|
||||
source=source,
|
||||
)
|
||||
|
||||
@@ -104,3 +107,67 @@ async def get_history(user: User = Depends(get_current_user), db: AsyncSession =
|
||||
id=s.id, barcode=s.barcode, product_name=s.product_name,
|
||||
brand=s.brand, score=s.score, scanned_at=s.scanned_at
|
||||
) for s in scans]
|
||||
|
||||
@router.get("/history/{scan_id}")
|
||||
async def get_scan_detail(scan_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Scan).where(Scan.id == scan_id, Scan.user_id == user.id)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan não encontrado")
|
||||
|
||||
analysis = json.loads(scan.analysis_json or '{}')
|
||||
# Also get product info
|
||||
prod_result = await db.execute(select(Product).where(Product.barcode == scan.barcode))
|
||||
product = prod_result.scalar_one_or_none()
|
||||
|
||||
return {
|
||||
"id": scan.id,
|
||||
"barcode": scan.barcode,
|
||||
"product_name": scan.product_name,
|
||||
"brand": scan.brand,
|
||||
"score": scan.score,
|
||||
"summary": scan.summary,
|
||||
"scanned_at": scan.scanned_at.isoformat() if scan.scanned_at else None,
|
||||
"category": product.category if product else None,
|
||||
"image_url": product.image_url if product else None,
|
||||
"nutri_score": product.nutri_score if product else None,
|
||||
"nova_group": product.nova_group if product else None,
|
||||
"positives": analysis.get("positives", []),
|
||||
"negatives": analysis.get("negatives", []),
|
||||
"ingredients": analysis.get("ingredients", []),
|
||||
}
|
||||
|
||||
@router.get("/history/{scan_id}")
|
||||
async def get_scan_detail(scan_id: int, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(
|
||||
select(Scan).where(Scan.id == scan_id, Scan.user_id == user.id)
|
||||
)
|
||||
scan = result.scalar_one_or_none()
|
||||
if not scan:
|
||||
raise HTTPException(status_code=404, detail="Scan não encontrado")
|
||||
|
||||
analysis = json.loads(scan.analysis_json or '{}')
|
||||
prod_result = await db.execute(select(Product).where(Product.barcode == scan.barcode))
|
||||
product = prod_result.scalar_one_or_none()
|
||||
|
||||
return {
|
||||
"id": scan.id,
|
||||
"barcode": scan.barcode,
|
||||
"product_name": scan.product_name,
|
||||
"brand": scan.brand,
|
||||
"score": scan.score,
|
||||
"summary": scan.summary,
|
||||
"scanned_at": scan.scanned_at.isoformat() if scan.scanned_at else None,
|
||||
"category": product.category if product else None,
|
||||
"image_url": product.image_url if product else None,
|
||||
"nutri_score": product.nutri_score if product else None,
|
||||
"nova_group": product.nova_group if product else None,
|
||||
"positives": analysis.get("positives", []),
|
||||
"negatives": analysis.get("negatives", []),
|
||||
"ingredients": analysis.get("ingredients", []),
|
||||
"nutrition": analysis.get("nutrition", {}),
|
||||
"nutrition_verdict": analysis.get("nutrition_verdict", ""),
|
||||
"recipe": analysis.get("recipe"),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Any, Dict
|
||||
from datetime import datetime
|
||||
|
||||
class ScanRequest(BaseModel):
|
||||
@@ -9,9 +9,18 @@ class IngredientAnalysis(BaseModel):
|
||||
name: str
|
||||
popular_name: Optional[str] = None
|
||||
explanation: str
|
||||
classification: str # "good", "warning", "bad"
|
||||
classification: str
|
||||
reason: str
|
||||
|
||||
class RecipeInfo(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
prep_time: Optional[str] = None
|
||||
calories: Optional[str] = None
|
||||
ingredients_list: Optional[List[str]] = None
|
||||
steps: Optional[List[str]] = None
|
||||
tip: Optional[str] = None
|
||||
|
||||
class ScanResult(BaseModel):
|
||||
barcode: str
|
||||
product_name: Optional[str] = None
|
||||
@@ -23,6 +32,9 @@ class ScanResult(BaseModel):
|
||||
positives: List[str]
|
||||
negatives: List[str]
|
||||
ingredients: List[IngredientAnalysis]
|
||||
nutrition: Optional[Dict[str, Any]] = None
|
||||
nutrition_verdict: Optional[str] = None
|
||||
recipe: Optional[RecipeInfo] = None
|
||||
nutri_score: Optional[str] = None
|
||||
nova_group: Optional[int] = None
|
||||
source: str = "open_food_facts"
|
||||
|
||||
Reference in New Issue
Block a user