File size: 13,672 Bytes
0dd2082
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a9ac143
7addd58
 
 
 
 
 
a9ac143
 
0dd2082
7addd58
 
a9ac143
7addd58
a9ac143
7addd58
 
 
a9ac143
 
0dd2082
7addd58
0dd2082
 
7addd58
0dd2082
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
const Groq = require('groq-sdk');
const config = require('../config');
const logger = require('../utils/logger');
const AppError = require('../utils/AppError');

let groqClient = null;

function getClient() {
    if (!groqClient && config.groqApiKey) {
        groqClient = new Groq({ apiKey: config.groqApiKey });
    }
    return groqClient;
}

async function parseIntent(query, clarificationContext = null) {
    const client = getClient();

    if (!client) {
        throw new Error('AI Engine is not configured (Missing GROQ_API_KEY). Search cannot proceed without intent analysis.');
    }

    try {
        const sanitizedQuery = query
            .replace(/\b(near me|around me|close to me|nearby|here)\b/gi, '')
            .trim();

        let systemPrompt = `You are a high-intelligence discovery engine. Your goal is to map ANY user query to a physical location, service, or product source.
Use your VAST WORLD KNOWLEDGE to interpret the "Vibe" and "Context" of the search.

CRITICAL DISCOVERY RULES:
1. CONTEXTUAL INTELLIGENCE: If someone mentions a movie, anime, or culture (e.g., "red thread from Your Name"), do NOT just treat it as keywords. Recognize that they are looking for "Kumihimo" or "Japanese traditional lucky charms". Map it to "Japanese Gift Shop", "Traditional Craft Store", or "Anime Merch Store".
2. NICHE ITEMS: Even for tiny items (e.g., "needle", "toothpick"), find the most logical place (e.g., "stationary" or "pharmacy").
3. NO CLARIFICATION LOOPS: If the query is a real word/phrase, proceed with a "Best Guess" category.

Return ONLY valid JSON:
{
  "reasoning": "string (Explain your high-level cultural/logical interpretation, e.g., 'Targeting Japanese gift shops for Kumihimo as seen in Kimi no Na wa')",
  "isOutOfScope": "boolean (false for almost everything except pure info like 'Who is Elon Musk')",
  "scopeMessage": "string or null",
  "needsClarification": "boolean (ONLY true for single nonsense words)",
  "clarificationQuestion": "string or null",
  "category": "string (The BEST physical store type, e.g., japanese_gift_shop, anime_store, craft_store, hardware_store)",
  "isSpecific": "boolean (Always true if the user mentions a specific product or cultural item)",
  "specificItem": "string or null (The exact cultural/product item, e.g., 'Kumihimo Braided Cord')",
  "location": "string or null (Extract city name. Use null if missing.)",
  "neighborhood": "string or null",
  "budget": { "min": number|null, "max": number|null, "currency": "string" },
  "features": ["array of likely features, e.g., 'Traditional Japanese', 'Anime Merch'"],
  "occasion": "string or null",
  "sortBy": "string (rating|price|distance|relevance)"
}`;

        if (clarificationContext) {
            systemPrompt += `\n\nCRITICAL CONTEXT: The user previously searched for "${clarificationContext.originalQuery}", and you asked them: "${clarificationContext.question}". The user answered: "${clarificationContext.answer}". 
You must analyze their answer to fully deduce their target. SET "needsClarification" TO FALSE unless their answer is completely incomprehensible. Do your absolute best to map their answer into the 'category', 'isSpecific', and 'specificItem' fields so the search can proceed!`;
        }

        logger.info(`Sending query to Groq LLaMA: "${sanitizedQuery}"`);

        const response = await client.chat.completions.create({
            model: 'llama-3.3-70b-versatile',
            messages: [
                {
                    role: 'system',
                    content: systemPrompt
                },
                { role: 'user', content: sanitizedQuery },
            ],
            temperature: 0.1,
            max_tokens: 300,
        });

        const text = response.choices[0].message.content.trim();
        const jsonMatch = text.match(/\{[\s\S]*\}/);
        if (jsonMatch) {
            console.log('--- AI RAW JSON ---');
            console.log(jsonMatch[0]);
            console.log('-------------------');
            return JSON.parse(jsonMatch[0]);
        }
        throw new Error('AI Engine returned an invalid response format.');
    } catch (err) {
        logger.error('AI intent parsing failed', { error: err.message });

        if (err.status === 429) {
            throw new AppError('The AI engine is currently busy. Please try your search again in a moment.', 429);
        }

        throw new AppError('AI parsing failed. Please try a different query.', 500);
    }
}

