-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathanalyzeWithLLM.js
More file actions
119 lines (103 loc) · 3.38 KB
/
analyzeWithLLM.js
File metadata and controls
119 lines (103 loc) · 3.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
const { GoogleGenerativeAI } = require("@google/generative-ai");
const LLM_FALLBACK = {
missing_features: [],
pitch: "AI automation opportunity",
};
/**
* Strip Gemini markdown fences and stray backticks before parsing.
* @param {string} raw
* @returns {string}
*/
function stripJsonFences(raw) {
if (!raw || typeof raw !== "string") return "";
let s = raw.trim();
const block = s.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
if (block) s = block[1].trim();
s = s.replace(/```json/gi, "").replace(/```/g, "").trim();
return s;
}
/**
* @param {string} raw
* @returns {{ missing_features: string[], pitch: string } | null}
*/
function safeParseLLMJson(raw) {
if (!raw || typeof raw !== "string") return null;
let s = stripJsonFences(raw);
const start = s.indexOf("{");
const end = s.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) return null;
s = s.slice(start, end + 1);
try {
const obj = JSON.parse(s);
if (!obj || typeof obj !== "object") return null;
const missing = Array.isArray(obj.missing_features)
? obj.missing_features.map((x) => String(x)).filter(Boolean)
: [];
const pitch = typeof obj.pitch === "string" ? obj.pitch.trim() : "";
return { missing_features: missing, pitch };
} catch {
return null;
}
}
/**
* @param {{
* name: string,
* review_count: number,
* rating: number,
* websiteUrl: string | null,
* scraped: boolean,
* scrapeError: string | null,
* hasForm: boolean,
* hasChat: boolean,
* hasBooking: boolean,
* hasPhone: boolean
* }} ctx
* @returns {Promise<{ missing_features: string[], pitch: string }>}
*/
async function analyzeWithLLM(ctx) {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey || !String(apiKey).trim()) {
return { ...LLM_FALLBACK };
}
try {
const genAI = new GoogleGenerativeAI(String(apiKey).trim());
const model = genAI.getGenerativeModel({
model: "gemini-1.5-flash",
generationConfig: {
temperature: 0.3,
responseMimeType: "application/json",
},
});
const payload = {
business_name: ctx.name,
yelp_reviews: ctx.review_count,
yelp_rating: ctx.rating,
website_url: ctx.websiteUrl,
website_scraped: ctx.scraped,
scrape_notes: ctx.scrapeError,
signals: {
has_contact_or_lead_form: ctx.hasForm,
has_live_chat: ctx.hasChat,
has_online_booking: ctx.hasBooking,
phone_visible_on_site: ctx.hasPhone,
},
};
const prompt = `You are a B2B sales assistant for a web agency.
Input (JSON):\n${JSON.stringify(payload)}
Respond with STRICT JSON only (no markdown, no backticks) matching exactly:
{"missing_features":["string"],"pitch":"string"}
Rules:
- missing_features: short feature names the site lacks or is weak on (e.g. "contact form", "live chat", "online booking"). Dedupe. Max 8 items.
- pitch: one concise paragraph (2-4 sentences) explaining how we can help this business, referencing their gaps.
- Use the provided booleans; do not invent a phone number or URL.`;
const result = await model.generateContent(prompt);
const response = result.response;
const text = response?.text?.() || "";
const parsed = safeParseLLMJson(text);
if (parsed) return parsed;
return { ...LLM_FALLBACK };
} catch {
return { ...LLM_FALLBACK };
}
}
module.exports = { analyzeWithLLM, safeParseLLMJson, LLM_FALLBACK };