Spaces:
Sleeping
fix: resolve remaining HIGH/MEDIUM issues from code review
Browse filesChatbot 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 +2 -2
- app/config/settings.py +11 -4
- app/main.py +17 -16
- app/models/intent_classifier/model.py +3 -2
- app/models/ner/model.py +8 -2
- app/services/realtime_data.py +31 -19
- app/services/vector_store.py +2 -1
|
@@ -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']
|
|
@@ -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(
|
|
@@ -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
|
|
|
|
|
|
|
| 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 |
-
|
| 223 |
-
|
| 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 |
-
|
| 230 |
else:
|
| 231 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 286 |
else:
|
| 287 |
-
|
| 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 |
-
|
| 328 |
except Exception as e:
|
| 329 |
logger.warning(f"Cloud LLM auto-init failed: {e}")
|
| 330 |
else:
|
| 331 |
-
|
| 332 |
|
| 333 |
-
|
| 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 |
-
"
|
| 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 |
},
|
|
@@ -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:
|
|
@@ -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'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
@@ -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 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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 |
|
|
@@ -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=[
|
| 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 |
)
|