Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |