Kiriten892 Claude Sonnet 4.6 commited on
Commit
c8a6da6
·
1 Parent(s): b442d39

fix: resolve remaining HIGH/MEDIUM issues from code review

Browse files

Chatbot ML Service:
- Replace all print() calls with logger.info/warning in load_models() startup path
- Remove LLM provider name and model ID from unauthenticated /health endpoint;
replace with generic llm_ready boolean to prevent vendor enumeration
- Reuse shared httpx.AsyncClient across weather requests (lazy-init property +
close() called in lifespan shutdown); eliminates per-request TCP handshake
- Cap ChromaDB query input at 256 chars before embedding for retrieval quality
- Add optional language param to IntentClassifier.predict() to skip redundant
language detection when caller has already detected it; update both call sites
in routes.py predict and stream handlers
- Remove 'anh' from NER false-positive blocklist so England/London alias resolves
correctly; add comment explaining deliberate omission
- Replace hardcoded timestamped TripAdvisor dataset filename with glob-based
auto-discovery of latest matching file in scraped_data/TripAdvisor/

Frontend:
- PaginationUI: replace href="#" with href="javascript:void(0)" and remove
e.preventDefault() calls to prevent scroll-to-top on page navigation
- HotelListPage: add destination?.id to hotel-fetch useEffect dependency array
so re-fetch triggers when destination ID changes with same name
- ActivityDetailPage: replace hardcoded "Vietnam" fallback with t('common.vietnam')

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

app/api/routes.py CHANGED
@@ -155,7 +155,7 @@ async def predict(request: PredictRequest):
155
 
156
  # 2. Intent classification
157
  try:
158
- intent_result = await asyncio.to_thread(intent_clf.predict, text)
159
  except Exception as e:
160
  logger.error(f"Intent classification failed: {e}")
161
  intent_result = {'intent': 'fallback', 'confidence': 0.0, 'method': 'error_fallback'}
@@ -1273,7 +1273,7 @@ async def predict_stream(request: PredictRequest):
1273
 
1274
  # Intent classification
1275
  try:
1276
- intent_result = await asyncio.to_thread(intent_clf.predict, text)
1277
  except Exception:
1278
  intent_result = {'intent': 'fallback', 'confidence': 0.0, 'method': 'error_fallback'}
1279
  intent = intent_result['intent']
 
155
 
156
  # 2. Intent classification
157
  try:
158
+ intent_result = await asyncio.to_thread(intent_clf.predict, text, language)
159
  except Exception as e:
160
  logger.error(f"Intent classification failed: {e}")
161
  intent_result = {'intent': 'fallback', 'confidence': 0.0, 'method': 'error_fallback'}
 
1273
 
1274
  # Intent classification
1275
  try:
1276
+ intent_result = await asyncio.to_thread(intent_clf.predict, text, language)
1277
  except Exception:
1278
  intent_result = {'intent': 'fallback', 'confidence': 0.0, 'method': 'error_fallback'}
1279
  intent = intent_result['intent']
app/config/settings.py CHANGED
@@ -1,9 +1,19 @@
 
1
  import os
2
  from dotenv import load_dotenv
3
 
4
  load_dotenv()
5
 
6
 
 
 
 
 
 
 
 
 
 
7
  def _env_bool(name: str, default: bool) -> bool:
8
  return os.getenv(name, str(default)).lower() == "true"
9
 
@@ -61,10 +71,7 @@ class Settings:
61
  VECTOR_STORE_PATH: str = os.path.join(MODEL_DIR, "vector_store")
62
  KB_PATH: str = os.path.join(DATA_DIR, "knowledge_base", "destinations.json")
63
  EVENTS_PATH: str = os.path.join(DATA_DIR, "knowledge_base", "events_calendar.json")
64
- TA_DATASET_PATH: str = os.getenv(
65
- "TA_DATASET_PATH",
66
- os.path.join(BASE_DIR, "scraped_data", "TripAdvisor", "dataset_tripadvisor_2026-04-19_05-48-05-642.json"),
67
- )
68
 
69
  # Vector store
