315 lines
11 KiB
TypeScript
315 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: [] };
|
||
|
|
}
|
||
|
|
}
|