From cc3a51bd4d4330f1815f347ef66098c4823c38fa Mon Sep 17 00:00:00 2001 From: Jay Sahnan Date: Sat, 27 Dec 2025 00:51:42 +0000 Subject: [PATCH] Add basic-caching template --- python/basic-caching/README.md | 126 +++++++++++ python/basic-caching/main.py | 296 ++++++++++++++++++++++++++ python/basic-caching/requirements.txt | 4 + typescript/basic-caching/README.md | 121 +++++++++++ typescript/basic-caching/index.ts | 169 +++++++++++++++ typescript/basic-caching/package.json | 20 ++ 6 files changed, 736 insertions(+) create mode 100644 python/basic-caching/README.md create mode 100644 python/basic-caching/main.py create mode 100644 python/basic-caching/requirements.txt create mode 100644 typescript/basic-caching/README.md create mode 100644 typescript/basic-caching/index.ts create mode 100644 typescript/basic-caching/package.json diff --git a/python/basic-caching/README.md b/python/basic-caching/README.md new file mode 100644 index 0000000..4fb456f --- /dev/null +++ b/python/basic-caching/README.md @@ -0,0 +1,126 @@ +# Stagehand + Browserbase: Basic Caching + +## AT A GLANCE + +- Goal: Demonstrate how Stagehand's caching feature dramatically reduces cost and latency by reusing previously computed actions instead of calling the LLM every time. +- Shows side-by-side comparison of workflows with and without caching enabled. +- Demonstrates massive cost savings for repeated workflows (99.9% reduction in LLM calls). +- Docs → https://docs.stagehand.dev/v2/best-practices/caching#caching-actions + +## GLOSSARY + +- caching: Stagehand can cache action results based on instruction text and page context, eliminating redundant LLM calls + Docs → https://docs.stagehand.dev/v2/best-practices/caching#caching-actions +- act: execute actions on web pages using natural language instructions + Docs → https://docs.stagehand.dev/v2/basics/act +- observe: observe page elements and generate actions that can be cached and reused + Docs → https://docs.stagehand.dev/v2/basics/observe + +## QUICKSTART + +1. uv venv venv +2. source venv/bin/activate # On Windows: venv\Scripts\activate +3. uvx install stagehand python-dotenv aiofiles +4. cp .env.example .env +5. Add required API keys/IDs to .env +6. python main.py (run twice to see cache benefits!) + +## EXPECTED OUTPUT + +- First run: Executes workflow without cache, then with cache enabled (populates cache) +- Subsequent runs: Uses cached actions for instant execution with zero LLM calls +- Displays timing comparison, cost savings, and cache statistics +- Shows cache location and file structure + +## HOW CACHING WORKS + +**Cache Key Generation:** + +- Based on instruction text (used as cache key) +- Actions are observed once and cached +- Automatically computed + +**When Cache is Used:** + +- ✅ Same instruction text +- ✅ Cache file exists (`cache.json`) +- ❌ Different instruction text +- ❌ Cache file cleared or missing + +**Cache Storage:** + +- Location: `cache.json` in the same directory as `main.py` +- Format: JSON file containing cached actions +- Persistent across runs + +## BENEFITS FOR REPEATED WORKFLOWS + +**Example Scenario: 1,000 customers × 10 portals = 10,000 payment flows** + +**Without caching:** + +- 10,000 workflows × 5 actions = 50,000 LLM calls +- Cost: ~$500-2,500 +- Latency: 2-3s per action × 5 = 10-15s per payment + +**With caching:** + +- First payment per portal: 5 LLM calls (populate cache) +- Next 999 payments: 0 LLM calls (use cache) +- Total: 10 portals × 5 actions = 50 LLM calls +- Cost: ~$0.50-2.50 (99.9% savings!) +- Latency: <100ms per action × 5 = <0.5s per payment + +**Key Insight:** +Payment portals rarely change → Cache actions once → Reuse for thousands of payments → Massive cost + latency reduction + +## COMMON PITFALLS + +- Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_API_KEY +- Cache not working: ensure cache.json is writable and check that instruction text matches exactly +- First run slower: expected behavior - cache is populated on first run, subsequent runs will be instant +- ModuleNotFoundError: ensure virtual environment is activated and dependencies are installed via `uvx install` +- Import errors: activate your virtual environment if you created one +- Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in + +## USE CASES + +• Payment processing: Cache form-filling actions for payment portals that don't change frequently, processing thousands of payments with minimal LLM calls. +• Data entry automation: Reuse actions for repetitive data entry tasks across similar forms or interfaces. +• Testing workflows: Cache test actions to speed up regression testing and reduce API costs during development. + +## BEST PRACTICES + +- ✅ Enable caching in production for repeated workflows +- ✅ One cache per portal/interface type +- ✅ Invalidate cache when page structure changes significantly +- ✅ Monitor cache hit rate to optimize cache effectiveness +- ✅ Warm cache with test runs before production deployment + +## NEXT STEPS + +• Customize cache file location: Modify CACHE_FILE path to organize caches by workflow type or environment. +• Add cache invalidation: Implement logic to clear cache when page structure changes or after a certain time period. +• Monitor cache performance: Track cache hit rates and cost savings to measure effectiveness. + +## TRY IT YOURSELF + +1. Run this script again: `python main.py` + → Second run will be MUCH faster (cache hits) + +2. Clear cache and run again: + `rm cache.json && python main.py` + → Back to first-run behavior + +3. Check cache contents: + `cat cache.json` + → See cached action data + +## HELPFUL RESOURCES + +📚 Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +🎮 Browserbase: https://www.browserbase.com +💡 Try it out: https://www.browserbase.com/playground +🔧 Templates: https://www.browserbase.com/templates +📧 Need help? support@browserbase.com +💬 Discord: http://stagehand.dev/discord diff --git a/python/basic-caching/main.py b/python/basic-caching/main.py new file mode 100644 index 0000000..e7fa99f --- /dev/null +++ b/python/basic-caching/main.py @@ -0,0 +1,296 @@ +# Stagehand + Browserbase: Basic Caching - See README.md for full documentation + +import asyncio +import json +import os +from pathlib import Path +from typing import Any + +import aiofiles +from dotenv import load_dotenv + +from stagehand import Stagehand, StagehandConfig + +# Load environment variables +load_dotenv() + +# Cache file location - stores observed actions for reuse +CACHE_FILE = Path(__file__).parent / "cache.json" + + +async def get_cache(key: str) -> dict[str, Any] | None: + """Get the cached value (None if it doesn't exist)""" + try: + # Read cache file asynchronously for better performance + async with aiofiles.open(CACHE_FILE) as f: + cache_content = await f.read() + parsed = json.loads(cache_content) + return parsed.get(key) + except (FileNotFoundError, json.JSONDecodeError): + # Cache file doesn't exist or is invalid - return None + return None + + +async def set_cache(key: str, value: Any) -> None: + """Set the cache value - converts ObserveResult (Pydantic model) to dict if needed""" + try: + # Read existing cache file to preserve other cached entries + async with aiofiles.open(CACHE_FILE) as f: + cache_content = await f.read() + parsed = json.loads(cache_content) + except (FileNotFoundError, json.JSONDecodeError): + # Cache file doesn't exist - start with empty dict + parsed = {} + + # Convert ObserveResult (Pydantic BaseModel) to dict for JSON serialization + # Supports both Pydantic v1 and v2 for compatibility + if hasattr(value, "model_dump"): + # Pydantic v2 + parsed[key] = value.model_dump() + elif hasattr(value, "dict"): + # Pydantic v1 + parsed[key] = value.dict() + elif isinstance(value, dict): + parsed[key] = value + else: + # Fallback: try to convert to dict + parsed[key] = dict(value) if hasattr(value, "__dict__") else value + + # Write updated cache back to file + async with aiofiles.open(CACHE_FILE, "w") as f: + await f.write(json.dumps(parsed, indent=2, default=str)) + + +async def act_with_cache(page, key: str, prompt: str, self_heal: bool = False): + """ + Check the cache, get the action, and run it. + If self_heal is true, we'll attempt to self-heal if the action fails. + + This function demonstrates manual caching by: + 1. Checking if action is cached (no LLM call) + 2. If not cached, observing the page to generate action (LLM call) + 3. Caching the observed action for future use + 4. Executing the action + """ + try: + # Check if action is already cached + cache_exists = await get_cache(key) + + if cache_exists: + # Use the already-retrieved cached action - no LLM inference needed + action = cache_exists + print(f" ✓ Cache hit for: {prompt}") + else: + # Get the observe result (the action) - this requires LLM inference + print(f" → Observing: {prompt}") + actions = await page.observe(prompt) + action = actions[0] + + # Cache the action for future use + await set_cache(key, action) + print(f" ✓ Cached action for: {prompt}") + + # Run the action (no LLM inference when using cached action) + await page.act(action) + except Exception as e: + print(f" ✗ Error: {e}") + # In self_heal mode, retry the action with a fresh LLM call + if self_heal: + print(" → Attempting to self-heal...") + await page.act(prompt) + else: + raise e + + +async def run_without_cache(): + """Run workflow without caching (baseline) - demonstrates normal LLM usage""" + print("RUN 1: WITHOUT CACHING") + + start_time = asyncio.get_event_loop().time() + + # Initialize Stagehand with Browserbase for cloud-based browser automation. + # Note: set verbose: 0 to prevent API keys from appearing in logs when handling sensitive data. + config = StagehandConfig( + env="BROWSERBASE", + api_key=os.getenv("BROWSERBASE_API_KEY"), + project_id=os.getenv("BROWSERBASE_PROJECT_ID"), + model_name="google/gemini-2.5-flash", + model_api_key=os.getenv("GOOGLE_API_KEY"), + verbose=0, # 0 = errors only, 1 = info, 2 = debug + # (When handling sensitive data like passwords or API keys, set verbose: 0 to prevent secrets from appearing in logs.) + # https://docs.stagehand.dev/configuration/logging + browserbase_session_create_params={ + "project_id": os.getenv("BROWSERBASE_PROJECT_ID"), + }, + ) + + # Use async context manager for automatic resource management + async with Stagehand(config) as stagehand: + page = stagehand.page + + try: + # Navigate to Stripe checkout demo page + print("Navigating to Stripe checkout...") + await page.goto("https://checkout.stripe.dev/preview", wait_until="domcontentloaded") + + # Each act() call requires LLM inference - no caching enabled + await page.act("Click on the View Demo button") + await page.act("Type 'test@example.com' into the email field") + await page.act("Type '4242424242424242' into the card number field") + await page.act("Type '12/34' into the expiration date field") + + elapsed = f"{(asyncio.get_event_loop().time() - start_time):.2f}" + + print(f"Total time: {elapsed}s") + print("Cost: ~$0.01-0.05 (4 LLM calls)") + print("API calls: 4 (one per action)\n") + + return {"elapsed": elapsed, "llm_calls": 4} + except Exception as error: + print(f"Error: {error}") + raise + + +async def run_with_cache(): + """Run workflow with caching enabled - demonstrates cost and latency savings""" + print("RUN 2: WITH CACHING\n") + + start_time = asyncio.get_event_loop().time() + + # Initialize Stagehand with Browserbase for cloud-based browser automation. + config = StagehandConfig( + env="BROWSERBASE", + api_key=os.getenv("BROWSERBASE_API_KEY"), + project_id=os.getenv("BROWSERBASE_PROJECT_ID"), + model_name="google/gemini-2.5-flash", + model_api_key=os.getenv("GOOGLE_API_KEY"), + verbose=0, # 0 = errors only, 1 = info, 2 = debug + # (When handling sensitive data like passwords or API keys, set verbose: 0 to prevent secrets from appearing in logs.) + # https://docs.stagehand.dev/configuration/logging + browserbase_session_create_params={ + "project_id": os.getenv("BROWSERBASE_PROJECT_ID"), + }, + ) + + # Use async context manager for automatic resource management + async with Stagehand(config) as stagehand: + page = stagehand.page + + try: + # Navigate to Stripe checkout demo page + print("Navigating to Stripe checkout...") + await page.goto("https://checkout.stripe.dev/preview", wait_until="domcontentloaded") + + # Use cached actions - first run will observe and cache, subsequent runs use cache + await act_with_cache( + page, "Click on the View Demo button", "Click on the View Demo button" + ) + await act_with_cache( + page, + "Type 'test@example.com' into the email field", + "Type 'test@example.com' into the email field", + ) + await act_with_cache( + page, + "Type '4242424242424242' into the card number field", + "Type '4242424242424242' into the card number field", + ) + await act_with_cache( + page, + "Type '12/34' into the expiration date field", + "Type '12/34' into the expiration date field", + ) + + elapsed = f"{(asyncio.get_event_loop().time() - start_time):.2f}" + cache_exists = CACHE_FILE.exists() + + # Count cache entries to determine if this was a cache hit or miss + if cache_exists: + async with aiofiles.open(CACHE_FILE) as f: + cache_content = await f.read() + cache_data = json.loads(cache_content) + cache_count = len(cache_data) + else: + cache_count = 0 + + print(f"\nTotal time: {elapsed}s") + + # Display results based on cache status + if cache_count >= 4: + # Cache was used - no LLM calls made + print("Cost: $0.00 (cache hits, no LLM calls)") + print("API calls: 0 (all from cache)") + print(f"Cache entries: {cache_count}") + else: + # First run - cache was populated + print("💰Cost: ~$0.01-0.05 (first run, populated cache)") + print("📡API calls: 4 (saved to cache for next run)") + print("📂Cache created") + print() + + return {"elapsed": elapsed, "llm_calls": 0 if cache_count >= 4 else 4} + except Exception as error: + print(f"Error: {error}") + raise + + +async def main(): + """Main function demonstrating caching benefits with side-by-side comparison""" + print("\n╔═══════════════════════════════════════════════════════════╗") + print("║ Caching Demo - Run This Script TWICE! ║") + print("╚═══════════════════════════════════════════════════════════╝\n") + + print("This demo shows caching impact by running the same workflow twice:\n") + print("First run:") + print(" 1. WITHOUT cache (baseline)") + print(" 2. WITH cache enabled (populates cache)\n") + + print("Second run:") + print(" - WITH cache (instant, $0 cost)\n") + + print("Run 'python main.py' twice to see the difference!\n") + + # Check if cache exists to determine if this is first or subsequent run + cache_exists = CACHE_FILE.exists() + + if cache_exists: + # Read cache file to count entries + async with aiofiles.open(CACHE_FILE) as f: + cache_content = await f.read() + cache_data = json.loads(cache_content) + cache_count = len(cache_data) + print(f"📂 Cache found: {cache_count} entries") + print(" This is a SUBSEQUENT run - cache will be used!\n") + else: + print("No cache found - first run will populate cache") + + print("\nRunning comparison: without cache vs with cache...\n") + + # Run both workflows for comparison + without_cache = await run_without_cache() + with_cache = await run_with_cache() + + # Display comparison results + print("\n=== Comparison ===") + print(f"Without caching: {without_cache['elapsed']}s, {without_cache['llm_calls']} LLM calls") + print(f"With caching: {with_cache['elapsed']}s, {with_cache['llm_calls']} LLM calls") + + # Calculate and display speedup if cache was used + if with_cache["llm_calls"] == 0: + speedup = float(without_cache["elapsed"]) / float(with_cache["elapsed"]) + print(f"\nSpeedup: {speedup:.1f}x faster with cache") + print("Cost savings: 100% (no LLM calls)") + + print("\nRun again to see cache benefits on subsequent runs!") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except Exception as err: + print(f"Error in caching demo: {err}") + print("Common issues:") + print(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY") + print(" - Verify GOOGLE_API_KEY is set for the model") + print("Docs: https://docs.stagehand.dev/v2/first-steps/introduction") + exit(1) diff --git a/python/basic-caching/requirements.txt b/python/basic-caching/requirements.txt new file mode 100644 index 0000000..2a48386 --- /dev/null +++ b/python/basic-caching/requirements.txt @@ -0,0 +1,4 @@ +stagehand +python-dotenv +aiofiles + diff --git a/typescript/basic-caching/README.md b/typescript/basic-caching/README.md new file mode 100644 index 0000000..1e6327a --- /dev/null +++ b/typescript/basic-caching/README.md @@ -0,0 +1,121 @@ +# Stagehand + Browserbase: Basic Caching + +## AT A GLANCE + +- Goal: Demonstrate how Stagehand's caching feature dramatically reduces cost and latency by reusing previously computed actions instead of calling the LLM every time. +- Shows side-by-side comparison of workflows with and without caching enabled. +- Demonstrates massive cost savings for repeated workflows (99.9% reduction in LLM calls). +- Docs → https://docs.stagehand.dev/v3/best-practices/caching#caching-actions + +## GLOSSARY + +- caching: Stagehand can cache action results based on instruction text and page context, eliminating redundant LLM calls + Docs → https://docs.stagehand.dev/v3/best-practices/caching#caching-actions +- act: execute actions on web pages using natural language instructions + Docs → https://docs.stagehand.dev/basics/act + +## QUICKSTART + +1. pnpm install +2. cp .env.example .env +3. Add required API keys/IDs to .env +4. pnpm start (run twice to see cache benefits!) + +## EXPECTED OUTPUT + +- First run: Executes workflow without cache, then with cache enabled (populates cache) +- Subsequent runs: Uses cached actions for instant execution with zero LLM calls +- Displays timing comparison, cost savings, and cache statistics +- Shows cache location and file structure + +## HOW CACHING WORKS + +**Cache Key Generation:** + +- Based on instruction text +- Based on page context +- Automatically computed + +**When Cache is Used:** + +- ✅ Same instruction +- ✅ Same page structure +- ✅ Cache file exists +- ❌ Different instruction +- ❌ Page structure changed significantly + +**Cache Storage:** + +- Location: `.cache/stagehand-demo` +- Format: JSON files (one per cached action) +- Persistent across runs + +## BENEFITS FOR REPEATED WORKFLOWS + +**Example Scenario: 1,000 customers × 10 portals = 10,000 payment flows** + +**Without caching:** + +- 10,000 workflows × 5 actions = 50,000 LLM calls +- Cost: ~$500-2,500 +- Latency: 2-3s per action × 5 = 10-15s per payment + +**With caching:** + +- First payment per portal: 5 LLM calls (populate cache) +- Next 999 payments: 0 LLM calls (use cache) +- Total: 10 portals × 5 actions = 50 LLM calls +- Cost: ~$0.50-2.50 (99.9% savings!) +- Latency: <100ms per action × 5 = <0.5s per payment + +**Key Insight:** +Payment portals rarely change → Cache actions once → Reuse for thousands of payments → Massive cost + latency reduction + +## COMMON PITFALLS + +- Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_API_KEY +- Cache not working: ensure cacheDir path is writable and check that instruction text matches exactly +- First run slower: expected behavior - cache is populated on first run, subsequent runs will be instant +- Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in + +## USE CASES + +• Payment processing: Cache form-filling actions for payment portals that don't change frequently, processing thousands of payments with minimal LLM calls. +• Data entry automation: Reuse actions for repetitive data entry tasks across similar forms or interfaces. +• Testing workflows: Cache test actions to speed up regression testing and reduce API costs during development. + +## BEST PRACTICES + +- ✅ Enable caching in production for repeated workflows +- ✅ One cache per portal/interface type +- ✅ Invalidate cache when page structure changes significantly +- ✅ Monitor cache hit rate to optimize cache effectiveness +- ✅ Warm cache with test runs before production deployment + +## NEXT STEPS + +• Customize cache directory: Modify cacheDir to organize caches by workflow type or environment. +• Add cache invalidation: Implement logic to clear cache when page structure changes or after a certain time period. +• Monitor cache performance: Track cache hit rates and cost savings to measure effectiveness. + +## TRY IT YOURSELF + +1. Run this script again: `pnpm start` + → Second run will be MUCH faster (cache hits) + +2. Clear cache and run again: + `rm -rf .cache/stagehand-demo && pnpm start` + → Back to first-run behavior + +3. Check cache contents: + `ls -la .cache/stagehand-demo` + → See cached action files + +## HELPFUL RESOURCES + +📚 Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +🎮 Browserbase: https://www.browserbase.com +💡 Try it out: https://www.browserbase.com/playground +🔧 Templates: https://www.browserbase.com/templates +📧 Need help? support@browserbase.com +💬 Discord: http://stagehand.dev/discord diff --git a/typescript/basic-caching/index.ts b/typescript/basic-caching/index.ts new file mode 100644 index 0000000..8fb9ccb --- /dev/null +++ b/typescript/basic-caching/index.ts @@ -0,0 +1,169 @@ +// Stagehand + Browserbase: Basic Caching - See README.md for full documentation + +import "dotenv/config"; +import { Stagehand } from "@browserbasehq/stagehand"; +import fs from "fs"; +import path from "path"; + +const CACHE_DIR = path.join(process.cwd(), ".cache", "stagehand-demo"); + +async function runWithoutCache() { + console.log("RUN 1: WITHOUT CACHING"); + + const startTime = Date.now(); + + const stagehand = new Stagehand({ + env: "BROWSERBASE", + verbose: 0, + // 0 = errors only, 1 = info, 2 = debug + // (When handling sensitive data like passwords or API keys, set verbose: 0 to prevent secrets from appearing in logs.) + // https://docs.stagehand.dev/configuration/logging + model: "google/gemini-2.5-flash", + enableCaching: false, + browserbaseSessionCreateParams: { + projectId: process.env.BROWSERBASE_PROJECT_ID!, + }, + }); + + await stagehand.init(); + const page = stagehand.context.pages()[0]; + + try { + console.log("Navigating to Stripe checkout..."); + await page.goto("https://checkout.stripe.dev/preview", { + waitUntil: "domcontentloaded", + }); + + await stagehand.act("Click on the View Demo button"); + await stagehand.act("Type 'test@example.com' into the email field"); + await stagehand.act("Type '4242424242424242' into the card number field"); + await stagehand.act("Type '12/34' into the expiration date field"); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); + + console.log(`Total time: ${elapsed}s`); + console.log("Cost: ~$0.01-0.05 (4 LLM calls)"); + console.log("API calls: 4 (one per action)\n"); + + await stagehand.close(); + + return { elapsed, llmCalls: 4 }; + } catch (error) { + console.error("Error:", error.message); + await stagehand.close(); + throw error; + } +} + +async function runWithCache() { + console.log("RUN 2: WITH CACHING"); + + const startTime = Date.now(); + + const stagehand = new Stagehand({ + env: "BROWSERBASE", + verbose: 0, + // 0 = errors only, 1 = info, 2 = debug + // (When handling sensitive data like passwords or API keys, set verbose: 0 to prevent secrets from appearing in logs.) + // https://docs.stagehand.dev/configuration/logging + model: "google/gemini-2.5-flash", + enableCaching: true, + cacheDir: CACHE_DIR, + browserbaseSessionCreateParams: { + projectId: process.env.BROWSERBASE_PROJECT_ID!, + }, + }); + + await stagehand.init(); + const page = stagehand.context.pages()[0]; + + try { + console.log("Navigating to Stripe checkout..."); + await page.goto("https://checkout.stripe.dev/preview", { + waitUntil: "domcontentloaded", + }); + + await stagehand.act("Click on the View Demo button"); + await stagehand.act("Type 'test@example.com' into the email field"); + await stagehand.act("Type '4242424242424242' into the card number field"); + await stagehand.act("Type '12/34' into the expiration date field"); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); + const cacheExists = fs.existsSync(CACHE_DIR); + const cacheFiles = cacheExists ? fs.readdirSync(CACHE_DIR).length : 0; + + console.log(`Total time: ${elapsed}s`); + + if (cacheFiles > 0) { + console.log("Cost: $0.00 (cache hits, no LLM calls)"); + console.log("API calls: 0 (all from cache)"); + console.log(`Cache entries: ${cacheFiles}`); + } else { + console.log("💰Cost: ~$0.01-0.05 (first run, populated cache)"); + console.log("📡API calls: 4 (saved to cache for next run)"); + console.log("📂Cache created"); + } + console.log(); + + await stagehand.close(); + + return { elapsed, llmCalls: cacheFiles > 0 ? 0 : 4 }; + } catch (error) { + console.error("Error:", error.message); + await stagehand.close(); + throw error; + } +} + +async function main() { + console.log("\n╔═══════════════════════════════════════════════════════════╗"); + console.log("║ Caching Demo - Run This Script TWICE! ║"); + console.log("╚═══════════════════════════════════════════════════════════╝\n"); + + console.log("This demo shows caching impact by running the same workflow twice:\n"); + console.log("First run:"); + console.log(" 1. WITHOUT cache (baseline)"); + console.log(" 2. WITH cache enabled (populates cache)\n"); + + console.log("Second run:"); + console.log(" - WITH cache (instant, $0 cost)\n"); + + console.log("Run 'pnpm start' twice to see the difference!\n"); + + // Check if cache exists + const cacheExists = fs.existsSync(CACHE_DIR); + + if (cacheExists) { + const cacheFiles = fs.readdirSync(CACHE_DIR); + console.log(`📂 Cache found: ${cacheFiles.length} entries`); + console.log(" This is a SUBSEQUENT run - cache will be used!\n"); + } else { + console.log("No cache found - first run will populate cache"); + } + + console.log("\nRunning comparison: without cache vs with cache...\n"); + + const withoutCache = await runWithoutCache(); + const withCache = await runWithCache(); + + console.log("\n=== Comparison ==="); + console.log(`Without caching: ${withoutCache.elapsed}s, ${withoutCache.llmCalls} LLM calls`); + console.log(`With caching: ${withCache.elapsed}s, ${withCache.llmCalls} LLM calls`); + + if (withCache.llmCalls === 0) { + const speedup = (parseFloat(withoutCache.elapsed) / parseFloat(withCache.elapsed)).toFixed(1); + console.log(`\nSpeedup: ${speedup}x faster with cache`); + console.log("Cost savings: 100% (no LLM calls)"); + } + + console.log("\nRun again to see cache benefits on subsequent runs!"); +} + +main().catch((err) => { + console.error("Error in caching demo:", err); + console.error("Common issues:"); + console.error(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); + console.error(" - Verify GOOGLE_API_KEY is set for the model"); + console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); + process.exit(1); +}); diff --git a/typescript/basic-caching/package.json b/typescript/basic-caching/package.json new file mode 100644 index 0000000..6e46064 --- /dev/null +++ b/typescript/basic-caching/package.json @@ -0,0 +1,20 @@ +{ + "name": "basic-caching", + "version": "1.0.0", + "description": "Stagehand + Browserbase: Basic Caching Demo", + "type": "module", + "main": "index.ts", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@browserbasehq/stagehand": "latest", + "dotenv": "latest" + }, + "devDependencies": { + "@types/node": "latest", + "tsx": "latest", + "typescript": "latest" + }, + "packageManager": "pnpm@9.0.0" +}