70
  EMBEDDING_MODEL: str = os.getenv(
 
1
+ import glob as _glob
2
  import os
3
  from dotenv import load_dotenv
4
 
5
  load_dotenv()
6
 
7
 
8
+ def _find_latest_tripadvisor_dataset(base_dir: str) -> str:
9
+ pattern = os.path.join(base_dir, "scraped_data", "TripAdvisor", "dataset_tripadvisor_*.json")
10
+ matches = _glob.glob(pattern)
11
+ if matches:
12
+ return max(matches, key=os.path.getmtime)
13
+ # fallback to a stable name if no timestamped file found
14
+ return os.path.join(base_dir, "scraped_data", "TripAdvisor", "dataset_tripadvisor.json")
15
+
16
+
17
  def _env_bool(name: str, default: bool) -> bool:
18
  return os.getenv(name, str(default)).lower() == "true"
19
 
 
71
  VECTOR_STORE_PATH: str = os.path.join(MODEL_DIR, "vector_store")
72
  KB_PATH: str = os.path.join(DATA_DIR, "knowledge_base", "destinations.json")
73
  EVENTS_PATH: str = os.path.join(DATA_DIR, "knowledge_base", "events_calendar.json")
74
+ TA_DATASET_PATH: str = os.getenv("TA_DATASET_PATH", _find_latest_tripadvisor_dataset(BASE_DIR))
 
 
 
75
 
76
  # Vector store
77
  EMBEDDING_MODEL: str = os.getenv(
app/main.py CHANGED
@@ -32,7 +32,9 @@ async def lifespan(app):
32
  """Application lifespan: load models on startup, cleanup on shutdown."""
33
  await load_models()
34
  yield
35
- # Cleanup on shutdown (if needed)
 
 
36
  logger.info("Shutting down Wanderlust AI Chatbot Service")
37
 
38
 
@@ -219,16 +221,16 @@ async def load_models():
219
  # User profiler
220
  user_profiler = UserProfiler()
221
 
222
- print("Core models loaded successfully!")
223
- print("Enhanced modules loaded: BudgetOptimizer, CuisineRecommender, UserProfiler, RouteOptimizer")
224
 
225
  # Load TripAdvisor enrichment index (always — lightweight, no GPU needed)
226
  ta_enricher = TripAdvisorEnricher(dataset_path=settings.TA_DATASET_PATH)
227
  ta_enricher.load()
228
  if ta_enricher.is_ready:
229
- print(f"TripAdvisor enricher loaded: {settings.TA_DATASET_PATH}")
230
  else:
231
- print("TripAdvisor enricher: dataset not found, enrichment disabled")
232
 
233
  # Load LLM-related services if enabled
234
  if settings.USE_LLM_GENERATOR:
@@ -238,7 +240,7 @@ async def load_models():
238
  from app.models.llm.context_builder import ContextBuilder
239
  from app.models.llm.openai_generator import OpenAICompatibleGenerator
240
 
241
- print("Initializing vector store...")
242
  vector_store = VectorStore(
243
  kb_path=kb_path,
244
  persist_dir=settings.VECTOR_STORE_PATH,
@@ -246,7 +248,7 @@ async def load_models():
246
  )
247
  vector_store.initialize()
248
 
249
- print("Initializing real-time data service...")
250
  realtime_service = RealtimeDataService(
251
  weather_api_url=settings.WEATHER_API_URL,
252
  cache_ttl=settings.WEATHER_CACHE_TTL,
@@ -264,14 +266,14 @@ async def load_models():
264
  cloud_gen = OpenAICompatibleGenerator(context_builder=context_builder)
265
  if cloud_gen.is_ready():
266
  llm_generator = cloud_gen
267
- print(f"Cloud LLM ready: {cloud_gen._provider['name']} / {cloud_gen._model}")
268
  else:
269
  # Fallback: local HuggingFace model (needs GPU)
270
  _use_local = os.getenv("USE_LOCAL_LLM", "false").lower() == "true"
271
  if _use_local:
272
  from app.models.llm.model_loader import LLMModelLoader
273
  from app.models.llm.inference import LLMGenerator
274
- print(f"Loading local LLM: {settings.BASE_MODEL_NAME} ({settings.LLM_QUANTIZATION})...")
275
  model_loader = LLMModelLoader(
276
  base_model_name=settings.BASE_MODEL_NAME,
277
  adapter_path=settings.LLM_ADAPTER_PATH,
@@ -282,9 +284,9 @@ async def load_models():
282
  model_loader=model_loader,
283
  context_builder=context_builder,
284
  )
285
- print("Local LLM loaded as fallback.")
286
  else:
287
- print("No cloud API key and USE_LOCAL_LLM=false — LLM disabled, using templates.")
288
 
289
  except ImportError as e:
290
  logger.warning(f"LLM dependencies not installed, falling back to templates: {e}")
@@ -324,13 +326,13 @@ async def load_models():
324
  cloud_gen = OpenAICompatibleGenerator(context_builder=context_builder)
325
  if cloud_gen.is_ready():
326
  llm_generator = cloud_gen
327
- print(f"Cloud LLM auto-enabled via API key: {cloud_gen._provider['name']} / {cloud_gen._model}")
328
  except Exception as e:
329
  logger.warning(f"Cloud LLM auto-init failed: {e}")
330
  else:
331
- print("LLM mode disabled (USE_LLM_GENERATOR=false, no API key). Using template-based responses.")
332
 
333
- print("All models loaded successfully!")
334
  global _models_loaded_at
335
  _models_loaded_at = time.time()
336
 
@@ -358,8 +360,7 @@ async def health_check():
358
  "llm": {
359
  "enabled": settings.USE_LLM_GENERATOR or (llm_generator is not None),
360
  "ready": llm_generator is not None and llm_generator.is_ready(),
361
- "provider": getattr(llm_generator, '_provider', {}).get('name', 'local') if llm_generator else None,
362
- "model": getattr(llm_generator, '_model', settings.BASE_MODEL_NAME) if llm_generator else None,
363
  "vector_store": vector_store is not None and vector_store.is_ready,
364
  "realtime_service": realtime_service is not None,
365
  },
 
32
  """Application lifespan: load models on startup, cleanup on shutdown."""
33
  await load_models()
34
  yield
35
+ # Cleanup on shutdown
36
+ if realtime_service is not None:
37
+ await realtime_service.close()
38
  logger.info("Shutting down Wanderlust AI Chatbot Service")
39
 
40
 
 
221
  # User profiler
222
  user_profiler = UserProfiler()
223
 
224
+ logger.info("Core models loaded successfully!")
225
+ logger.info("Enhanced modules loaded: BudgetOptimizer, CuisineRecommender, UserProfiler, RouteOptimizer")
226
 
227
  # Load TripAdvisor enrichment index (always — lightweight, no GPU needed)
228
  ta_enricher = TripAdvisorEnricher(dataset_path=settings.TA_DATASET_PATH)
229
  ta_enricher.load()
230
  if ta_enricher.is_ready:
231
+ logger.info(f"TripAdvisor enricher loaded: {settings.TA_DATASET_PATH}")
232
  else:
233
+ logger.warning("TripAdvisor enricher: dataset not found, enrichment disabled")
234
 
235
  # Load LLM-related services if enabled
236
  if settings.USE_LLM_GENERATOR:
 
240
  from app.models.llm.context_builder import ContextBuilder
241
  from app.models.llm.openai_generator import OpenAICompatibleGenerator
242
 
243
+ logger.info("Initializing vector store...")
244
  vector_store = VectorStore(
245
  kb_path=kb_path,
246
  persist_dir=settings.VECTOR_STORE_PATH,
 
248
  )
249
  vector_store.initialize()
250
 
251
+ logger.info("Initializing real-time data service...")
252
  realtime_service = RealtimeDataService(
253
  weather_api_url=settings.WEATHER_API_URL,
254
  cache_ttl=settings.WEATHER_CACHE_TTL,
 
266
  cloud_gen = OpenAICompatibleGenerator(context_builder=context_builder)
267
  if cloud_gen.is_ready():
268
  llm_generator = cloud_gen
269
+ logger.info("Cloud LLM ready")
270
  else:
271
  # Fallback: local HuggingFace model (needs GPU)
272
  _use_local = os.getenv("USE_LOCAL_LLM", "false").lower() == "true"
273
  if _use_local:
274
  from app.models.llm.model_loader import LLMModelLoader
275
  from app.models.llm.inference import LLMGenerator
276
+ logger.info(f"Loading local LLM: {settings.BASE_MODEL_NAME} ({settings.LLM_QUANTIZATION})...")
277
  model_loader = LLMModelLoader(
278
  base_model_name=settings.BASE_MODEL_NAME,
279
  adapter_path=settings.LLM_ADAPTER_PATH,
 
284
  model_loader=model_loader,
285
  context_builder=context_builder,
286
  )
287
+ logger.info("Local LLM loaded as fallback.")
288
  else:
289
+ logger.info("No cloud API key and USE_LOCAL_LLM=false — LLM disabled, using templates.")
290
 
291
  except ImportError as e:
292
  logger.warning(f"LLM dependencies not installed, falling back to templates: {e}")
 
326
  cloud_gen = OpenAICompatibleGenerator(context_builder=context_builder)
327
  if cloud_gen.is_ready():
328
  llm_generator = cloud_gen
329
+ logger.info("Cloud LLM auto-enabled via API key")
330
  except Exception as e:
331
  logger.warning(f"Cloud LLM auto-init failed: {e}")
332
  else:
333
+ logger.info("LLM mode disabled (USE_LLM_GENERATOR=false, no API key). Using template-based responses.")
334
 
335
+ logger.info("All models loaded successfully!")
336
  global _models_loaded_at
337
  _models_loaded_at = time.time()
338
 
 
360
  "llm": {
361
  "enabled": settings.USE_LLM_GENERATOR or (llm_generator is not None),
362
  "ready": llm_generator is not None and llm_generator.is_ready(),
363
+ "llm_ready": llm_generator is not None,
 
364
  "vector_store": vector_store is not None and vector_store.is_ready,
365
  "realtime_service": realtime_service is not None,
366
  },
app/models/intent_classifier/model.py CHANGED
@@ -2,6 +2,7 @@ import os
2
  import json
3
  import joblib
4
  import numpy as np
 
5
  from sklearn.feature_extraction.text import TfidfVectorizer
6
  from sklearn.linear_model import LogisticRegression
7
  from sklearn.pipeline import Pipeline, FeatureUnion
@@ -138,9 +139,9 @@ class IntentClassifier:
138
  "report": classification_report(y_test, y_pred, output_dict=True)
139
  }
140
 
141
- def predict(self, text: str) -> dict:
142
  """Predict intent for a given text."""
143
- lang = detect_language(text)
144
  processed = preprocess(text, lang)
145
 
146
  if self.is_trained and self.pipeline is not None:
 
2
  import json
3
  import joblib
4
  import numpy as np
5
+ from typing import Optional
6
  from sklearn.feature_extraction.text import TfidfVectorizer
7
  from sklearn.linear_model import LogisticRegression
8
  from sklearn.pipeline import Pipeline, FeatureUnion
 
139
  "report": classification_report(y_test, y_pred, output_dict=True)
140
  }
141
 
142
+ def predict(self, text: str, language: Optional[str] = None) -> dict:
143
  """Predict intent for a given text."""
144
+ lang = language or detect_language(text)
145
  processed = preprocess(text, lang)
146
 
147
  if self.is_trained and self.pipeline is not None:
app/models/ner/model.py CHANGED
@@ -532,8 +532,14 @@ class TravelNER:
532
  'con', 'hay', 'bay', 'cho', 'coi', 'den', 'mot', 'nha', 'gia',
533
  'tam', 'tan', 'tay', 'hoa', 'van', 'mon',
534
  'massa', 'massage', 'tura', 'tour',
535
- # Common nouns that happen to be short KB location keys
536
- 'anh', 'batu', 'sunrise', 'temple', 'shrine', 'market', 'beach',
 
 
 
 
 
 
537
  })