async function rankResults(intent, results) {
    const client = getClient();

    // If no client or results are empty, just slice the top 10 as fallback
    if (!client || !results || results.length === 0) {
        return results.slice(0, 10);
    }

    try {
        if (!Array.isArray(results) || results.length === 0) {
            return [];
        }

        logger.info(`Ranking ${results.length} results using LLaMA`);

        const userLocationStr = intent.userLocation
            ? `The user's EXACT CURRENT GPS COORDINATES are: Latitude ${intent.userLocation.lat}, Longitude ${intent.userLocation.lng}.`
            : '';

        const intentContext = `
USER INTENT:
- Target Category: ${intent.category || 'Any'}
- Specific Item: ${intent.isSpecific ? intent.specificItem : 'None'}
- Location: ${intent.location || 'Anywhere'} (Target Search Area)
${userLocationStr}
- Budget: Max ${intent.budget?.max || 'Unlimited'}
- Preferences: ${(intent.features || []).join(', ') || 'None'}
- Sort Priority: ${intent.sortBy || 'relevance'}
`;

        const resultsContext = results.map(r =>
            `ID: ${r.id} | Name: ${r.name} | Category: ${r.category} | Rating: ${r.rating} | Features: ${(r.features || []).join(', ')} | User Reviews: ${r.reviews && r.reviews.length ? JSON.stringify(r.reviews) : 'None'}`
        ).join('\n');

        const promptContext = intentContext + "\nAVAILABLE PLACES:\n" + resultsContext;

        const response = await client.chat.completions.create({
            model: 'llama-3.1-8b-instant',
            messages: [
                {
                    role: 'system',
                    content: `You are an expert location curator. The user searched for: ${JSON.stringify(intent)}.
I will provide a list of raw scraped places. Your job is to select up to the Top 10 best matching places that ACTUALLY fit the user's criteria.

CRITICAL FILTERING RULES:
1. THINK FIRST: Verify that the place logically matches the user's intent based on "Actual Map Type" and User Reviews.
2. If "intent.isSpecific" is true, DO NOT strictly filter by category. Instead, evaluate if the place logically sells the "specificItem" (e.g., a "Convenience Store" sells "Monster Energy"). If it's highly likely they sell it, include it!
3. If "intent.isSpecific" is false, and the place does not match the requested category, OMIT IT entirely (e.g., omit a Park if the user wants a Gym).
4. If EXACT CURRENT GPS COORDINATES are provided: Prioritize locations that mention proximity to the user's area in their features, address, or reviews.
5. You do not have to return 10 places. If only 3 places genuinely match, only return those 3.
6. If "User Reviews" are provided for a place, read them and summarize the general human consensus in ONE short sentence. If no reviews exist, leave the summary blank.

Return ONLY a valid JSON object with your overall reasoning and a final array of matched objects. No markdown outside the JSON.
Example format:
{
  "reasoning": "I excluded X because it is a park. Y and Z perfectly match the cafe intent.",
  "matches": [
    { "id": "gmaps-1", "reviewSummary": "Customers highly praise the cold brew and fast wifi." },
    { "id": "gmaps-2", "reviewSummary": "" }
  ]
}`
                },
                { role: 'user', content: promptContext }
            ],
            temperature: 0.1,
            max_tokens: 600
        });

        const text = response.choices[0].message.content.trim();

        const jsonMatch = text.match(/\{[\s\S]*\}/);

        if (jsonMatch) {
            const parsed = JSON.parse(jsonMatch[0]);
            const rankedObjectsMatches = parsed.matches || [];

            // Re-map the IDs back to the original objects and inject the LLaMA review summary
            const rankedObjects = rankedObjectsMatches
                .map(match => {
                    const original = results.find(r => r.id === match.id);
                    if (original) {
                        return { ...original, reviewSummary: match.reviewSummary || null };
                    }
                    return null;
                })
                .filter(Boolean)
                .slice(0, 10);

            if (rankedObjects.length > 0) {
                return rankedObjects;
            } else {
                return [];
            }
        }

        logger.warn('AI ranking returned non-JSON', { text });
        return [];

    } catch (err) {
        logger.error('AI ranking failed', { error: err.message });
        return [];
    }
}

