wanderlust-chatbot / scripts /collect_real_intent_data.py
Kiriten892's picture
feat: real-data dataset enrichment + nearby_attractions + travel_tips intents
2993806
Raw
History Blame Contribute Delete
7.59 kB
"""
Collect & Merge Real Intent Data
==================================
Orchestrates all scrapers and merges results into intent_train.json.
Steps:
1. Run HuggingFace dataset scraper (multiwoz, sgd, nlu corpora)
2. Run Reddit scraper (r/travel, r/solotravel, ...)
3. Run TripAdvisor Forum scraper (forum thread titles)
4. Merge all scraped files + existing intent_train.json
5. Deduplicate by text (case-insensitive)
6. Cap per-intent at TARGET_CAP to avoid new imbalance
7. Write merged result back to intent_train.json
IMPORTANT: This script never writes synthetic/hardcoded text.
All samples come from real scraped or published datasets.
Run from chatbot-ml-service/ directory:
python scripts/collect_real_intent_data.py [--skip-scrape] [--dry-run]
Flags:
--skip-scrape : skip running scrapers (use already-saved scraped_*.json files)
--dry-run : show counts only, do not write to intent_train.json
"""
import argparse
import json
import logging
import os
import subprocess
import sys
from collections import Counter
from typing import Optional
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATASETS_DIR = os.path.join(BASE_DIR, "app", "data", "datasets")
INTENT_TRAIN_FILE = os.path.join(DATASETS_DIR, "intent_train.json")
SCRAPED_FILES = [
os.path.join(DATASETS_DIR, "scraped_hf_intent.json"),
os.path.join(DATASETS_DIR, "scraped_reddit_intent.json"),
os.path.join(DATASETS_DIR, "scraped_tripadvisor_intent.json"),
]
SCRAPER_SCRIPTS = [
os.path.join(BASE_DIR, "scripts", "scrapers", "huggingface_dataset_scraper.py"),
os.path.join(BASE_DIR, "scripts", "scrapers", "reddit_scraper.py"),
os.path.join(BASE_DIR, "scripts", "scrapers", "tripadvisor_qa_scraper.py"),
]
# Known valid intents in this project
ALLOWED_INTENTS = {
"weather_info", "budget_advice", "find_flight", "fallback",
"food_recommend", "transport_info", "plan_trip", "activity_suggest",
"visa_info", "greeting", "find_hotel", "thank",
"compare_destinations", "show_map", "destination_info",
"nearby_attractions", "travel_tips",
}
# Per-intent target cap — we don't want to over-weight any single source
TARGET_CAP = 500
# Underrepresented intents get priority — don't cap them as strictly
PRIORITY_INTENTS = {"destination_info", "show_map", "compare_destinations",
"nearby_attractions", "travel_tips", "visa_info",
"find_hotel", "greeting", "thank"}
PRIORITY_CAP = 500 # same cap but filled first
def load_json(path: str) -> list[dict]:
if not os.path.exists(path):
logger.warning(f"File not found: {path}")
return []
with open(path, encoding="utf-8") as f:
try:
return json.load(f)
except json.JSONDecodeError as e:
logger.error(f"JSON error in {path}: {e}")
return []
def run_scraper(script: str) -> bool:
logger.info(f"Running scraper: {script}")
result = subprocess.run(
[sys.executable, script],
cwd=BASE_DIR,
capture_output=False,
)
if result.returncode != 0:
logger.error(f"Scraper failed: {script}")
return False
return True
def merge_and_cap(
existing: list[dict],
scraped: list[dict],
target_cap: int,
priority_cap: int,
) -> list[dict]:
"""
Merge existing + scraped samples.
Priority intents are filled first from scraped data.
Then existing data fills remaining slots.
Then non-priority scraped data fills remaining slots.
Per-intent caps are respected throughout.
"""
# Start with existing data (ground truth baseline)
counts: Counter = Counter(s["intent"] for s in existing)
result: list[dict] = list(existing)
# Filter scraped to only allowed intents
valid_scraped = [
s for s in scraped
if s.get("intent") in ALLOWED_INTENTS and s.get("text") and len(s["text"]) >= 8
]
# Deduplicate scraped vs existing
existing_texts: set[str] = {s["text"].lower().strip() for s in existing}
new_scraped = [
s for s in valid_scraped
if s["text"].lower().strip() not in existing_texts
]
logger.info(f"New (non-duplicate) scraped samples: {len(new_scraped)}")
# Sort: priority intents first
def priority_key(s: dict) -> int:
return 0 if s["intent"] in PRIORITY_INTENTS else 1
new_scraped.sort(key=priority_key)
# Add scraped samples respecting caps
added: Counter = Counter()
skipped = 0
for s in new_scraped:
intent = s["intent"]
cap = priority_cap if intent in PRIORITY_INTENTS else target_cap
if counts[intent] < cap:
result.append(s)
counts[intent] += 1
added[intent] += 1
else:
skipped += 1
logger.info(f"Added from scraped: {sum(added.values())} samples")
logger.info(f"Skipped (cap reached): {skipped}")
logger.info(f"Per-intent added: {dict(added.most_common())}")
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--skip-scrape", action="store_true",
help="Use existing scraped_*.json files without re-running scrapers")
parser.add_argument("--dry-run", action="store_true",
help="Show counts only, do not write to intent_train.json")
args = parser.parse_args()
# Step 1: Run scrapers
if not args.skip_scrape:
for script in SCRAPER_SCRIPTS:
success = run_scraper(script)
if not success:
logger.warning(f"Scraper returned non-zero, continuing anyway: {script}")
else:
logger.info("--skip-scrape: using existing scraped files")
# Step 2: Load existing training data
logger.info(f"Loading existing intent_train.json ...")
existing = load_json(INTENT_TRAIN_FILE)
logger.info(f"Existing samples: {len(existing)}")
existing_counts = Counter(s["intent"] for s in existing)
logger.info("Existing per-intent:")
for k, v in sorted(existing_counts.items(), key=lambda x: x[1]):
logger.info(f" {k}: {v}")
# Step 3: Load all scraped files
all_scraped: list[dict] = []
for path in SCRAPED_FILES:
data = load_json(path)
logger.info(f"Loaded {len(data)} samples from {os.path.basename(path)}")
all_scraped.extend(data)
logger.info(f"Total scraped (before dedup): {len(all_scraped)}")
# Step 4: Merge
merged = merge_and_cap(existing, all_scraped, TARGET_CAP, PRIORITY_CAP)
# Step 5: Summary
merged_counts = Counter(s["intent"] for s in merged)
logger.info("\n=== FINAL COUNTS ===")
for k, v in sorted(merged_counts.items(), key=lambda x: x[1]):
old = existing_counts.get(k, 0)
gain = v - old
logger.info(f" {k}: {old}{v} (+{gain})")
logger.info(f" TOTAL: {len(existing)}{len(merged)} (+{len(merged)-len(existing)})")
if args.dry_run:
logger.info("--dry-run: not writing to intent_train.json")
return
# Step 6: Write merged data
with open(INTENT_TRAIN_FILE, "w", encoding="utf-8") as f:
json.dump(merged, f, ensure_ascii=False, indent=2)
logger.info(f"✅ Written to {INTENT_TRAIN_FILE}")
# Step 7: Cleanup scraped temp files (keep for audit)
logger.info("Scraped source files kept in datasets/ for audit trail.")
if __name__ == "__main__":
main()