538
 
539
  def _extract_locations(self, text: str) -> list:
 
532
  'con', 'hay', 'bay', 'cho', 'coi', 'den', 'mot', 'nha', 'gia',
533
  'tam', 'tan', 'tay', 'hoa', 'van', 'mon',
534
  'massa', 'massage', 'tura', 'tour',
535
+ # Common nouns that happen to be short KB location keys.
536
+ # NOTE: 'anh' is intentionally excluded here it is a curated alias for
537
+ # England (→ london) in ALIAS_LOCATIONS, so blocking it would prevent
538
+ # queries like "tôi muốn đi Anh" from extracting a location. The alias
539
+ # mapping takes higher priority than KB entries and is specific enough
540
+ # that occasional false-positives from the Vietnamese pronoun "anh" are
541
+ # acceptable.
542
+ 'batu', 'sunrise', 'temple', 'shrine', 'market', 'beach',
543
  })
544
 
545
  def _extract_locations(self, text: str) -> list:
app/services/realtime_data.py CHANGED
@@ -5,6 +5,8 @@ import time
5
  from datetime import datetime
6
  from typing import Optional
7
 
 
 
8
  logger = logging.getLogger(__name__)
9
 
10
  # Fallback destination coordinates (used when KB doesn't have the entry)
@@ -34,6 +36,7 @@ class RealtimeDataService:
34
  self._weather_cache: dict[str, dict] = {}
