Spaces:
Running
Running
File size: 7,899 Bytes
0dd2082 a9ac143 0dd2082 6d2c2d9 432ec27 6d2c2d9 432ec27 6d2c2d9 432ec27 6d2c2d9 432ec27 0dd2082 432ec27 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 | const aiService = require('../services/ai.service');
const orchestrator = require('../services/orchestrator.service');
const scrapeGoogleMaps = require('../scripts/scraper');
const logger = require('../utils/logger');
const { success } = require('../utils/response');
async function handleSearch(req, res, next) {
try {
const { query, location, filters, userLocation, clarificationContext, page, limit } = req.body;
if (!query) {
return res.status(400).json({ error: 'Query is required' });
}
logger.info('Search request received', { query, location, clarificationContext });
const intent = await aiService.parseIntent(query, clarificationContext);
logger.info('Intent parsed', { intent });
if (intent.isOutOfScope) {
logger.info('Query out of scope. Providing guidance.', { message: intent.scopeMessage });
return res.json({
success: true,
isOutOfScope: true,
scopeMessage: intent.scopeMessage,
intent
});
}
if (intent.needsClarification) {
logger.info('Query ambiguous. Requesting clarification.', { question: intent.clarificationQuestion });
return res.json({
success: true,
needsClarification: true,
clarificationQuestion: intent.clarificationQuestion,
intent
});
}
if (location && !intent.location) {
intent.location = location;
}
// If they provided GPS, synthesize a generic location tag so the database schema doesn't crash on NULL
if (!intent.location && userLocation && userLocation.lat) {
intent.location = 'User Proximity';
}
// Phase: Sanitize "me/nearby" if coordinates are active
if (intent.location && (intent.location.toLowerCase() === 'me' || intent.location.toLowerCase() === 'nearby') && userLocation && userLocation.lat) {
intent.location = 'User Proximity';
}
if (!intent.location) {
return res.status(400).json({
success: false,
error: 'Please specify a location (e.g., "cafes in Paris" or use the location filter).'
});
}
if (filters) {
intent.features = [...(intent.features || []), ...(filters.features || [])];
if (filters.maxBudget) {
intent.budget = { ...intent.budget, max: filters.maxBudget };
}
if (filters.category && filters.category !== 'all') {
intent.category = filters.category;
}
if (filters.sortBy) {
intent.sortBy = filters.sortBy;
}
}
// Append explicit GPS coordinates if the client provided them
if (userLocation && userLocation.lat && userLocation.lng) {
intent.userLocation = userLocation;
logger.info(`Attached user GPS coordinates to intent: ${userLocation.lat}, ${userLocation.lng}`);
}
// Pass the request to the orchestrator
let data = await orchestrator.search(intent, { page, limit });
// Phase 14: Smart "Soft-Miss" Detection
const checkSatisfaction = () => {
if (!intent.isSpecific || !intent.specificItem) return true;
const item = intent.specificItem.toLowerCase();
return data.results.some(r => {
const searchSpace = [
r.name,
...(r.features || []),
r.rawCategory || '',
r.reviewSummary || ''
].join(' ').toLowerCase();
return searchSpace.includes(item);
});
};
const isDeeplySatisfied = checkSatisfaction();
// Phase 12 & 14: Trigger Scraper on:
// - Completely empty results, OR
// - Specific-item searches that are not well satisfied, OR
// - Very sparse generic results (e.g., fewer than 3 places)
const hasNoResults = data.results.length === 0;
const hasVeryFewGenericResults = !intent.isSpecific && data.results.length > 0 && data.results.length < 3;
const shouldTriggerScrape = hasNoResults || !isDeeplySatisfied || hasVeryFewGenericResults;
if (shouldTriggerScrape) {
const reason = hasNoResults
? '0 results found'
: !isDeeplySatisfied
? 'Generic results found for niche item'
: 'Very few results found for broad query';
logger.info(`${reason} in DB. Auto-triggering headless scraper...`, { query, item: intent.specificItem });
try {
// For specific items, we MUST use the full query to find the brand in Maps
const scrapeQuery = intent.isSpecific ? query : (intent.category || query);
await scrapeGoogleMaps(scrapeQuery, intent.category || 'all', intent.location, intent.userLocation);
logger.info('Auto-scrape complete. Re-querying Orchestrator...');
data = await orchestrator.search(intent, { page, limit });
} catch (scrapeErr) {
logger.error('Auto-scraping failed silently:', scrapeErr.message);
}
}
logger.info('Sending raw results to AI for ranking and generating dynamic filters', { count: data.results.length });
const [rankedResults, dynamicFilters] = await Promise.all([
aiService.rankResults(intent, data.results),
aiService.generateDynamicFilters(intent)
]);
logger.info('AI ranking complete', { topCount: rankedResults.length });
// --- Discovery Quotes (Your Name & Inspiration) ---
const quotes = [
"Musubi is the old way of calling the guardian god. To tie thread is Musubi. To connect people is Musubi. The flow of time is Musubi.",
"I’m always searching for something, a person, a place... I don't know what it is or where it is, but I know it's important to me...",
"Treasure the experience. Dreams fade away after you wake up.",
"Wherever you are in the world, I'll search for you.",
"It's like a dream. It's like a miracle.",
"The names are... Mitsuha! Taki!",
"Once in a while when I wake up. I find myself crying.",
"I wanted to tell you that... wherever you may end up in this world, I will search for you.",
"There's no way we could meet. But one thing is certain. If we see each other, we'll know. That you were the one who lived inside me. That I am the one who lived inside you."
];
const isMovieQuery = query.toLowerCase().includes('your name') ||
query.toLowerCase().includes('kimi no na wa') ||
(intent.reasoning && intent.reasoning.toLowerCase().includes('kimi no na wa')) ||
(intent.reasoning && intent.reasoning.toLowerCase().includes('your name'));
// Activate quote if results are 0 (to cheer up the user) OR if it's a specific movie query
const easterEgg = (isMovieQuery || rankedResults.length === 0)
? quotes[Math.floor(Math.random() * quotes.length)]
: null;
success(res, {
query,
intent,
results: rankedResults,
dynamicFilters,
easterEgg,
meta: {
...data.meta,
total: rankedResults.length,
rankedByAI: true
},
});
} catch (err) {
logger.error('CRITICAL SEARCH ERROR:', {
message: err.message,
stack: err.stack,
query: req.body.query
});
next(err);
}
}
module.exports = { handleSearch };
|