async function generateSemanticSuggestions(query) {
    const client = getClient();
    if (!client) {
        return [];
    }

    try {
        const response = await client.chat.completions.create({
            model: 'llama-3.1-8b-instant',
            messages: [
                {
                    role: 'system',
                    content: `You are an intelligent search suggestion engine. Analyze the user query's meaning, detect its intent type, handle ambiguity, and generate 3 or 4 context-aware search suggestions. Return ONLY valid JSON:
{
  "intentType": "place" | "product" | "entertainment" | "concept" | "ambiguous",
  "confidenceScore": number (0.0 to 1.0),
  "strategy": "string (Explain your plan, e.g., 'Clarify multiple meanings' or 'Suggest local places')",
  "suggestions": [
     {"type": "suggestion", "text": "Specific, actionable search query 1"},
     {"type": "suggestion", "text": "Specific, actionable search query 2"},
     ...
  ]
}

Rules:
1. If intent is "place": suggest location-based queries (e.g., "best cafes near me", "cafes with wifi").
2. If intent is "product": suggest product-based queries (e.g., "best price for iPhone", "iPhone reviews").
3. If intent is "ambiguous" or confidence < 0.6 (e.g., "predator", "apple"): generate CLARIFICATION suggestions (e.g., "Predator gaming laptops", "Predator movie", "Apple store locations").
4. Keep suggestions short and natural. Ensure your output is purely the JSON.`
                },
                { role: 'user', content: query }
            ],
            temperature: 0.3, // Slightly higher temp for creative suggestions
            max_tokens: 300
        });

        const text = response.choices[0].message.content.trim();
        const jsonMatch = text.match(/\{[\s\S]*\}/);

        if (jsonMatch) {
            const parsed = JSON.parse(jsonMatch[0]);
            return parsed.suggestions || [];
        }
        return [];
    } catch (err) {
        logger.error('AI suggestion generation failed', { error: err.message });
        return [];
    }
}

async function generateDynamicFilters(intent) {
    const client = getClient();
    if (!client) {
        return [];
    }

    try {
        const response = await client.chat.completions.create({
            model: 'llama-3.1-8b-instant',
            messages: [
                {
                    role: 'system',
                    content: `You are an AI UI Engineer designing a dynamic filter panel for a search application.
Based on the user's search intent, generate a JSON array of 3 to 5 highly relevant UI filters.

Return ONLY valid JSON in this exact structure:
{
  "filters": [
    { 
      "type": "range", 
      "id": "maxBudget", 
      "label": "Max Budget (₹)", 
      "min": 0, 
      "max": 10000, 
      "step": 500 
    },
    { 
      "type": "select", 
      "id": "features", 
      "label": "Features / Attributes", 
      "options": ["Option 1", "Option 2", "Option 3", "Option 4"] 
    },
    { 
      "type": "sort", 
      "id": "sortBy", 
      "label": "Sort By", 
      "options": [
        {"value": "relevance", "label": "Relevance"},
        {"value": "rating", "label": "Highest Rating"},
        {"value": "price_low", "label": "Price: Low → High"}
      ] 
    }
  ]
}

Rules:
1. Always include EXACTLY one "sort" filter.
2. If the intent is a "place" (e.g., cafe, restaurant, gym), include features like WiFi, Outdoor Seating, AC, Parking.
3. If the intent is a "product" (e.g., energy drinks, phones), include features like Availability, In Stock, Brand.
4. If the intent is "service" (e.g., salon, mechanic), include features like Appointment Required, Walk-ins, Same Day.
5. Provide a "range" filter (like maxBudget) ONLY if price is a relevant factor. If it's an informational query, omit it.
6. The JSON payload must strictly match the above schema format as it will be parsed directly into React DOM nodes. Do NOT output markdown text outside the JSON block.`
                },
                { role: 'user', content: JSON.stringify(intent) }
            ],
            temperature: 0.2,
            max_tokens: 400
        });

        const text = response.choices[0].message.content.trim();
        const jsonMatch = text.match(/\{[\s\S]*\}/);

        if (jsonMatch) {
            const parsed = JSON.parse(jsonMatch[0]);
            return parsed.filters || [];
        }
        return [];
    } catch (err) {
        logger.error('Dynamic Filter generation failed', { error: err.message });
        return [];
    }
}

module.exports = { parseIntent, rankResults, generateSemanticSuggestions, generateDynamicFilters };