35
  self._events: list[dict] = []
36
  self._destinations: dict[str, dict] = {}
 
37
 
38
  # Load events calendar
39
  if os.path.exists(events_path):
@@ -59,6 +62,17 @@ class RealtimeDataService:
59
 
60
  logger.info(f"Weather coords loaded: {len(self._dest_coords)} destinations")
61
 
 
 
 
 
 
 
 
 
 
 
 
62
  async def get_weather(self, destination_id: str) -> Optional[dict]:
63
  """Get current weather for a destination. Uses cache with TTL."""
64
  cache_key = destination_id
@@ -71,26 +85,24 @@ class RealtimeDataService:
71
  return None
72
 
73
  try:
74
- import httpx
75
  url = f"{self.weather_api_url}/{coords['name']}?format=j1"
76
- async with httpx.AsyncClient(timeout=5.0) as client:
77
- response = await client.get(url)
78
- if response.status_code == 200:
79
- raw = response.json()
80
- current = raw.get("current_condition", [{}])[0]
81
- weather_data = {
82
- "temp_c": current.get("temp_C", "N/A"),
83
- "feels_like_c": current.get("FeelsLikeC", "N/A"),
84
- "humidity": current.get("humidity", "N/A"),
85
- "description": current.get("weatherDesc", [{}])[0].get("value", "N/A"),
86
- "wind_kmph": current.get("windspeedKmph", "N/A"),
87
- "destination": coords["name"],
88
- }
89
- self._weather_cache[cache_key] = {
90
- "data": weather_data,
91
- "timestamp": time.time()
92
- }
93
- return weather_data
94
  except Exception as e:
95
  logger.warning(f"Failed to fetch weather for {destination_id}: {e}")
96
 
 
5
  from datetime import datetime
6
  from typing import Optional
7
 
8
+ import httpx
9
+
10
  logger = logging.getLogger(__name__)
11
 
12
  # Fallback destination coordinates (used when KB doesn't have the entry)
 
36
  self._weather_cache: dict[str, dict] = {}
37
  self._events: list[dict] = []
38
  self._destinations: dict[str, dict] = {}
39
+ self._http_client: Optional[httpx.AsyncClient] = None
40
 
41
  # Load events calendar
42
  if os.path.exists(events_path):
 
62
 
63
  logger.info(f"Weather coords loaded: {len(self._dest_coords)} destinations")
64
 
65
+ @property
66
+ def http_client(self) -> httpx.AsyncClient:
67
+ if self._http_client is None:
68
+ self._http_client = httpx.AsyncClient(timeout=5.0)
69
+ return self._http_client
70
+
71
+ async def close(self) -> None:
72
+ if self._http_client is not None:
73
+ await self._http_client.aclose()
74
+ self._http_client = None
75
+
76
  async def get_weather(self, destination_id: str) -> Optional[dict]:
77
  """Get current weather for a destination. Uses cache with TTL."""
78
  cache_key = destination_id
 
85
  return None
86
 
87
  try:
 
88
  url = f"{self.weather_api_url}/{coords['name']}?format=j1"
89
+ response = await self.http_client.get(url)
90
+ if response.status_code == 200:
91
+ raw = response.json()
92
+ current = raw.get("current_condition", [{}])[0]
93
+ weather_data = {
94
+ "temp_c": current.get("temp_C", "N/A"),
95
+ "feels_like_c": current.get("FeelsLikeC", "N/A"),
96
+ "humidity": current.get("humidity", "N/A"),
97
+ "description": current.get("weatherDesc", [{}])[0].get("value", "N/A"),
98
+ "wind_kmph": current.get("windspeedKmph", "N/A"),
99
+ "destination": coords["name"],
100
+ }
101
+ self._weather_cache[cache_key] = {
102
+ "data": weather_data,
103
+ "timestamp": time.time()
104
+ }
105
+ return weather_data
 
106
  except Exception as e:
107
  logger.warning(f"Failed to fetch weather for {destination_id}: {e}")
108
 
app/services/vector_store.py CHANGED
@@ -288,10 +288,11 @@ class VectorStore:
288
  return []
289
 
290
  where_filter = {"dest_id": dest_id} if dest_id else None
 
291
 
292
  try:
293
  results = self.collection.query(
294
- query_texts=[query],
295
  n_results=top_k,
296
  where=where_filter
297
  )
 
288
  return []
289
 
290
  where_filter = {"dest_id": dest_id} if dest_id else None
291
+ query_text = query[:256] # cap for embedding quality
292
 
293
  try:
294
  results = self.collection.query(
295
+ query_texts=[query_text],
296
  n_results=top_k,
297
  where=where_filter
298
  )