arbritage/services/geminiService.ts
2026-01-26 11:20:25 -03:00

314 lines
11 KiB
TypeScript

import { GoogleGenAI, Type } from "@google/genai";
import { Product, AuctionLot, BiddingTender } from "../types";
// Vite: variáveis precisam começar com VITE_
const API_KEY = import.meta.env.VITE_GOOGLE_API_KEY as string | undefined;
if (!API_KEY) {
// Erro explícito pra não “falhar silencioso”
throw new Error(
"Missing VITE_GOOGLE_API_KEY. Add it to your .env.local (VITE_GOOGLE_API_KEY=...) and restart the dev server."
);
}
const ai = new GoogleGenAI({ apiKey: API_KEY });
/**
* Helper: converte File em Part (inlineData) para o Gemini
*/
async function fileToPart(file: File) {
return new Promise<{ inlineData: { data: string; mimeType: string } }>((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error("Failed to read file"));
reader.onloadend = () => {
const result = reader.result as string | null;
if (!result) return reject(new Error("Empty file result"));
const base64Data = result.split(",")[1] || "";
resolve({
inlineData: {
data: base64Data,
mimeType: file.type || "application/octet-stream",
},
});
};
reader.readAsDataURL(file);
});
}
/**
* Sourcing Otimizado: Foco no MENOR PREÇO de compra (PY)
* e MENOR PREÇO de venda competitiva (BR - "Mais Vendidos")
*/
export async function searchProducts(
query: string
): Promise<{ products: Product[]; sources: any[] }> {
// try { // Removed to propagate errors
const prompt = `OBJETIVO: Análise de Arbitragem Profissional.
1. BUSCA REAL OBRIGATÓRIA:
Você DEVE usar a tool googleSearch para buscar dados REAIS e ATUAIS no site www.comprasparaguai.com.br.
Busque os 20 itens com o MENOR PREÇO ABSOLUTO para "${query}".
Foque em lojas de grande renome (Nissei, Cellshop, Atacado Games, Mega Eletronicos).
2. BUSCA BRASIL (REFERÊNCIA):
Para cada item, encontre o MENOR PREÇO de venda no Mercado Livre e Amazon Brasil.
3. REGRAS CRÍTICAS (ANTI-ALUCINAÇÃO):
- JAMAIS invente produtos ou preços.
- JAMAIS retorne itens com "(Exemplo)", "(Example)" ou dados fictícios.
- Se não encontrar dados REAIS, retorne lista vazia.
- Use a cotação de dólar turismo/paralelo atual (~5.75 BRL/USD).
4. RETORNO:
Apenas JSON conforme o schema.`;
const response = await ai.models.generateContent({
model: "gemini-3-pro-preview", // Stable fast model
contents: prompt,
config: {
tools: [{ googleSearch: {} }],
responseMimeType: "application/json",
responseSchema: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING, description: "Nome técnico do produto" },
priceUSD: { type: Type.NUMBER, description: "Menor preço em USD no Paraguai" },
priceBRL: { type: Type.NUMBER, description: "Preço USD convertido para BRL" },
store: { type: Type.STRING, description: "Loja do Paraguai (Nissei, Cellshop, etc)" },
url: { type: Type.STRING, description: "Link da oferta no Paraguai" },
marketPriceBRL: {
type: Type.NUMBER,
description: "Menor preço encontrado entre os MAIS VENDIDOS no Brasil",
},
amazonPrice: { type: Type.NUMBER, description: "Menor preço Amazon (Best Seller)" },
amazonUrl: { type: Type.STRING },
mlPrice: { type: Type.NUMBER, description: "Menor preço Mercado Livre (Mais Vendido)" },
mlUrl: { type: Type.STRING },
},
required: ["name", "priceUSD", "priceBRL", "store", "url", "marketPriceBRL"],
},
},
},
});
const text = response.text || "[]";
const products = JSON.parse(text);
const sources = response.candidates?.[0]?.groundingMetadata?.groundingChunks || [];
// Ordena para mostrar primeiro as melhores oportunidades (menor custo em BRL)
const sortedProducts = Array.isArray(products)
? products.sort((a: any, b: any) => (a.priceBRL ?? 0) - (b.priceBRL ?? 0))
: [];
return { products: sortedProducts as Product[], sources };
// } catch (error) {
// console.error("Erro na busca de arbitrage:", error);
// return { products: [], sources: [] };
// }
}
/**
* Busca Oportunidades (>25% Margem) por Categoria
*/
export async function searchOpportunities(category: string, includeOverhead: boolean = true): Promise<{ products: Product[]; sources: any[] }> {
// try {
const prompt = `OBJETIVO: Encontrar Oportunidades de Arbitragem (>25% Lucro) na categoria "${category}".
1. BUSCA PARAGUAI:
Vasculhe as principais lojas do Paraguai (Nissei, Cellshop, Mega, Atacado Games) em busca de produtos da categoria "${category}" que estejam com preços muito atrativos ou em oferta.
Identifique pelo menos 20 produtos promissores.
2. CÁLCULO DE MARGEM:
Para cada produto, estime o custo total em BRL (Preço PY * 5.75 * ${includeOverhead ? '1.20' : '1.00'} de taxas).
Compare com o MENOR preço de venda real no Brasil (Mercado Livre/Amazon - Filtro "Mais Vendidos").
Margem = (Preço Venda BR - Custo Total) / Preço Venda BR.
3. FILTRO RIGOROSO:
Retorne APENAS produtos onde a Margem estimada seja SUPERIOR a 20-25%.
Se não encontrar nada com 25%, mostre os que tiverem a maior margem possível.
4. RETORNO:
Apenas JSON conforme o schema.`;
const response = await ai.models.generateContent({
model: "gemini-3-pro-preview",
contents: prompt,
config: {
tools: [{ googleSearch: {} }],
responseMimeType: "application/json",
responseSchema: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING, description: "Nome técnico do produto" },
priceUSD: { type: Type.NUMBER, description: "Menor preço em USD no Paraguai" },
priceBRL: { type: Type.NUMBER, description: "Preço USD convertido para BRL" },
store: { type: Type.STRING, description: "Loja do Paraguai (Nissei, Cellshop, etc)" },
url: { type: Type.STRING, description: "Link da oferta no Paraguai" },
marketPriceBRL: {
type: Type.NUMBER,
description: "Menor preço encontrado entre os MAIS VENDIDOS no Brasil",
},
amazonPrice: { type: Type.NUMBER, description: "Menor preço Amazon (Best Seller)" },
amazonUrl: { type: Type.STRING },
mlPrice: { type: Type.NUMBER, description: "Menor preço Mercado Livre (Mais Vendido)" },
mlUrl: { type: Type.STRING },
},
required: ["name", "priceUSD", "priceBRL", "store", "url", "marketPriceBRL"],
},
},
},
});
const text = response.text || "[]";
const products = JSON.parse(text);
const sources = response.candidates?.[0]?.groundingMetadata?.groundingChunks || [];
// Ordena por maior margem (diferença entre venda BR e custo PY)
const sortedProducts = Array.isArray(products)
? products.sort((a: any, b: any) => {
const marginA = (a.marketPriceBRL - a.priceBRL) / a.marketPriceBRL;
const marginB = (b.marketPriceBRL - b.priceBRL) / b.marketPriceBRL;
return marginB - marginA; // Maior margem primeiro
})
: [];
return { products: sortedProducts as Product[], sources };
// } catch (error) {
// console.error("Erro na busca de oportunidades:", error);
// return { products: [], sources: [] };
// }
}
/**
* Leilões: Analisa texto, link ou ARQUIVO PDF
*/
export async function analyzeAuctionData(
input: string | File
): Promise<{ lot: AuctionLot | null; sources: any[] }> {
try {
const promptPrefix = "Analise este lote de leilão da Receita Federal.";
const parts: any[] = [];
if (input instanceof File) {
const filePart = await fileToPart(input);
parts.push(filePart);
parts.push({
text: `${promptPrefix}
Leia o documento PDF anexo e extraia TODOS os itens da tabela de mercadorias.
REGRAS:
1. Identifique os itens reais.
2. Descubra o MENOR valor real de mercado no Brasil (baseado nos itens mais vendidos) via Google Search.
3. Use URLs REAIS dos sites de busca.`,
});
} else {
parts.push({
text: `${promptPrefix}
Analise este conteúdo: "${input}".
REGRAS:
1. Identifique os itens reais.
2. Descubra o MENOR valor real de mercado no Brasil (baseado nos itens mais vendidos) via Google Search.
3. Use URLs REAIS dos sites de busca.`,
});
}
const analysisResponse = await ai.models.generateContent({
model: "gemini-3-pro-preview",
contents: { parts }, // padronizado
config: { tools: [{ googleSearch: {} }] },
});
const analysisText = analysisResponse.text || "";
const sources = analysisResponse.candidates?.[0]?.groundingMetadata?.groundingChunks || [];
// Segunda passada: força JSON limpo
const extraction = await ai.models.generateContent({
model: "gemini-3-pro-preview",
contents: `Transforme em JSON conforme o schema. Texto base: ${analysisText}`,
config: {
responseMimeType: "application/json",
responseSchema: {
type: Type.OBJECT,
properties: {
id: { type: Type.STRING },
title: { type: Type.STRING },
location: { type: Type.STRING },
items: { type: Type.ARRAY, items: { type: Type.STRING } },
currentBid: { type: Type.NUMBER },
minBid: { type: Type.NUMBER },
mlMarketPrice: { type: Type.NUMBER },
},
required: ["id", "title", "items", "minBid", "mlMarketPrice"],
},
},
});
const lotData = JSON.parse(extraction.text || "{}");
return {
lot: {
...lotData,
status: "Analyzing" as any,
url: typeof input === "string" && input.startsWith("http") ? input : "",
},
sources,
};
} catch (error) {
console.error("Erro ao analisar leilão:", error);
return { lot: null, sources: [] };
}
}
/**
* Licitações Otimizadas
*/
export async function searchBiddings(
query: string
): Promise<{ tenders: BiddingTender[]; sources: any[] }> {
try {
const prompt = `Pesquise licitações ativas para "${query}" no Brasil.
Priorize PNCP e Compras.gov.br.
Retorne apenas JSON conforme o schema.`;
const searchResponse = await ai.models.generateContent({
model: "gemini-3-pro-preview",
contents: prompt,
config: {
tools: [{ googleSearch: {} }],
responseMimeType: "application/json",
responseSchema: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
id: { type: Type.STRING },
org: { type: Type.STRING },
object: { type: Type.STRING },
estimatedValue: { type: Type.NUMBER },
deadline: { type: Type.STRING },
items: { type: Type.ARRAY, items: { type: Type.STRING } },
requirements: { type: Type.ARRAY, items: { type: Type.STRING } },
marketReferencePrice: { type: Type.NUMBER },
url: { type: Type.STRING },
},
required: ["id", "org", "object", "url"],
},
},
},
});
const tenders = JSON.parse(searchResponse.text || "[]");
const sources = searchResponse.candidates?.[0]?.groundingMetadata?.groundingChunks || [];
return { tenders: (Array.isArray(tenders) ? tenders : []) as BiddingTender[], sources };
} catch (error) {
console.error("Erro na busca de licitações:", error);
return { tenders: [], sources: [] };
}
}