← back to Govarbitrage
src/agents/appraiser.ts
86 lines
import { prisma } from "@/lib/db";
import { runAgent, type AgentRunResult } from "./framework";
import { runResearch } from "@/pipeline/research";
// The Appraiser re-researches poorly-identified listings WITH the local AI
// (Ollama qwen3:14b) so valuations become item-specific instead of the
// keyword-heuristic placeholder (the "$900 retail / 214% ROI for everything"
// artifact). Sequential (concurrency 1) — one local LLM, ~30-60s per listing.
const num = (d: unknown): number | null => (d == null ? null : Number(d));
/**
* Pick up to `limit` ACTIVE, still-open listings ordered by soonest close
* where identification is weak (manufacturer/model missing, or the last
* research pass was heuristic-only / low confidence), and re-run research
* with useAI + writeIdentity.
*/
export async function runAppraiser(limit = 25): Promise<AgentRunResult> {
return runAgent("appraiser", async (ctx) => {
const now = new Date();
const candidates = await prisma.listing.findMany({
where: {
listingStatus: "ACTIVE",
closingAt: { gt: now },
OR: [
{ manufacturer: null },
{ model: null },
{ identifiedBy: null },
{ identifiedBy: "heuristic" },
{ research: { is: { confidenceScore: { lt: 40 } } } },
],
},
orderBy: { closingAt: "asc" },
take: limit,
include: { research: true },
});
let aiIdentified = 0;
let fallbacks = 0;
for (const l of candidates) {
const beforeRetail = num(l.research?.newRetail);
const beforeConfidence = num(l.research?.confidenceScore);
const result = await runResearch(l.id, { useAI: true, writeIdentity: true });
ctx.count();
const after = await prisma.research.findUnique({ where: { listingId: l.id } });
const afterRetail = num(after?.newRetail);
const afterConfidence = num(after?.confidenceScore);
const fmt = (v: number | null, pct = false) =>
v == null ? "—" : pct ? `${Math.round(v)}/100` : `$${Math.round(v)}`;
const delta =
`newRetail ${fmt(beforeRetail)} → ${fmt(afterRetail)}, ` +
`confidence ${fmt(beforeConfidence, true)} → ${fmt(afterConfidence, true)}`;
if (result.identifiedBy === "heuristic") {
fallbacks++;
await ctx.finding({
kind: "AI_FALLBACK",
severity: "WARN",
listingId: l.id,
listingTitle: l.title,
title: `Ollama returned nothing — heuristic fallback kept for "${l.title.slice(0, 80)}"`,
detail: `${delta}. Valuation remains keyword-heuristic; re-run when the local model is reachable.`,
});
} else {
aiIdentified++;
await ctx.finding({
kind: "REAPPRAISED",
severity: "INFO",
listingId: l.id,
listingTitle: l.title,
title: `Re-appraised via ${result.identifiedBy}: ${delta}`,
detail:
`${l.title}. Net $${Math.round(result.expectedNetProfit)}, ` +
`ROI ${Math.round(result.roi * 100)}%, overall score ${Math.round(result.overallScore)}.`,
});
}
}
return `${candidates.length} listings appraised — ${aiIdentified} AI-identified, ${fallbacks} heuristic fallbacks`;
});
}