feat: enhance README and improve product data handling in cache, llm, reader, and writer modules

This commit is contained in:
Victor Noguera
2026-04-07 22:36:01 -04:00
parent 1548979859
commit d192799850
6 changed files with 332 additions and 77 deletions

View File

@@ -22,6 +22,45 @@ Keep each reasoning under 100 characters to stay within output limits.`;
export async function analyzeProducts(
products: EnrichedProduct[],
): Promise<LlmVerdict[]> {
try {
return await analyzeProductsInternal(products);
} catch (err) {
const msg = String(err);
if (products.length > 1 && msg.includes("Context size has been exceeded")) {
console.warn(
`LLM context exceeded for batch of ${products.length}, retrying one product at a time...`,
);
const fallback: LlmVerdict[] = [];
for (const product of products) {
try {
const single = await analyzeProductsInternal([product]);
fallback.push(
single[0] ?? {
asin: product.record.asin,
verdict: "SKIP",
confidence: 0,
reasoning: "LLM returned empty verdict",
},
);
} catch {
fallback.push({
asin: product.record.asin,
verdict: "SKIP",
confidence: 0,
reasoning: "LLM context overflow on single-item fallback",
});
}
}
return fallback;
}
throw err;
}
}
async function analyzeProductsInternal(
products: EnrichedProduct[],
): Promise<LlmVerdict[]> {
const productSummaries = products.map(summarizeForLlm);
@@ -55,7 +94,10 @@ export async function analyzeProducts(
}
function summarizeForLlm(p: EnrichedProduct) {
const salePrice = p.keepa?.currentPrice ?? p.spApi.estimatedSalePrice;
const salePrice =
p.keepa?.currentPrice ??
p.record.sellingPriceFromSheet ??
p.spApi.estimatedSalePrice;
const referralFee = salePrice * (p.spApi.referralFeePercent / 100);
const fbaProfit =
salePrice - p.record.unitCost - p.spApi.fbaFee - referralFee;
@@ -64,9 +106,12 @@ function summarizeForLlm(p: EnrichedProduct) {
return {
asin: p.record.asin,
name: p.record.name,
name: clampText(p.record.name, 80),
brand: p.record.brand,
category: p.record.category ?? p.keepa?.categoryTree?.join(" > "),
category: clampText(
p.record.category ?? p.keepa?.categoryTree?.join(" > "),
60,
),
unitCost: p.record.unitCost,
currentPrice: salePrice,
priceRange90d: p.keepa
@@ -85,10 +130,15 @@ function summarizeForLlm(p: EnrichedProduct) {
salesRankDrops90: p.keepa?.salesRankDrops90,
},
spreadsheetEstimates: {
avgPrice90: p.record.avgPrice90FromSheet,
sellingPrice: p.record.sellingPriceFromSheet,
fbaNet: p.record.fbaNet,
grossProfit: p.record.grossProfit,
grossProfitPct: p.record.grossProfitPct,
netProfit: p.record.netProfitFromSheet,
roi: p.record.roiFromSheet,
},
supplier: clampText(p.record.supplier, 40),
moq: p.record.moq,
moqCost: p.record.moqCost,
totalQtyAvail: p.record.totalQtyAvail,
@@ -115,6 +165,13 @@ function summarizeForLlm(p: EnrichedProduct) {
};
}
function clampText(value: unknown, maxLen: number): string | undefined {
if (value == null) return undefined;
const s = String(value).trim();
if (!s) return undefined;
return s.length > maxLen ? `${s.slice(0, maxLen - 1)}.` : s;
}
function cleanLlmJson(text: string): string {
// Remove ```json ... ``` or ``` ... ``` wrapping
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
@@ -148,7 +205,9 @@ function parseVerdicts(
reasoning: String(v.reasoning ?? "No reasoning provided"),
}));
} catch (err) {
console.warn("Failed to parse LLM response, marking all as ANALYSIS_FAILED");
console.warn(
"Failed to parse LLM response, marking all as ANALYSIS_FAILED",
);
console.warn("Raw LLM content:", content.slice(0, 500));
return products.map((p) => ({
asin: p.record.asin,