"""tools.py — 8 scraping + export tools. Version 8.0.0 | 4 April 2026. ZERO for/while/if.""" from langchain_core.tools import tool import os import json import csv # ═══════════════════════════════════════════════ # DEBUG SWITCH — True = prints ON, False = OFF # ═══════════════════════════════════════════════ DEBUG = True debug = {True: print, False: lambda *a, **k: None}[DEBUG] RAW_DATA_PATH = "/tmp/last_scrape.json" CSV_DATA_PATH = "/tmp/last_scrape.csv" @tool def search_for_url(query: str) -> str: """Search the web to find a real URL for a website or business. ALWAYS use this BEFORE scraping to find the correct URL. For Instagram, find the username instead. Args: query: Search query like 'Taj Dubai TripAdvisor' or 'Nike Instagram username'. Returns: Search results with real URLs.""" debug(f"\n>>> TOOL: search_for_url(query='{query}')") from ddgs import DDGS results = list(DDGS().text(query, max_results=5)) debug(f">>> Found {len(results)} results") lines = list(map(lambda r: f"- {r['title']}: {r['href']}", results)) return "\n".join(lines) or "No results found." @tool def apify_search_actors(query: str) -> str: """Search Apify Store for scraping actors by keyword. Apify has 22,000+ actors for ANY website. Args: query: Keywords like 'yelp reviews scraper' or 'linkedin profile'. Returns: Matching actors with name, ID, and user count.""" debug(f"\n>>> TOOL: apify_search_actors(query='{query}')") from apify_client import ApifyClient client = ApifyClient(os.getenv("APIFY_TOKEN")) result = client.store().list(search=query, limit=5).items debug(f">>> Found {len(result)} actors") lines = list(map(lambda a: f"- {a['name']} (ID: {a['username']}/{a['name']}) — {a.get('stats', {}).get('totalUsers', 0)} users", result)) return "\n".join(lines) or "No actors found." @tool def apify_get_actor_info(actor_id: str) -> str: """Get FULL input schema for an Apify actor. This fetches the actor's BUILD to get the REAL schema with field names, types, descriptions, defaults, and required fields. ALWAYS call this before apify_run_actor to know exact input format. Args: actor_id: Actor ID like 'apify/instagram-scraper'. Returns: Actor description + complete input schema with all fields.""" debug(f"\n>>> TOOL: apify_get_actor_info(actor_id='{actor_id}')") import requests from apify_client import ApifyClient client = ApifyClient(os.getenv("APIFY_TOKEN")) # Step 1: Get actor metadata (for internal object ID) actor = client.actor(actor_id).get() debug(f">>> Got actor: {actor.get('name')}, id={actor.get('id')}") # Step 2: Fetch default BUILD — this is where the REAL schema lives # Source: langchain_apify/utils.py get_actor_latest_build() actor_obj_id = actor.get('id', '') build_url = f"https://api.apify.com/v2/acts/{actor_obj_id}/builds/default" debug(f">>> Fetching build schema from: {build_url}") response = requests.get(build_url, timeout=15) build_data = response.json().get('data', {}) actor_def = build_data.get('actorDefinition', {}) # Step 3: Extract input schema (properties, required, descriptions) input_schema = actor_def.get('input', {}) description = actor_def.get('description', actor.get('description', '')) debug(f">>> Schema has {len(input_schema.get('properties', {}))} properties") # Step 4: Format for agent — show ALL fields the actor accepts schema_str = json.dumps(input_schema, indent=2, default=str)[:4000] return f"Actor: {actor.get('name')}\nDescription: {description[:500]}\n\nINPUT SCHEMA (use these EXACT field names):\n{schema_str}" @tool def apify_run_actor(actor_id: str, run_input: dict) -> str: """Run ANY Apify actor by ID with the given input. ALWAYS call apify_get_actor_info first to know the correct input format. Args: actor_id: Actor ID like 'maxcopell/tripadvisor-reviews'. run_input: Input dict matching the actor's schema from apify_get_actor_info. Returns: Scraped results as JSON.""" debug(f"\n>>> TOOL: apify_run_actor(actor_id='{actor_id}', run_input={run_input})") from apify_client import ApifyClient client = ApifyClient(os.getenv("APIFY_TOKEN")) debug(f">>> Starting actor run...") run = client.actor(actor_id).call(run_input=run_input) debug(f">>> Run done, dataset={run.get('defaultDatasetId')}") items = list(client.dataset(run["defaultDatasetId"]).iterate_items(limit=50)) debug(f">>> Got {len(items)} items") # Save raw data for CSV generation json.dump(items, open(RAW_DATA_PATH, "w", encoding="utf-8"), indent=2, default=str) # Auto-generate CSV from raw data _auto_csv(items) output = json.dumps(items, indent=2, default=str)[:15000] debug(f">>> Output: {len(output)} chars") return f"[{len(items)} items scraped]\n\n{output}" def _auto_csv(items): """Auto-generate CSV from scraped items. No if/else.""" first_item = (items[:1] + [{}])[0] headers = list(first_item.keys())[:10] writer = csv.writer(open(CSV_DATA_PATH, "w", newline="", encoding="utf-8")) writer.writerow(headers) list(map(lambda item: writer.writerow(list(map(lambda h: str(item.get(h, ""))[:300], headers))), items)) debug(f">>> Auto-CSV: {CSV_DATA_PATH} ({len(items)} rows, {len(headers)} cols)") @tool def save_as_csv(filename: str, headers: list[str], rows: list[list[str]]) -> str: """Save scraped data as a downloadable CSV file. Args: filename: Name for the CSV file. headers: Column headers like ['Reviewer', 'Rating', 'Review', 'Date']. rows: List of rows, each row is a list of cell values. Returns: Confirmation message.""" debug(f"\n>>> TOOL: save_as_csv(filename='{filename}', {len(headers)} cols, {len(rows)} rows)") path = f"/tmp/{filename.replace(' ', '_')}.csv" writer = csv.writer(open(path, "w", newline="", encoding="utf-8")) writer.writerow(headers) list(map(writer.writerow, rows)) debug(f">>> Saved CSV: {path}") return f"CSV saved: {path} ({len(rows)} rows, {len(headers)} columns)" def get_all_tools(): """Return all 5 tools with error handling enabled.""" tools = [search_for_url, apify_search_actors, apify_get_actor_info, apify_run_actor, save_as_csv] # Enable error handling — tool errors return as string, not crash list(map(lambda t: setattr(t, 'handle_tool_error', True), tools)) debug(f">>> tools.py: {len(tools)} tools ready (handle_tool_error=True)") list(map(lambda t: debug(f">>> - {t.name}"), tools)) return tools