diff --git a/.gitignore b/.gitignore index aba53cd..bddbc64 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,37 @@ .env cursorrules.md .cursorrules + +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ +env/ +*.egg-info/ +dist/ +build/ +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..225bbda --- /dev/null +++ b/.prettierignore @@ -0,0 +1,17 @@ +node_modules +.pnpm-store +dist +build +.next +.nuxt +.cache +coverage +*.min.js +*.min.css +package-lock.json +yarn.lock +pnpm-lock.yaml +.DS_Store +.env +.env.local + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4a89cfa --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/README.md b/README.md index 661687f..30d3b2b 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,17 @@ A comprehensive collection of ready-to-use automation templates demonstrating th ## ๐Ÿ“ Available Templates ### TypeScript Templates (`/typescript/`) + - **context** - AI-powered context switching and page analysis - **CUA** - Computer Use Agent for autonomous web browsing and decision-making -- **formFilling** - Automated form submission and data entry +- **formFilling** - Automated form submission and data entry - **giftfinder** - AI-powered product discovery and recommendation - **pickleball** - Court booking automation with user interaction - **proxies** - Proxy testing and geolocation verification - **realEstateCheck** - License verification and data extraction ### Python Templates (`/python/`) + - **context** - AI-powered context switching and page analysis - **CUA** - Computer Use Agent for autonomous web browsing and decision-making - **formFilling** - Automated form submission and data entry @@ -30,6 +32,7 @@ A comprehensive collection of ready-to-use automation templates demonstrating th - **realEstateCheck** - License verification and data extraction > **๐Ÿ“– Each template includes a comprehensive README with:** +> > - Detailed setup instructions > - Required environment variables > - Use cases and examples @@ -48,16 +51,20 @@ A comprehensive collection of ready-to-use automation templates demonstrating th ## ๐Ÿ“š Resources ### Documentation + - **Stagehand Docs**: https://docs.stagehand.dev/v3/first-steps/introduction - **Browserbase Docs**: https://docs.browserbase.com - **API Reference**: https://docs.browserbase.com/api ### Support + - **Community**: Join our Discord community +- **Discord**: http://stagehand.dev/discord - **Email Support**: support@browserbase.com - **GitHub Issues**: Report bugs and request features ### Examples & Tutorials + - **Getting Started Guide**: Learn the basics of Stagehand - **Advanced Patterns**: Complex automation workflows - **Best Practices**: Tips for reliable automation @@ -72,6 +79,7 @@ We welcome contributions! Here's how you can help: 4. **Share Templates**: Create and share your own templates ### Template Guidelines + - Follow the established structure and naming conventions - Include comprehensive README documentation - Add proper error handling and logging diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..1670f4a --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,30 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import prettierConfig from "eslint-config-prettier"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + prettierConfig, + { + files: ["**/*.{ts,tsx,js,jsx}"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, + { + ignores: ["node_modules/**", "dist/**", "build/**", "*.config.js"], + }, +); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..68b865e --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "stagehand-templates", + "version": "1.0.0", + "description": "Stagehand + Browserbase Templates", + "private": true, + "type": "module", + "scripts": { + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", + "lint": "eslint \"**/*.{ts,tsx,js,jsx}\"", + "lint:fix": "eslint \"**/*.{ts,tsx,js,jsx}\" --fix", + "lint:python": "uvx ruff check python/", + "lint:python:fix": "uvx ruff check --fix python/", + "format:python": "uvx ruff format python/" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.50.1", + "@typescript-eslint/parser": "^8.50.1", + "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", + "prettier": "^3.2.5", + "typescript-eslint": "^8.50.1" + }, + "packageManager": "pnpm@9.0.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0dc8069 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,944 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^8.50.1 + version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2)(typescript@5.9.3) + eslint: + specifier: ^9.39.2 + version: 9.39.2 + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.39.2) + prettier: + specifier: ^3.2.5 + version: 3.7.4 + typescript-eslint: + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2)(typescript@5.9.3) + +packages: + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.50.1: + resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)': + dependencies: + eslint: 9.39.2 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + eslint: 9.39.2 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.50.1': {} + + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.50.1(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + eslint-visitor-keys: 4.2.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.2): + dependencies: + eslint: 9.39.2 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picomatch@4.0.3: {} + + prelude-ls@1.2.1: {} + + prettier@3.7.4: {} + + punycode@2.3.1: {} + + resolve-from@4.0.0: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.50.1(eslint@9.39.2)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b30c9e9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[tool.ruff] +# Set the maximum line length +line-length = 100 + +# Exclude common directories +exclude = [ + ".git", + "__pycache__", + ".venv", + "venv", + "node_modules", + "dist", + "build", +] + +[tool.ruff.lint] +# Enable a comprehensive set of rules +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "SIM", # flake8-simplify +] + +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "N815", # mixedCase in class scope (common in Pydantic models for API compatibility) + "N806", # mixedCase in function scope (template/example code) + "E712", # equality comparison to True (sometimes needed for clarity) + "B904", # raise without from (template code, less strict) +] + +# Make style issues warnings instead of errors (like ESLint) +# This allows template/example code to be more flexible +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # unused imports in __init__.py are common + +[tool.ruff.lint.isort] +# Use double quotes +known-first-party = ["stagehand"] + +[tool.ruff.format] +# Use double quotes for strings +quote-style = "double" +# Indent with spaces +indent-style = "space" +# Use 2 spaces for indentation +skip-magic-trailing-comma = false +line-ending = "auto" + diff --git a/python/business-lookup/README.md b/python/business-lookup/README.md index 3c0a027..8c815a8 100644 --- a/python/business-lookup/README.md +++ b/python/business-lookup/README.md @@ -1,26 +1,29 @@ # Stagehand + Browserbase: Business Lookup with Agent ## AT A GLANCE + - Goal: Automate business registry searches using an autonomous AI agent with computer-use capabilities. - Uses Stagehand Agent in CUA mode to navigate complex UI elements, apply filters, and extract structured business data. - Demonstrates extraction with Pydantic schema validation for consistent data retrieval. - Docs โ†’ https://docs.stagehand.dev/basics/agent ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent - extract: extract structured data from web pages using natural language instructions Docs โ†’ https://docs.stagehand.dev/basics/extract ## QUICKSTART -1) python -m venv venv -2) source venv/bin/activate # On Windows: venv\Scripts\activate -3) uvx install stagehand python-dotenv pydantic -4) cp .env.example .env -5) Add required API keys/IDs to .env -6) python main.py + +1. uv venv venv +2. source venv/bin/activate # On Windows: venv\Scripts\activate +3. uvx install stagehand python-dotenv pydantic +4. cp .env.example .env # Add required API keys/IDs to .env +5. python main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Displays live session link for monitoring - Navigates to SF Business Registry search page @@ -31,6 +34,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_API_KEY - Google API access: ensure you have access to Google's gemini-2.5-computer-use-preview-10-2025 model @@ -39,19 +43,22 @@ - Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in ## USE CASES + โ€ข Business verification: Automate registration status checks, license validation, and compliance verification for multiple businesses. โ€ข Data enrichment: Collect structured business metadata (NAICS codes, addresses, ownership) for research or CRM updates. โ€ข Due diligence: Streamline background checks by autonomously searching and extracting business registration details from public registries. ## NEXT STEPS + โ€ข Parameterize search: Accept business names as command-line arguments or from a CSV file for batch processing. โ€ข Expand extraction: Add support for additional fields like tax status, licenses, or historical registration changes. โ€ข Multi-registry support: Extend agent to search across multiple city or state business registries with routing logic. ## 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 +๐Ÿ“š 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/business-lookup/main.py b/python/business-lookup/main.py index 9f1b2bd..7435868 100644 --- a/python/business-lookup/main.py +++ b/python/business-lookup/main.py @@ -1,12 +1,13 @@ # Stagehand + Browserbase: Business Lookup with Agent - See README.md for full documentation -import os import asyncio import json +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field -from typing import Optional + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -14,9 +15,10 @@ # Business search variables business_name = "Jalebi Street" + async def main(): print("Starting business lookup...") - + # 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( @@ -28,8 +30,8 @@ async def main(): browserbase_session_create_params={ "project_id": os.environ.get("BROWSERBASE_PROJECT_ID"), }, - verbose=1 # 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.) + verbose=1, # 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 ) @@ -39,11 +41,11 @@ async def main(): # Initialize browser session to start automation. print("Stagehand initialized successfully") session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Live View Link: https://browserbase.com/sessions/{session_id}") @@ -54,7 +56,7 @@ async def main(): await page.goto( "https://data.sfgov.org/stories/s/Registered-Business-Lookup/k6sk-2y6w/", wait_until="domcontentloaded", - timeout=60000 + timeout=60000, ) # Create agent with computer use capabilities for autonomous business search. @@ -73,34 +75,36 @@ async def main(): result = await agent.execute( instruction=f'Find and look up the business "{business_name}" in the SF Business Registry. Use the DBA Name filter to search for "{business_name}", apply the filter, and click on the business row to view detailed information. Scroll towards the right to see the NAICS code.', max_steps=30, - auto_screenshot=True + auto_screenshot=True, ) if not result.success: raise Exception("Agent failed to complete the search") - + print("Agent completed search successfully") # Extract comprehensive business information after agent completes the search. # Using structured schema ensures consistent data extraction even if page layout changes. print("Extracting business information...") - + # Define schema using Pydantic class BusinessInfo(BaseModel): dba_name: str = Field(..., description="DBA Name") - ownership_name: Optional[str] = Field(None, description="Ownership Name") + ownership_name: str | None = Field(None, description="Ownership Name") business_account_number: str = Field(..., description="Business Account Number") - location_id: Optional[str] = Field(None, description="Location Id") - street_address: Optional[str] = Field(None, description="Street Address") - business_start_date: Optional[str] = Field(None, description="Business Start Date") - business_end_date: Optional[str] = Field(None, description="Business End Date") - neighborhood: Optional[str] = Field(None, description="Neighborhood") + location_id: str | None = Field(None, description="Location Id") + street_address: str | None = Field(None, description="Street Address") + business_start_date: str | None = Field(None, description="Business Start Date") + business_end_date: str | None = Field(None, description="Business End Date") + neighborhood: str | None = Field(None, description="Neighborhood") naics_code: str = Field(..., description="NAICS Code") - naics_code_description: Optional[str] = Field(None, description="NAICS Code Description") + naics_code_description: str | None = Field( + None, description="NAICS Code Description" + ) business_info = await page.extract( "Extract all visible business information including DBA Name, Ownership Name, Business Account Number, Location Id, Street Address, Business Start Date, Business End Date, Neighborhood, NAICS Code, and NAICS Code Description", - schema=BusinessInfo + schema=BusinessInfo, ) print("Business information extracted:") @@ -123,4 +127,3 @@ class BusinessInfo(BaseModel): print(" - Verify GOOGLE_API_KEY is set for the agent") print("Docs: https://docs.stagehand.dev/v3/first-steps/introduction") exit(1) - diff --git a/python/company-address-finder/README.md b/python/company-address-finder/README.md index fc96677..4018fb2 100644 --- a/python/company-address-finder/README.md +++ b/python/company-address-finder/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Company Address Finder ## AT A GLANCE + - Goal: Automate discovery of company legal information and physical addresses from Terms of Service and Privacy Policy pages. - CUA Agent: Uses autonomous computer-use agent to search for company homepages via Google and navigate to legal documents. - Data Extraction: Extracts structured data including homepage URLs, ToS/Privacy Policy links, and physical mailing addresses. @@ -9,6 +10,7 @@ - Scalable: Supports both sequential and concurrent processing (concurrent requires Startup/Developer plan or higher). ## GLOSSARY + - agent: autonomous AI agent with computer-use capabilities that can navigate websites like a human Docs โ†’ https://docs.stagehand.dev/basics/agent - extract: pull structured data from web pages using natural language instructions and Pydantic schemas @@ -20,15 +22,16 @@ - exponential backoff: retry strategy that increases wait time between attempts for reliability ## QUICKSTART -1) python -m venv venv -2) source venv/bin/activate # On Windows: venv\Scripts\activate -3) uvx install stagehand python-dotenv pydantic -4) cp .env.example .env -5) Add your Browserbase API key, Project ID, and Google Generative AI API key to .env -6) Edit COMPANY_NAMES array in main.py to specify which companies to process -7) python main.py + +1. uv venv venv +2. source venv/bin/activate # On Windows: venv\Scripts\activate +3. uvx install stagehand python-dotenv pydantic +4. cp .env.example .env # Add your Browserbase API key, Project ID, and Google Generative AI API key to .env +5. Edit COMPANY_NAMES array in main.py to specify which companies to process +6. python main.py ## EXPECTED OUTPUT + - Initializes browser session for each company with live view link - Agent navigates to Google and searches for company homepage - Extracts Terms of Service and Privacy Policy links from homepage @@ -38,6 +41,7 @@ - Displays processing status and session closure for each company ## COMMON PITFALLS + - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_GENERATIVE_AI_API_KEY (or GOOGLE_API_KEY) - Google API access: ensure you have access to gemini-2.5-computer-use-preview-10-2025 model - Concurrent processing: MAX_CONCURRENT > 1 requires Browserbase Startup or Developer plan or higher (default is 1 for sequential) @@ -46,12 +50,14 @@ - Session timeouts: long-running batches may hit 900s timeout (adjust browserbase_session_create_params if needed) ## USE CASES + โ€ข Legal compliance research: Collect company addresses and legal document URLs for due diligence, vendor verification, or compliance audits. โ€ข Business intelligence: Build datasets of company locations and legal information for market research or competitive analysis. โ€ข Contact data enrichment: Augment CRM or database records with verified physical addresses extracted from official company documents. โ€ข Multi-company batch processing: Process lists of companies (investors, partners, clients) to gather standardized location data at scale. ## NEXT STEPS + โ€ข Parameterize inputs: Accept company names from CSV files, command-line arguments, or API endpoints for dynamic batch processing. โ€ข Expand extraction: Add support for additional fields like contact emails, phone numbers, business registration numbers, or founding dates. โ€ข Multi-source validation: Cross-reference addresses from multiple pages (About, Contact, Footer) to improve accuracy and confidence. @@ -59,8 +65,10 @@ โ€ข Error handling: Implement more granular error categorization (not found vs. no address vs. extraction failure) for better reporting. ## 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 \ No newline at end of file + +๐Ÿ“š 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/company-address-finder/main.py b/python/company-address-finder/main.py index 5930b6b..f276492 100644 --- a/python/company-address-finder/main.py +++ b/python/company-address-finder/main.py @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Company Address Finder - See README.md for full documentation -import os import asyncio import json +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field, HttpUrl +from stagehand import Stagehand, StagehandConfig + # Load environment variables load_dotenv() @@ -28,7 +30,9 @@ class CompanyData(BaseModel): class TermsOfServiceLink(BaseModel): - terms_of_service_link: HttpUrl = Field(..., description="The URL link to the Terms of Service page") + terms_of_service_link: HttpUrl = Field( + ..., description="The URL link to the Terms of Service page" + ) class PrivacyPolicyLink(BaseModel): @@ -43,7 +47,7 @@ class CompanyAddress(BaseModel): # Handles transient network/page load failures for reliability async def with_retry(fn, description: str, max_retries: int = 3, delay_ms: int = 2000): last_error = None - + for attempt in range(1, max_retries + 1): try: return await fn() @@ -52,7 +56,7 @@ async def with_retry(fn, description: str, max_retries: int = 3, delay_ms: int = if attempt < max_retries: print(f"{description} - Attempt {attempt} failed, retrying in {delay_ms}ms...") await asyncio.sleep(delay_ms / 1000.0) - + raise Exception(f"{description} - Failed after {max_retries} attempts: {last_error}") @@ -61,9 +65,9 @@ async def with_retry(fn, description: str, max_retries: int = 3, delay_ms: int = # Falls back to Privacy Policy if address not found in Terms of Service async def process_company(company_name: str) -> CompanyData: print(f"\nProcessing: {company_name}") - + stagehand = None - + try: # Initialize Stagehand with Browserbase stagehand = Stagehand( @@ -84,27 +88,29 @@ async def process_company(company_name: str) -> CompanyData: "width": 1920, "height": 1080, } - } - } + }, + }, ) ) - + print(f"[{company_name}] Initializing browser session...") await stagehand.init() - - session_id = getattr(stagehand, 'session_id', None) or getattr(stagehand, 'browserbase_session_id', None) + + session_id = getattr(stagehand, "session_id", None) or getattr( + stagehand, "browserbase_session_id", None + ) if session_id: print(f"[{company_name}] Live View Link: https://browserbase.com/sessions/{session_id}") - + page = stagehand.page - + # Navigate to Google as starting point for CUA agent to search and find company homepage print(f"[{company_name}] Navigating to Google...") await with_retry( lambda: page.goto("https://www.google.com/", wait_until="domcontentloaded"), - f"[{company_name}] Initial navigation to Google" + f"[{company_name}] Initial navigation to Google", ) - + # Create CUA agent for autonomous navigation # Agent can interact with the browser like a human: search, click, scroll, and navigate print(f"[{company_name}] Creating Computer Use Agent...") @@ -116,116 +122,118 @@ async def process_company(company_name: str) -> CompanyData: Do not ask follow up questions, the user will trust your judgement.""", options={ "api_key": os.getenv("GEMINI_API_KEY"), - } + }, ) - + print(f"[{company_name}] Finding company homepage using CUA agent...") await with_retry( lambda: agent.execute( instruction=f"Navigate to the {company_name} website", max_steps=5, - auto_screenshot=True + auto_screenshot=True, ), - f"[{company_name}] Navigation to website" + f"[{company_name}] Navigation to website", ) - + homepage_url = page.url print(f"[{company_name}] Homepage found: {homepage_url}") - + # Extract both legal document links in parallel for speed (independent operations) print(f"[{company_name}] Finding Terms of Service & Privacy Policy links...") - + results = await asyncio.gather( page.extract( "extract the link to the Terms of Service page (may also be labeled as Terms of Use, Terms and Conditions, or similar equivalent names)", - schema=TermsOfServiceLink + schema=TermsOfServiceLink, ), page.extract( "extract the link to the Privacy Policy page (may also be labeled as Privacy Notice, Privacy Statement, or similar equivalent names)", - schema=PrivacyPolicyLink + schema=PrivacyPolicyLink, ), - return_exceptions=True + return_exceptions=True, ) - + terms_of_service_link = "" privacy_policy_link = "" - + if not isinstance(results[0], Exception) and results[0]: terms_of_service_link = str(results[0].terms_of_service_link) print(f"[{company_name}] Terms of Service: {terms_of_service_link}") - + if not isinstance(results[1], Exception) and results[1]: privacy_policy_link = str(results[1].privacy_policy_link) print(f"[{company_name}] Privacy Policy: {privacy_policy_link}") - + address = "" - + # Try Terms of Service first - most likely to contain physical address for legal/contact purposes if terms_of_service_link: print(f"[{company_name}] Extracting address from Terms of Service...") await with_retry( lambda: page.goto(terms_of_service_link), - f"[{company_name}] Navigate to Terms of Service" + f"[{company_name}] Navigate to Terms of Service", ) - + try: address_result = await page.extract( "Extract the physical company mailing address (street, city, state, postal code, and country if present) from the Terms of Service page. Ignore phone numbers or email addresses.", - schema=CompanyAddress + schema=CompanyAddress, ) - + if address_result.company_address and address_result.company_address.strip(): address = address_result.company_address.strip() print(f"[{company_name}] Address found in Terms of Service: {address}") - except Exception as error: + except Exception: print(f"[{company_name}] Could not extract address from Terms of Service page") - + # Fallback: check Privacy Policy if address not found in Terms of Service if not address and privacy_policy_link: - print(f"[{company_name}] Address not found in Terms of Service, trying Privacy Policy...") + print( + f"[{company_name}] Address not found in Terms of Service, trying Privacy Policy..." + ) await with_retry( lambda: page.goto(privacy_policy_link), - f"[{company_name}] Navigate to Privacy Policy" + f"[{company_name}] Navigate to Privacy Policy", ) - + try: address_result = await page.extract( "Extract the physical company mailing address (street, city, state, postal code, and country if present) from the Privacy Policy page. Ignore phone numbers or email addresses.", - schema=CompanyAddress + schema=CompanyAddress, ) - + if address_result.company_address and address_result.company_address.strip(): address = address_result.company_address.strip() print(f"[{company_name}] Address found in Privacy Policy: {address}") - except Exception as error: + except Exception: print(f"[{company_name}] Could not extract address from Privacy Policy page") - + if not address: address = "Address not found in Terms of Service or Privacy Policy pages" print(f"[{company_name}] {address}") - + result = CompanyData( company_name=company_name, homepage_url=homepage_url, terms_of_service_link=terms_of_service_link, privacy_policy_link=privacy_policy_link, - address=address + address=address, ) - + print(f"[{company_name}] Successfully processed") return result - + except Exception as error: print(f"[{company_name}] Error: {error}") - + return CompanyData( company_name=company_name, homepage_url="", terms_of_service_link="", privacy_policy_link="", - address=f"Error: {error}" + address=f"Error: {error}", ) - + finally: if stagehand: try: @@ -239,16 +247,18 @@ async def process_company(company_name: str) -> CompanyData: # Collects results and outputs final JSON summary async def main(): print("Starting Company Address Finder...") - + company_names = COMPANY_NAMES max_concurrent = max(1, MAX_CONCURRENT) company_count = len(company_names) is_sequential = max_concurrent == 1 - - print(f"\nProcessing {company_count} {'company' if company_count == 1 else 'companies'} {'sequentially' if is_sequential else f'concurrently (batch size: {max_concurrent})'}...") - + + print( + f"\nProcessing {company_count} {'company' if company_count == 1 else 'companies'} {'sequentially' if is_sequential else f'concurrently (batch size: {max_concurrent})'}..." + ) + all_results = [] - + if is_sequential: for i, company_name in enumerate(company_names): print(f"[{i + 1}/{len(company_names)}] {company_name}") @@ -256,24 +266,26 @@ async def main(): all_results.append(result) else: for i in range(0, len(company_names), max_concurrent): - batch = company_names[i:i + max_concurrent] + batch = company_names[i : i + max_concurrent] batch_number = i // max_concurrent + 1 total_batches = (len(company_names) + max_concurrent - 1) // max_concurrent - + print(f"\nBatch {batch_number}/{total_batches}: {', '.join(batch)}") - + batch_promises = [process_company(name) for name in batch] batch_results = await asyncio.gather(*batch_promises) all_results.extend(batch_results) - - print(f"Batch {batch_number}/{total_batches} completed: {len(batch_results)} companies processed") - + + print( + f"Batch {batch_number}/{total_batches} completed: {len(batch_results)} companies processed" + ) + print("\n" + "=" * 80) print("RESULTS (JSON):") print("=" * 80) print(json.dumps([result.model_dump() for result in all_results], indent=2)) print("=" * 80) - + print(f"\nComplete: processed {len(all_results)}/{len(company_names)} companies") diff --git a/python/company-value-prop-generator/README.md b/python/company-value-prop-generator/README.md index 932022e..4685aa1 100644 --- a/python/company-value-prop-generator/README.md +++ b/python/company-value-prop-generator/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Value Prop One-Liner Generator ## AT A GLANCE + - Goal: Automatically extract and format website value propositions into concise one-liners for email personalization - Demonstrates Stagehand's `extract` method with Pydantic schemas to pull structured data from landing pages - Shows direct OpenAI API usage to transform extracted content with custom prompts @@ -8,18 +9,20 @@ - Docs โ†’ https://docs.browserbase.com/stagehand/extract ## GLOSSARY + - Extract: Stagehand method that uses AI to pull structured data from pages using natural language instructions Docs โ†’ https://docs.browserbase.com/stagehand/extract - Value Proposition: The core benefit or unique selling point a company communicates to customers ## QUICKSTART -1) cd python/company-value-prop-generator -2) pip install python-dotenv stagehand openai pydantic -3) cp .env.example .env -4) Add required API keys/IDs to .env -5) python main.py + +1. cd python/company-value-prop-generator +2. pip install python-dotenv stagehand openai pydantic +3. cp .env.example .env # Add required API keys/IDs to .env +4. python main.py ## EXPECTED OUTPUT + - Stagehand initializes and creates a Browserbase session - Displays live session link for monitoring - Navigates to target domain and waits for page load @@ -31,6 +34,7 @@ - Closes browser session ## COMMON PITFALLS + - Dependency install errors: ensure pip install completed - Missing credentials: - BROWSERBASE_PROJECT_ID (required for browser automation) @@ -41,18 +45,21 @@ - Slow-loading sites: 5-minute timeout configured, but extremely slow sites may still timeout ## USE CASES + โ€ข Generate personalized email openers by extracting value props from prospect domains โ€ข Build prospecting tools that automatically understand what companies do from their websites โ€ข Create dynamic messaging systems that adapt content based on extracted company information ## NEXT STEPS + โ€ข Batch process multiple domains by iterating over a list and aggregating results โ€ข Extract additional metadata like company description, industry tags, or key features alongside value prop โ€ข Add caching layer to avoid re-extracting value props for previously analyzed domains ## HELPFUL RESOURCES -๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction -๐ŸŽฎ Browserbase: https://www.browserbase.com -๐Ÿ’ก Templates: https://www.browserbase.com/templates -๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +๐ŸŽฎ Browserbase: https://www.browserbase.com +๐Ÿ’ก Templates: https://www.browserbase.com/templates +๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ’ฌ Discord: http://stagehand.dev/discord diff --git a/python/company-value-prop-generator/main.py b/python/company-value-prop-generator/main.py index 6b66051..cc49e20 100644 --- a/python/company-value-prop-generator/main.py +++ b/python/company-value-prop-generator/main.py @@ -1,11 +1,13 @@ # Stagehand + Browserbase: Value Prop One-Liner Generator - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig -from pydantic import BaseModel, Field from openai import OpenAI +from pydantic import BaseModel, Field + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -42,11 +44,11 @@ async def generate_one_liner(domain: str) -> str: async with Stagehand(config) as stagehand: print("Stagehand initialized successfully!") session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Live View Link: https://browserbase.com/sessions/{session_id}") @@ -57,7 +59,7 @@ async def generate_one_liner(domain: str) -> str: # 5min timeout to handle slow-loading sites or network issues await page.goto( f"https://{domain}/", - wait_until='domcontentloaded', + wait_until="domcontentloaded", timeout=300000, ) @@ -74,11 +76,7 @@ async def generate_one_liner(domain: str) -> str: print(f"๐Ÿ“Š Extracted value prop for {domain}: {value_prop}") # Validate extraction returned meaningful content - if ( - not value_prop - or value_prop.lower() == "null" - or value_prop.lower() == "undefined" - ): + if not value_prop or value_prop.lower() == "null" or value_prop.lower() == "undefined": print("โš ๏ธ Value prop extraction returned empty or invalid result") raise ValueError(f"No value prop found for {domain}") @@ -153,7 +151,9 @@ async def main(): print(f"\nโŒ Error: {error_message}") print("\nCommon issues:") print(" - Check .env file has OPENAI_API_KEY set (required for LLM generation)") - print(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY set (required for browser automation)") + print( + " - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY set (required for browser automation)" + ) print(" - Ensure the domain is accessible and not a placeholder/maintenance page") print(" - Verify internet connectivity and that the target site is reachable") print("Docs: https://docs.browserbase.com/stagehand") @@ -166,4 +166,3 @@ async def main(): except Exception as err: print(f"Fatal error: {err}") exit(1) - diff --git a/python/context/README.md b/python/context/README.md index 658f8e4..7aba118 100644 --- a/python/context/README.md +++ b/python/context/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Context Authentication Example ## AT A GLANCE + - Goal: demonstrate persistent authentication using Browserbase **contexts** that survive across sessions. - Flow: create context โ†’ log in once โ†’ persist cookies/tokens โ†’ reuse context in a new session โ†’ extract data โ†’ clean up. - Benefits: skip re-auth on subsequent runs, reduce MFA prompts, speed up protected flows, and keep state stable across retries. Docs โ†’ https://docs.browserbase.com/features/contexts ## GLOSSARY + - context: a persistent browser state (cookies, localStorage, cache) stored server-side and reusable by new sessions. Docs โ†’ https://docs.browserbase.com/features/contexts - persist: when true, any state changes during a session are written back to the context for future reuse. @@ -14,40 +16,46 @@ Docs โ†’ https://docs.stagehand.dev/basics/act ## QUICKSTART - 1) cd context-template - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install -r requirements.txt - 5) pip install browserbase pydantic requests - 6) cp .env.example .env - 7) Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env - 8) python main.py + +1. cd context-template +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install -r requirements.txt +5. pip install browserbase pydantic requests +6. cp .env.example .env # Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env +7. python main.py ## EXPECTED OUTPUT + - Creates context, performs login, saves auth state - Reuses context in new session to access authenticated pages - Extracts user data using structured schemas - Cleans up context after completion ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains all required variables - Context persistence: ensure persist=True is set to save login state - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Persistent login sessions: Automate workflows that require authentication without re-logging in every run. โ€ข Access to gated content: Crawl or extract data from behind login walls (e.g., booking portals, dashboards, intranets). โ€ข Multi-step workflows: Maintain cookies/tokens across different automation steps or scheduled jobs. ## NEXT STEPS + โ€ข Extend to multiple apps: Reuse the same context across different authenticated websites within one session. โ€ข Add session validation: Extract and verify account info (e.g., username, profile details) to confirm successful auth. โ€ข Secure lifecycle: Rotate, refresh, and delete contexts programmatically to enforce security policies. ## 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 + +๐Ÿ“š 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/context/main.py b/python/context/main.py index f8c258e..156a2f7 100644 --- a/python/context/main.py +++ b/python/context/main.py @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Context Authentication Example - See README.md for full documentation -import os import asyncio -from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig +import os + +import requests from browserbase import Browserbase +from dotenv import load_dotenv from pydantic import BaseModel, Field -import requests + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -16,12 +18,10 @@ async def create_session_context_id(): print("Creating new Browserbase context...") # First create a context using Browserbase SDK to get a context ID. bb = Browserbase(api_key=os.environ.get("BROWSERBASE_API_KEY")) - context = bb.contexts.create( - project_id=os.environ.get("BROWSERBASE_PROJECT_ID") - ) - + context = bb.contexts.create(project_id=os.environ.get("BROWSERBASE_PROJECT_ID")) + print(f"Created context ID: {context.id}") - + # Create a single session using the context ID to perform initial login. print("Creating session for initial login...") session = bb.sessions.create( @@ -31,10 +31,10 @@ async def create_session_context_id(): "id": context.id, "persist": True, # Save authentication state to context } - } + }, ) print(f"Live view: https://browserbase.com/sessions/{session.id}") - + # Connect Stagehand to the existing session (no new session created). print("Connecting Stagehand to session...") config = StagehandConfig( @@ -44,35 +44,35 @@ async def create_session_context_id(): model_name="openai/gpt-4.1", model_api_key=os.environ.get("OPENAI_API_KEY"), browserbase_session_id=session.id, - verbose=1 # 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.) + verbose=1, # 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 ) - + async with Stagehand(config) as stagehand: page = stagehand.page email = os.environ.get("SF_REC_PARK_EMAIL") password = os.environ.get("SF_REC_PARK_PASSWORD") - + # Navigate to login page with extended timeout for slow-loading sites. print("Navigating to SF Rec & Park login page...") await page.goto( "https://www.rec.us/organizations/san-francisco-rec-park", - wait_until='domcontentloaded', - timeout=60000 + wait_until="domcontentloaded", + timeout=60000, ) # Perform login sequence: each step is atomic to handle dynamic page changes. print("Starting login sequence...") await page.act("Click the Login button") - await page.act(f"Fill in the email or username field with \"{email}\"") + await page.act(f'Fill in the email or username field with "{email}"') await page.act("Click the next, continue, or submit button to proceed") - await page.act(f"Fill in the password field with \"{password}\"") + await page.act(f'Fill in the password field with "{password}"') await page.act("Click the login, sign in, or submit button") print("Login sequence completed!") - + print("Authentication state saved to context") - + # Return the context ID for reuse in future sessions. return {"id": context.id} @@ -86,11 +86,11 @@ async def delete_context(context_id: str): f"https://api.browserbase.com/v1/contexts/{context_id}", headers={ "X-BB-API-Key": os.environ.get("BROWSERBASE_API_KEY"), - } + }, ) print(f"Context deleted successfully (status: {response.status_code})") except Exception as error: - error_msg = getattr(error, 'response', {}).get('data') or str(error) + error_msg = getattr(error, "response", {}).get("data") or str(error) print(f"Error deleting context: {error_msg}") @@ -98,7 +98,7 @@ async def main(): print("Starting Context Authentication Example...") # Create context with login state for reuse in authenticated sessions. context_id = await create_session_context_id() - + # Initialize new session using existing context to inherit authentication state. # persist: true ensures any new changes (cookies, cache) are saved back to context. config = StagehandConfig( @@ -114,18 +114,18 @@ async def main(): "id": context_id["id"], "persist": True, } - } + }, }, - verbose=1 # 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.) + verbose=1, # 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 ) - + async with Stagehand(config) as stagehand: print("Authenticated session ready!") - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): print(f"Live view: https://browserbase.com/sessions/{stagehand.session_id}") - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): print(f"Live view: https://browserbase.com/sessions/{stagehand.browserbase_session_id}") page = stagehand.page @@ -134,31 +134,28 @@ async def main(): print("Navigating to authenticated area (should skip login)...") await page.goto( "https://www.rec.us/organizations/san-francisco-rec-park", - wait_until='domcontentloaded', - timeout=60000 + wait_until="domcontentloaded", + timeout=60000, ) # Navigate to user-specific area to access personal data. await page.act("Click on the reservations button") - + # Extract structured user data using Pydantic schema for type safety. # Schema ensures consistent data format and validates extracted content. print("Extracting user profile data...") - + # Define schema using Pydantic class UserData(BaseModel): full_name: str = Field(..., description="the user's full name") address: str = Field(..., description="the user's address") - - user_data = await page.extract( - "Extract the user's full name and address", - schema=UserData - ) - + + user_data = await page.extract("Extract the user's full name and address", schema=UserData) + print(f"Extracted user data: {user_data}") - + print("Session closed successfully") - + # Clean up context to prevent accumulation and ensure security. await delete_context(context_id["id"]) diff --git a/python/council-events/README.md b/python/council-events/README.md index cf4891b..a047763 100644 --- a/python/council-events/README.md +++ b/python/council-events/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Philadelphia Council Events Scraper ## AT A GLANCE + - Goal: automate extraction of Philadelphia Council events for 2025 from the official calendar. - Flow: navigate to phila.legistar.com โ†’ click calendar โ†’ select 2025 โ†’ extract event data (name, date, time). - Benefits: quickly gather upcoming council events without manual browsing, structured data ready for analysis or notifications. Docs โ†’ https://docs.stagehand.dev/v3/first-steps/introduction ## GLOSSARY + - act: perform UI actions from a prompt (click, select, navigate). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from a page using AI and Pydantic schemas. @@ -14,15 +16,16 @@ - Pydantic schema: type-safe data models that validate extracted content. ## QUICKSTART - 1) cd council-events - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install stagehand python-dotenv pydantic - 5) cp .env.example .env - 6) Add your Browserbase API key, Project ID, and OpenAI API key to .env - 7) python main.py + +1. cd council-events +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install stagehand python-dotenv pydantic +5. cp .env.example .env # Add your Browserbase API key, Project ID, and OpenAI API key to .env +6. python main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Philadelphia Council calendar - Selects 2025 events from dropdown @@ -32,6 +35,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and OPENAI_API_KEY - No events found: check if the website structure has changed or if 2025 calendar is available @@ -39,19 +43,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Civic monitoring: Track upcoming council meetings, hearings, and votes for advocacy or journalism. โ€ข Event aggregation: Pull council calendars into dashboards, newsletters, or community notification systems. โ€ข Research & analysis: Collect historical event data to analyze meeting frequency, topics, or scheduling patterns. ## NEXT STEPS + โ€ข Multi-year extraction: Loop through multiple years to build historical event database. โ€ข Event details: Click into individual events to extract agendas, attendees, and documents. โ€ข Notifications: Set up scheduled runs to detect new events and send alerts via email/Slack. ## 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://github.com/browserbase/stagehand/tree/main/examples -๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ“š 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://github.com/browserbase/stagehand/tree/main/examples +๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ’ฌ Discord: http://stagehand.dev/discord diff --git a/python/council-events/main.py b/python/council-events/main.py index 32072ec..4e24695 100644 --- a/python/council-events/main.py +++ b/python/council-events/main.py @@ -1,11 +1,12 @@ # Stagehand + Browserbase: Philadelphia Council Events Scraper - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field -from typing import List + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -13,6 +14,7 @@ class Event(BaseModel): """Single event with name, date, and time""" + name: str = Field(..., description="the name of the event") date: str = Field(..., description="the date of the event") time: str = Field(..., description="the time of the event") @@ -20,7 +22,8 @@ class Event(BaseModel): class EventResults(BaseModel): """Collection of events extracted from the calendar""" - results: List[Event] = Field(..., description="array of events") + + results: list[Event] = Field(..., description="array of events") async def main(): @@ -38,8 +41,8 @@ async def main(): model_name="openai/gpt-4.1", model_api_key=os.environ.get("OPENAI_API_KEY"), verbose=1, - # 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.) + # 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 ) @@ -51,11 +54,11 @@ async def main(): # Provide live session URL for debugging and monitoring session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Watch live: https://browserbase.com/sessions/{session_id}") @@ -77,22 +80,22 @@ async def main(): # Extract event data using AI to parse the structured information print("Extracting event information...") results = await page.extract( - "Extract the table with the name, date and time of the events", - schema=EventResults + "Extract the table with the name, date and time of the events", schema=EventResults ) print(f"Found {len(results.results)} events") print("Event data extracted successfully:") - + # Display results in formatted JSON import json + print(json.dumps(results.model_dump(), indent=2)) print("Session closed successfully") except Exception as error: print(f"Error during event extraction: {error}") - + # Provide helpful troubleshooting information print("\nCommon issues:") print("1. Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY") @@ -100,7 +103,7 @@ async def main(): print("3. Ensure internet access and https://phila.legistar.com is accessible") print("4. Verify Browserbase account has sufficient credits") print("5. Check if the calendar page structure has changed") - + raise @@ -110,4 +113,3 @@ async def main(): except Exception as err: print(f"Application error: {err}") exit(1) - diff --git a/python/download-financial-statements/README.md b/python/download-financial-statements/README.md index 931cdb1..bc0d54c 100644 --- a/python/download-financial-statements/README.md +++ b/python/download-financial-statements/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Download Apple's Quarterly Financial Statements ## AT A GLANCE + - Goal: automate downloading Apple's quarterly financial statements (PDFs) from their investor relations site. - Download Handling: Browserbase automatically captures PDFs opened during the session and bundles them into a ZIP file. - Retry Logic: polls Browserbase downloads API with configurable timeout to ensure files are ready before retrieval. - Live Debugging: displays live view URL for real-time session monitoring. ## GLOSSARY + - act: perform UI actions from a prompt (click, scroll, navigate) Docs โ†’ https://docs.stagehand.dev/basics/act - downloads API: retrieve files downloaded during a Browserbase session as a ZIP archive @@ -15,12 +17,13 @@ Docs โ†’ https://docs.browserbase.com/features/session-live-view ## QUICKSTART - 1) cd python/download-financial-statements - 2) cp .env.example .env - 3) Add your Browserbase API key and Project ID to .env - 4) uvx --with browserbase --with python-dotenv stagehand main.py + +1. cd python/download-financial-statements +2. cp .env.example .env # Add your Browserbase API key and Project ID to .env +3. uvx --with browserbase --with python-dotenv stagehand main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Apple.com โ†’ Investors section - Locates Q1-Q4 2025 quarterly earnings reports @@ -30,6 +33,7 @@ - Displays session history and closes cleanly ## COMMON PITFALLS + - "Cannot find module": ensure uvx is installed (`pip install uv`) or use `pip install stagehand-ai browserbase python-dotenv` for traditional setup - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY - Download timeout: increase `retry_for_seconds` parameter if downloads take longer than 45 seconds @@ -37,20 +41,22 @@ - Network issues: check internet connection and Apple website accessibility ## USE CASES + โ€ข Financial reporting automation: Download quarterly/annual reports from investor relations sites for analysis, archiving, or compliance. โ€ข Document batch retrieval: Collect multiple PDFs (contracts, invoices, statements) from web portals without manual clicking. โ€ข Scheduled data collection: Run on cron/Lambda to automatically fetch latest financial filings or regulatory documents. ## NEXT STEPS + โ€ข Generalize for other sites: Extract URL patterns, adapt act() prompts, and support multiple companies/document types. โ€ข Parse downloaded PDFs: Unzip, OCR/parse text (PyPDF2/pdfplumber), and load into structured format (CSV/DB/JSON). โ€ข Add validation: Check file count, sizes, naming conventions; alert on failures; retry missing quarters. ## HELPFUL RESOURCES -๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v2/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 - +๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v2/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/download-financial-statements/main.py b/python/download-financial-statements/main.py index 77a3c14..c32074a 100644 --- a/python/download-financial-statements/main.py +++ b/python/download-financial-statements/main.py @@ -1,10 +1,12 @@ # Stagehand + Browserbase: Download Apple's Quarterly Financial Statements - See README.md for full documentation -import os import asyncio +import os + +from browserbase import Browserbase from dotenv import load_dotenv + from stagehand import Stagehand, StagehandConfig -from browserbase import Browserbase # Load environment variables from .env file # Required: BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, GOOGLE_API_KEY @@ -12,66 +14,61 @@ async def save_downloads_with_retry( - bb: Browserbase, - session_id: str, - retry_for_seconds: int = 30 + bb: Browserbase, session_id: str, retry_for_seconds: int = 30 ) -> int: """ Polls Browserbase API for downloads with timeout handling. - + Browserbase stores downloaded files during a session and makes them available via API. Files may take a few seconds to process, so this function implements retry logic to wait for downloads to be ready before retrieving them. - + Args: bb: Browserbase client instance for API calls session_id: The Browserbase session ID to retrieve downloads from retry_for_seconds: Maximum time to wait for downloads (default: 30 seconds) - + Returns: int: The size of the downloaded ZIP file in bytes - + Raises: TimeoutError: If downloads aren't ready within the specified timeout """ print(f"Waiting up to {retry_for_seconds} seconds for downloads to complete...") - + # Track elapsed time to implement timeout without using threading timers start_time = asyncio.get_event_loop().time() timeout = retry_for_seconds - + while True: elapsed = asyncio.get_event_loop().time() - start_time - + # Check if we've exceeded the timeout period if elapsed >= timeout: raise TimeoutError("Download timeout exceeded") - + try: print("Checking for downloads...") # Use asyncio.to_thread for synchronous Browserbase SDK calls # This prevents blocking the event loop while waiting for API responses - response = await asyncio.to_thread( - bb.sessions.downloads.list, - session_id - ) + response = await asyncio.to_thread(bb.sessions.downloads.list, session_id) download_buffer = await asyncio.to_thread(response.read) - + # Check if downloads are ready (non-empty buffer indicates files are available) if len(download_buffer) > 0: print(f"Downloads ready! File size: {len(download_buffer)} bytes") - + # Save the ZIP file containing all downloaded PDFs to disk - with open('downloaded_files.zip', 'wb') as f: + with open("downloaded_files.zip", "wb") as f: f.write(download_buffer) print("Files saved as: downloaded_files.zip") return len(download_buffer) else: print("Downloads not ready yet, retrying...") except Exception as e: - print(f'Error fetching downloads: {e}') + print(f"Error fetching downloads: {e}") raise - + # Poll every 2 seconds to check if downloads are ready # This interval balances responsiveness with API rate limits await asyncio.sleep(2) @@ -80,7 +77,7 @@ async def save_downloads_with_retry( async def main(): """ Main application entry point. - + Orchestrates the entire PDF download automation process: 1. Initializes Browserbase and Stagehand clients 2. Navigates to Apple's investor relations site @@ -88,11 +85,11 @@ async def main(): 4. Waits for downloads to process and saves them as a ZIP file """ print("Starting Apple Financial Statements Download Automation...") - + # Initialize Browserbase client for session management and downloads API print("Initializing Browserbase client...") bb = Browserbase(api_key=os.environ.get("BROWSERBASE_API_KEY")) - + # Initialize Stagehand with Browserbase for cloud-based browser automation # Stagehand provides natural language browser control (act, extract, observe) config = StagehandConfig( @@ -102,35 +99,35 @@ async def main(): model_name="google/gemini-2.5-flash-preview-05-20", # AI model for interpreting natural language actions model_api_key=os.environ.get("GOOGLE_API_KEY"), verbose=2, - # Logging levels: 0 = errors only, 1 = info, 2 = debug + # Logging levels: 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 ) - + try: # Initialize browser session to start automation # Using context manager ensures proper cleanup even if errors occur async with Stagehand(config) as stagehand: print("Stagehand initialized successfully!") page = stagehand.page - + # Display live view URL for debugging and monitoring # Live view allows real-time observation of browser automation live_view_links = bb.sessions.debug(stagehand.session_id) live_view_link = live_view_links.debuggerFullscreenUrl print(f"๐Ÿ” Live View Link: {live_view_link}") - + # Navigate to Apple homepage with extended timeout for slow-loading sites print("Navigating to Apple.com...") await page.goto("https://www.apple.com/", timeout=60000) - + # Navigate to investor relations section using natural language actions # act() uses AI to interpret instructions and perform browser interactions print("Navigating to Investors section...") await page.act("Click the 'Investors' button at the bottom of the page") await page.act("Scroll down to the Financial Data section of the page") await page.act("Under Quarterly Earnings Reports, click on '2025'") - + # Download all quarterly financial statements # When a URL of a PDF is opened, Browserbase automatically downloads and stores the PDF # Files are captured in the session and can be retrieved via the downloads API @@ -140,22 +137,22 @@ async def main(): await page.act("Click the 'Financial Statements' link under Q3") await page.act("Click the 'Financial Statements' link under Q2") await page.act("Click the 'Financial Statements' link under Q1") - + # Retrieve all downloads triggered during this session from Browserbase API # Files take time to process, so we poll with retry logic (45 second timeout) print("Retrieving downloads from Browserbase...") await save_downloads_with_retry(bb, stagehand.session_id, 45) print("All downloads completed successfully!") - + # Print history if available in Stagehand v2 # History shows all actions taken during the session for debugging - if hasattr(stagehand, 'history'): + if hasattr(stagehand, "history"): print("\nStagehand History:") print(stagehand.history) - + # Context manager automatically closes the session and cleans up resources print("Session closed successfully") - + except Exception as error: print(f"Error during automation: {error}") raise @@ -175,4 +172,3 @@ async def main(): print(" - Ensure sufficient timeout for slow-loading pages") print("Docs: https://docs.stagehand.dev/v2/first-steps/introduction") exit(1) - diff --git a/python/form-filling/README.md b/python/form-filling/README.md index 2d129c9..5ac84aa 100644 --- a/python/form-filling/README.md +++ b/python/form-filling/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Form Filling Automation ## AT A GLANCE + - Goal: showcase how to automate form filling with Stagehand and Browserbase. - Smart Form Automation: dynamically fill contact forms with variable-driven data. - Field Detection: analyze page structure with `observe` before interacting with fields. @@ -8,6 +9,7 @@ Docs โ†’ https://docs.browserbase.com/features/sessions ## GLOSSARY + - act: perform UI actions from a prompt (type, click, fill forms) Docs โ†’ https://docs.stagehand.dev/basics/act - observe: analyze a page and return selectors or action plans before executing @@ -15,15 +17,16 @@ - variable substitution: inject dynamic values into actions using `%variable%` syntax ## QUICKSTART - 1) cd form-fill-template - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install -r requirements.txt - 5) cp .env.example .env - 6) Add your Browserbase API key and Project ID to .env - 7) python main.py + +1. cd form-fill-template +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install -r requirements.txt +5. cp .env.example .env # Add your Browserbase API key and Project ID to .env +6. python main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to contact form page - Analyzes available form fields using observe @@ -32,6 +35,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains all required API keys - Form detection: ensure target page has fillable form fields @@ -40,18 +44,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Lead & intake automation: Auto-fill contact/quote/request forms from CRM or CSV to speed up inbound/outbound workflows. โ€ข QA & regression testing: Validate form fields, required rules, and error states across releases/environments. โ€ข Bulk registrations & surveys: Programmatically complete repeatable sign-ups or survey passes for pilots and internal ops. ## NEXT STEPS + โ€ข Wire in data sources: Load variables from CSV/JSON/CRM, map fields via observe, and support per-site field aliases. โ€ข Submit & verify: Enable submit, capture success toasts/emails, take screenshots, and retry on validation errors. โ€ข Handle complex widgets: Add file uploads, multi-step flows, dropdown/radio/datepickers, and basic anti-bot tactics (delays/proxies). ## 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 + +๐Ÿ“š 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/form-filling/main.py b/python/form-filling/main.py index 303000f..2fd7d9e 100644 --- a/python/form-filling/main.py +++ b/python/form-filling/main.py @@ -1,8 +1,10 @@ # Stagehand + Browserbase: Form Filling Automation - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv + from stagehand import Stagehand, StagehandConfig # Load environment variables @@ -15,12 +17,14 @@ company = "TechCorp Solutions" job_title = "Software Developer" email = "alex.johnson@techcorp.com" -message = "Hello, I'm interested in learning more about your services and would like to schedule a demo." +message = ( + "Hello, I'm interested in learning more about your services and would like to schedule a demo." +) async def main(): print("Starting Form Filling Example...") - + # Initialize Stagehand with Browserbase for cloud-based browser automation. config = StagehandConfig( env="BROWSERBASE", @@ -31,8 +35,8 @@ async def main(): browserbase_session_create_params={ "project_id": os.environ.get("BROWSERBASE_PROJECT_ID"), }, - verbose=1 # 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.) + verbose=1, # 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 ) @@ -40,32 +44,34 @@ async def main(): # Use async context manager for automatic resource management async with Stagehand(config) as stagehand: print("Stagehand initialized successfully!") - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): print(f"Live View Link: https://browserbase.com/sessions/{stagehand.session_id}") - elif hasattr(stagehand, 'browserbase_session_id'): - print(f"Live View Link: https://browserbase.com/sessions/{stagehand.browserbase_session_id}") + elif hasattr(stagehand, "browserbase_session_id"): + print( + f"Live View Link: https://browserbase.com/sessions/{stagehand.browserbase_session_id}" + ) page = stagehand.page # Navigate to contact page with extended timeout for slow-loading sites. print("Navigating to Browserbase contact page...") await page.goto( - 'https://www.browserbase.com/contact', - wait_until='domcontentloaded', # Wait for DOM to be ready before proceeding. - timeout=60000 # Extended timeout for reliable page loading. + "https://www.browserbase.com/contact", + wait_until="domcontentloaded", # Wait for DOM to be ready before proceeding. + timeout=60000, # Extended timeout for reliable page loading. ) # Fill form using individual act() calls for reliability print("Filling in contact form...") - + # Fill each field individually for better reliability - await page.act(f"Fill in the first name field with \"{first_name}\"") - await page.act(f"Fill in the last name field with \"{last_name}\"") - await page.act(f"Fill in the company field with \"{company}\"") - await page.act(f"Fill in the job title field with \"{job_title}\"") - await page.act(f"Fill in the email field with \"{email}\"") - await page.act(f"Fill in the message field with \"{message}\"") - + await page.act(f'Fill in the first name field with "{first_name}"') + await page.act(f'Fill in the last name field with "{last_name}"') + await page.act(f'Fill in the company field with "{company}"') + await page.act(f'Fill in the job title field with "{job_title}"') + await page.act(f'Fill in the email field with "{email}"') + await page.act(f'Fill in the message field with "{message}"') + # Language choice in Stagehand act() is crucial for reliable automation. # Use "click" for dropdown interactions rather than "select" await page.act("Click on the How Can we help? dropdown") @@ -75,10 +81,10 @@ async def main(): # Uncomment the line below if you want to submit the form # await page.act("Click the submit button") - + print("Form filled successfully! Waiting 3 seconds...") await page.wait_for_timeout(30000) - + print("Session closed successfully") except Exception as error: diff --git a/python/gemini-cua/README.md b/python/gemini-cua/README.md index b12a506..f2aa697 100644 --- a/python/gemini-cua/README.md +++ b/python/gemini-cua/README.md @@ -1,23 +1,26 @@ # Stagehand + Browserbase: Computer Use Agent (CUA) Example ## AT A GLANCE + - Goal: demonstrate autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase. - Uses Stagehand Agent to automate complex workflows with AI powered browser agents - Leverages Google's gemini-2.5-computer-use-preview model for autonomous web interaction and decision-making. ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent ## QUICKSTART - 1) python -m venv venv - 2) source venv/bin/activate # On Windows: venv\Scripts\activate - 3) pip install -r requirements.txt - 4) cp .env.example .env - 5) Add your Browserbase API key, Project ID, and Google API key to .env - 6) python main.py + +1. uv venv venv +2. source venv/bin/activate # On Windows: venv\Scripts\activate +3. pip install -r requirements.txt +4. cp .env.example .env # Add your Browserbase API key, Project ID, and Google API key to .env +5. python main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Google search engine - Executes autonomous search and data extraction task @@ -26,24 +29,29 @@ - Closes session cleanly ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_API_KEY - Google API access: ensure you have access to Google's gemini-2.5-computer-use-preview model - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Autonomous research: Let AI agents independently research topics, gather information, and compile reports without manual intervention. โ€ข Complex web workflows: Automate multi-step processes that require decision-making, form filling, and data extraction across multiple pages. โ€ข Content discovery: Search for specific information, verify data accuracy, and cross-reference sources autonomously. ## NEXT STEPS + โ€ข Customize instructions: Modify the instruction variable to test different autonomous tasks and scenarios. โ€ข Add error handling: Implement retry logic, fallback strategies, and better error recovery for failed agent actions. โ€ข Extend capabilities: Add support for file downloads, form submissions, and more complex interaction patterns. ## 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 \ No newline at end of file + +๐Ÿ“š 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/gemini-cua/main.py b/python/gemini-cua/main.py index a4c24fd..b5491ba 100644 --- a/python/gemini-cua/main.py +++ b/python/gemini-cua/main.py @@ -1,8 +1,10 @@ # Stagehand + Browserbase: Computer Use Agent (CUA) Example - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv + from stagehand import Stagehand, StagehandConfig # Load environment variables @@ -26,29 +28,26 @@ # ============================================================================ + async def main(): print("Starting Computer Use Agent Example...") - + # Initialize Stagehand with Browserbase for cloud-based browser automation. config = StagehandConfig( env="BROWSERBASE", api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), - model_api_key=os.environ.get("GOOGLE_API_KEY"), # this is the model stagehand uses in act, observe, extract (not agent) + model_api_key=os.environ.get( + "GOOGLE_API_KEY" + ), # this is the model stagehand uses in act, observe, extract (not agent) browserbase_session_create_params={ "project_id": os.environ.get("BROWSERBASE_PROJECT_ID"), "proxies": True, # Using proxies will give the agent a better chance of success - requires Developer Plan or higher, comment out if you don't have access "region": "us-west-2", - "browser_settings": { - "block_ads": True, - "viewport": { - "width": 1288, - "height": 711 - } - } + "browser_settings": {"block_ads": True, "viewport": {"width": 1288, "height": 711}}, }, - verbose=1 # 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.) + verbose=1, # 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 ) @@ -62,9 +61,9 @@ async def main(): # Navigate to search engine with extended timeout for slow-loading sites. print("Navigating to Google search...") await page.goto( - "https://www.google.com/", + "https://www.google.com/", wait_until="domcontentloaded", - timeout=60000 # Extended timeout for reliable page loading + timeout=60000, # Extended timeout for reliable page loading ) # Create agent with computer use capabilities for autonomous web browsing. @@ -84,8 +83,8 @@ async def main(): print("Executing instruction:", instruction) result = await agent.execute( instruction=instruction, - max_steps=30, # The maximum number of steps the agent can take to complete the task - auto_screenshot=True + max_steps=30, # The maximum number of steps the agent can take to complete the task + auto_screenshot=True, ) if result.success == True: @@ -93,13 +92,14 @@ async def main(): print("Result:", result) else: print("Task failed or was incomplete") - + print("Session closed successfully") except Exception as error: print(f"Error executing computer use agent: {error}") raise + if __name__ == "__main__": try: asyncio.run(main()) diff --git a/python/gift-finder/README.md b/python/gift-finder/README.md index 1811d02..4623ca0 100644 --- a/python/gift-finder/README.md +++ b/python/gift-finder/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: AI-Powered Gift Finder ## AT A GLANCE + - Goal: find personalized gift recommendations using AI-generated search queries and intelligent product scoring. - AI Integration: Stagehand for AI-generated search queries and score products based on recipient profile. - Concurrent Sessions: runs multiple browser sessions simultaneously to search different queries in parallel. - Proxies: uses Browserbase proxies with UK geolocation for European website access (Firebox.eu). ## GLOSSARY + - act: perform UI actions from a prompt (search, click, type) Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from pages using schemas @@ -17,16 +19,17 @@ Docs โ†’ https://docs.browserbase.com/features/proxies ## QUICKSTART - 1) cd gift-finder-template - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install -r requirements.txt - 5) pip install InquirerPy pydantic openai - 6) cp .env.example .env - 7) Add your Browserbase API key, Project ID, and OpenAI API key to .env - 8) python main.py + +1. cd gift-finder-template +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install -r requirements.txt +5. pip install InquirerPy pydantic openai +6. cp .env.example .env # Add your Browserbase API key, Project ID, and OpenAI API key to .env +7. python main.py ## EXPECTED OUTPUT + - Prompts user for recipient and description - Generates 3 search queries using OpenAI - Runs concurrent browser sessions to search Firebox.eu @@ -35,6 +38,7 @@ - Displays top 3 personalized gift recommendations ## COMMON PITFALLS + - Browserbase Developer plan or higher is required to use proxies (they have been commented out in the code) - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains all required API keys @@ -42,18 +46,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Multi-retailer product discovery: Generate smart queries, browse in parallel, and extract structured results across sites (with geo-specific proxies when needed). โ€ข Personalized gifting/recommendations: Score items against a recipient profile for gift lists, concierge shopping, or corporate gifting portals. โ€ข Assortment & market checks: Rapidly sample categories to compare price/availability/ratings across regions or competitors. ## NEXT STEPS + โ€ข Add site adapters: Plug in more retailers with per-site extract schemas, result normalization, and de-duplication (canonical URL matching). โ€ข Upgrade ranking: Blend AI scores with signals (price, reviews, shipping, stock), and persist results to JSON/CSV/DB for re-scoring and audits. โ€ข Scale & geo-test: Fan out more concurrent sessions and run a geo matrix via proxies (e.g., UK/EU/US) to compare localized inventory and pricing. ## 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 + +๐Ÿ“š 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/gift-finder/main.py b/python/gift-finder/main.py index c515215..980638b 100644 --- a/python/gift-finder/main.py +++ b/python/gift-finder/main.py @@ -1,13 +1,14 @@ # Stagehand + Browserbase: AI-Powered Gift Finder - See README.md for full documentation -import os import asyncio -from typing import List, Optional +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from openai import OpenAI from pydantic import BaseModel, Field, HttpUrl +from stagehand import Stagehand, StagehandConfig + # Load environment variables load_dotenv() @@ -30,23 +31,23 @@ class Product(BaseModel): url: str price: str rating: str - ai_score: Optional[int] = None - ai_reason: Optional[str] = None + ai_score: int | None = None + ai_reason: str | None = None class SearchResult(BaseModel): query: str session_index: int - products: List[Product] + products: list[Product] client = OpenAI() -async def generate_search_queries(recipient: str, description: str) -> List[str]: +async def generate_search_queries(recipient: str, description: str) -> list[str]: """ Generate intelligent search queries based on recipient profile. - + Uses AI to create thoughtful, complementary gift search terms that go beyond obvious basics to find unique and meaningful gifts. """ @@ -60,7 +61,7 @@ async def generate_search_queries(recipient: str, description: str) -> List[str] messages=[ { "role": "user", - "content": f"""Generate exactly 3 short gift search queries (1-2 words each) for finding gifts for a {recipient.lower()} who is described as: "{description}". + "content": f"""Generate exactly 3 short gift search queries (1-2 words each) for finding gifts for a {recipient.lower()} who is described as: "{description}". IMPORTANT: Assume they already have the basic necessities related to their interests. Focus on: - Complementary items that enhance their hobbies @@ -89,13 +90,13 @@ async def generate_search_queries(recipient: str, description: str) -> List[str] async def score_products( - products: List[Product], + products: list[Product], recipient: str, description: str, -) -> List[Product]: +) -> list[Product]: """ Score and rank products based on recipient profile using AI. - + Analyzes each product against the recipient's interests, relationship context, value, uniqueness, and practical usefulness to find the best gift matches. """ @@ -109,10 +110,12 @@ async def score_products( return [] # Format products for AI analysis with index numbers for reference - product_list = "\n".join([ - f"{index + 1}. {product.title} - {product.price} - {product.rating}" - for index, product in enumerate(all_products) - ]) + product_list = "\n".join( + [ + f"{index + 1}. {product.title} - {product.price} - {product.rating}" + for index, product in enumerate(all_products) + ] + ) print(f"Scoring {len(all_products)} products...") @@ -151,7 +154,7 @@ async def score_products( }} ] -IMPORTANT: +IMPORTANT: - Return raw JSON only, no code blocks - Include all {len(all_products)} products - Keep reasons under 100 characters @@ -163,12 +166,22 @@ async def score_products( try: # Clean up AI response by removing markdown code blocks - response_content = response.choices[0].message.content.strip() if response.choices[0].message.content else "[]" + response_content = ( + response.choices[0].message.content.strip() + if response.choices[0].message.content + else "[]" + ) - response_content = response_content.replace("```json\n", "").replace("```json", "").replace("```\n", "").replace("```", "") + response_content = ( + response_content.replace("```json\n", "") + .replace("```json", "") + .replace("```\n", "") + .replace("```", "") + ) # Parse JSON response from AI scoring import json + scores_data = json.loads(response_content) # Map AI scores back to products using index matching @@ -176,7 +189,11 @@ async def score_products( for index, product in enumerate(all_products): score_info = next((s for s in scores_data if s.get("productIndex") == index + 1), None) product.ai_score = score_info.get("score", 0) if score_info else 0 - product.ai_reason = score_info.get("reason", "No scoring available") if score_info else "No scoring available" + product.ai_reason = ( + score_info.get("reason", "No scoring available") + if score_info + else "No scoring available" + ) scored_products.append(product) # Sort by AI score descending to show best matches first @@ -197,7 +214,7 @@ async def score_products( async def get_user_input() -> GiftFinderAnswers: """ Collect user input for gift recipient and description. - + Uses the CONFIG dictionary at the top of the file for configuration. """ print("Welcome to the Gift Finder App!") @@ -207,18 +224,17 @@ async def get_user_input() -> GiftFinderAnswers: # Validate description length if len(CONFIG["description"].strip()) < 5: - raise ValueError("Description must be at least 5 characters long. Please update the CONFIG at the top of the file.") + raise ValueError( + "Description must be at least 5 characters long. Please update the CONFIG at the top of the file." + ) - return GiftFinderAnswers( - recipient=CONFIG["recipient"], - description=CONFIG["description"] - ) + return GiftFinderAnswers(recipient=CONFIG["recipient"], description=CONFIG["description"]) async def main() -> None: """ Main application entry point. - + Orchestrates the entire gift finding process: 1. Collects user input 2. Generates intelligent search queries @@ -241,7 +257,7 @@ async def main() -> None: print("\nGenerated Search Queries:") for index, query in enumerate(search_queries): - cleaned_query = query.replace('"', '').replace("'", '') + cleaned_query = query.replace('"', "").replace("'", "") print(f" {index + 1}. {cleaned_query}") except Exception as error: print(f"Error generating search queries: {error}") @@ -253,7 +269,7 @@ async def main() -> None: print("\nStarting concurrent browser searches...") async def run_single_search(query: str, session_index: int) -> SearchResult: - print(f"Starting search session {session_index + 1} for: \"{query}\"") + print(f'Starting search session {session_index + 1} for: "{query}"') # Create separate Stagehand instance for each search to run concurrently # Each session searches independently to maximize speed and parallel processing @@ -262,7 +278,7 @@ async def run_single_search(query: str, session_index: int) -> SearchResult: api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), verbose=1, # Silent logging to avoid cluttering output - # Logging levels: 0 = errors only, 1 = info, 2 = debug + # Logging levels: 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_name="openai/gpt-4.1", @@ -286,8 +302,8 @@ async def run_single_search(query: str, session_index: int) -> SearchResult: "width": 1920, "height": 1080, } - } - } + }, + }, ) try: @@ -297,11 +313,11 @@ async def run_single_search(query: str, session_index: int) -> SearchResult: # Display live view URL for debugging and monitoring session_id = None - if hasattr(session_stagehand, 'session_id'): + if hasattr(session_stagehand, "session_id"): session_id = session_stagehand.session_id - elif hasattr(session_stagehand, 'browserbase_session_id'): + elif hasattr(session_stagehand, "browserbase_session_id"): session_id = session_stagehand.browserbase_session_id - + if session_id: live_view_url = f"https://www.browserbase.com/sessions/{session_id}" print(f"Session {session_index + 1} Live View: {live_view_url}") @@ -311,60 +327,57 @@ async def run_single_search(query: str, session_index: int) -> SearchResult: await session_page.goto("https://firebox.eu/") # Perform search using natural language actions - print(f"Session {session_index + 1}: Searching for \"{query}\"...") + print(f'Session {session_index + 1}: Searching for "{query}"...') await session_page.act(f"Type {query} into the search bar") await session_page.act("Click the search button") await session_page.wait_for_timeout(1000) # Extract structured product data using Pydantic schema for type safety print(f"Session {session_index + 1}: Extracting product data...") - + # Define Pydantic schemas for structured data extraction class ProductItem(BaseModel): title: str = Field(..., description="the title/name of the product") url: HttpUrl = Field(..., description="the full URL link to the product page") - price: str = Field(..., description="the price of the product (include currency symbol)") - rating: str = Field(..., description="the star rating or number of reviews (e.g., '4.5 stars' or '123 reviews')") - + price: str = Field( + ..., description="the price of the product (include currency symbol)" + ) + rating: str = Field( + ..., + description="the star rating or number of reviews (e.g., '4.5 stars' or '123 reviews')", + ) + class ProductsData(BaseModel): - products: List[ProductItem] = Field(..., max_length=3, description="array of the first 3 products from search results") - + products: list[ProductItem] = Field( + ..., + max_length=3, + description="array of the first 3 products from search results", + ) + products_data = await session_page.extract( - "Extract the first 3 products from the search results", - schema=ProductsData + "Extract the first 3 products from the search results", schema=ProductsData ) print( - f"Session {session_index + 1}: Found {len(products_data.products)} products for \"{query}\"" + f'Session {session_index + 1}: Found {len(products_data.products)} products for "{query}"' ) # Convert to Product objects products = [ - Product( - title=p.title, - url=str(p.url), - price=p.price, - rating=p.rating - ) + Product(title=p.title, url=str(p.url), price=p.price, rating=p.rating) for p in products_data.products ] - return SearchResult( - query=query, - session_index=session_index + 1, - products=products - ) + return SearchResult(query=query, session_index=session_index + 1, products=products) except Exception as error: print(f"Session {session_index + 1} failed: {error}") - return SearchResult( - query=query, - session_index=session_index + 1, - products=[] - ) + return SearchResult(query=query, session_index=session_index + 1, products=[]) # Create concurrent search tasks for all generated queries - search_promises = [run_single_search(query, index) for index, query in enumerate(search_queries)] + search_promises = [ + run_single_search(query, index) for index, query in enumerate(search_queries) + ] print("\nBrowser Sessions Starting...") print("Live view links will appear as each session initializes") @@ -424,4 +437,4 @@ class ProductsData(BaseModel): except Exception as err: print(f"Application error: {err}") print("Check your environment variables") - exit(1) \ No newline at end of file + exit(1) diff --git a/python/job-application/README.md b/python/job-application/README.md index 919d952..0bde93c 100644 --- a/python/job-application/README.md +++ b/python/job-application/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Automated Job Application Agent ## AT A GLANCE + - Goal: Automate job applications by discovering job listings and submitting applications with unique agent identifiers. - Concurrent Processing: applies to multiple jobs in parallel with configurable concurrency limits based on Browserbase project settings. - Dynamic Data Generation: generates unique agent IDs and email addresses for each application. @@ -8,6 +9,7 @@ - Docs โ†’ https://docs.stagehand.dev/basics/agent ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent - act: perform UI actions from a prompt (click, type, fill forms) @@ -19,14 +21,15 @@ - asyncio.Semaphore: concurrency control mechanism to limit parallel job applications based on project limits ## QUICKSTART -1) python -m venv venv -2) source venv/bin/activate # On Windows: venv\Scripts\activate -3) uvx install stagehand browserbase pydantic python-dotenv httpx -4) cp .env.example .env -5) Add required API keys/IDs to .env (BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, GOOGLE_GENERATIVE_AI_API_KEY) -6) python main.py + +1. uv venv venv +2. source venv/bin/activate # On Windows: venv\Scripts\activate +3. uvx install stagehand browserbase pydantic python-dotenv httpx +4. cp .env.example .env # Add required API keys/IDs to .env (BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, GOOGLE_GENERATIVE_AI_API_KEY) +5. python main.py ## EXPECTED OUTPUT + - Fetches project concurrency limit from Browserbase (maxed at 5) - Initializes main Stagehand session with Browserbase - Displays live session link for monitoring @@ -49,6 +52,7 @@ - Displays completion message when all applications are finished ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via uvx install - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_GENERATIVE_AI_API_KEY - Google API access: ensure you have access to Google's gemini-2.5-flash model @@ -60,12 +64,14 @@ - Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in ## USE CASES + โ€ข Bulk job applications: Automate applying to multiple job postings simultaneously with unique credentials for each application. โ€ข Agent deployment automation: Streamline the process of deploying multiple AI agents by automating the application and registration workflow. โ€ข Testing & QA: Validate job application forms and workflows across multiple listings to ensure consistent functionality. โ€ข Recruitment automation: Scale agent recruitment processes by programmatically submitting applications with generated identifiers. ## NEXT STEPS + โ€ข Add filtering: Implement job filtering by title keywords, location, or other criteria before applying. โ€ข Error handling: Add retry logic for failed applications and better error reporting with job-specific logs. โ€ข Resume customization: Support multiple resume versions or dynamic resume generation based on job requirements. @@ -74,9 +80,10 @@ โ€ข Multi-site support: Extend to support multiple job boards with site-specific form field mappings. ## HELPFUL RESOURCES -๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v2/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 +๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v2/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/job-application/main.py b/python/job-application/main.py index 5abef77..ee66a8c 100644 --- a/python/job-application/main.py +++ b/python/job-application/main.py @@ -1,15 +1,16 @@ # Stagehand + Browserbase: Job Application Automation - See README.md for full documentation -import os import asyncio -import time +import os import random -from typing import List -from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig +import time + +import httpx from browserbase import Browserbase +from dotenv import load_dotenv from pydantic import BaseModel, Field, HttpUrl -import httpx + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -23,13 +24,13 @@ class JobInfo(BaseModel): class JobsData(BaseModel): - jobs: List[JobInfo] + jobs: list[JobInfo] async def get_project_concurrency() -> int: """ Fetch project concurrency limit from Browserbase SDK. - + Retrieves the maximum concurrent sessions allowed for the project, capped at 5. """ @@ -37,8 +38,7 @@ async def get_project_concurrency() -> int: # Use asyncio.to_thread to run synchronous SDK call in thread pool project = await asyncio.to_thread( - bb.projects.retrieve, - os.environ.get("BROWSERBASE_PROJECT_ID") + bb.projects.retrieve, os.environ.get("BROWSERBASE_PROJECT_ID") ) return min(project.concurrency, 5) @@ -48,26 +48,26 @@ def generate_random_email() -> str: Generate a random email address for form submission. """ - random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8)) + random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=8)) return f"agent-{random_string}@example.com" def generate_agent_id() -> str: """ Generate a unique agent identifier for job applications. - + Combines timestamp and random string to ensure uniqueness across multiple job applications and sessions. """ timestamp = int(time.time() * 1000) - random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=7)) + random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=7)) return f"agent-{timestamp}-{random_string}" async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): """ Apply to a single job posting with automated form filling. - + Uses Stagehand to navigate to job page, fill out application form, upload resume, and submit the application. """ @@ -79,7 +79,7 @@ async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), model_name="google/gemini-2.5-flash", - model_api_key=os.environ.get("GOOGLE_GENERATIVE_AI_API_KEY") + model_api_key=os.environ.get("GOOGLE_GENERATIVE_AI_API_KEY"), ) try: @@ -89,13 +89,15 @@ async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): # Get session ID for live viewing/debugging session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id if session_id: - print(f"[{job_info.title}] Watch live: https://browserbase.com/sessions/{session_id}") + print( + f"[{job_info.title}] Watch live: https://browserbase.com/sessions/{session_id}" + ) page = stagehand.page @@ -120,7 +122,7 @@ async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): await page.act(f"type '{email}' into the contact endpoint field") - await page.act(f"type 'us-west-2' into the deployment region field") + await page.act("type 'us-west-2' into the deployment region field") # Upload agent profile/resume file # Using observe() to find the upload button, then setting files programmatically @@ -141,11 +143,13 @@ async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): resume_buffer = response.content # Upload file using Playwright's set_input_files with buffer - await file_input.set_input_files({ - "name": "Agent Resume.pdf", - "mimeType": "application/pdf", - "buffer": resume_buffer, - }) + await file_input.set_input_files( + { + "name": "Agent Resume.pdf", + "mimeType": "application/pdf", + "buffer": resume_buffer, + } + ) print(f"[{job_info.title}] Uploaded resume from {resume_url}") # Select multi-region deployment option @@ -164,14 +168,14 @@ async def apply_to_job(job_info: JobInfo, semaphore: asyncio.Semaphore): async def main(): """ Main application entry point. - + Orchestrates the job application process: 1. Fetches project concurrency limits 2. Scrapes job listings from the job board 3. Applies to all jobs in parallel with concurrency control """ print("Starting Job Application Automation...") - + # Get project concurrency limit to control parallel execution max_concurrency = await get_project_concurrency() print(f"Executing with concurrency limit: {max_concurrency}") @@ -182,7 +186,7 @@ async def main(): api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), model_name="google/gemini-2.5-flash", - model_api_key=os.environ.get("GOOGLE_GENERATIVE_AI_API_KEY") + model_api_key=os.environ.get("GOOGLE_GENERATIVE_AI_API_KEY"), ) # Use async context manager for automatic resource management @@ -191,9 +195,9 @@ async def main(): # Get session ID for live viewing/debugging session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id if session_id: @@ -212,8 +216,7 @@ async def main(): # Extract all job listings with titles and URLs using structured schema # Using extract() with Pydantic schema ensures consistent data extraction jobs_result = await page.extract( - "extract all job listings with their titles and URLs", - schema=JobsData + "extract all job listings with their titles and URLs", schema=JobsData ) jobs_data = jobs_result.jobs diff --git a/python/license-verification/README.md b/python/license-verification/README.md index d31005d..98cf9c2 100644 --- a/python/license-verification/README.md +++ b/python/license-verification/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Data Extraction with Structured Schemas ## AT A GLANCE + - Goal: show how to extract structured, validated data from websites using Stagehand + Pydantic. - Data Extraction: automate navigation, form submission, and structured scraping in one flow. - Schema Validation: enforce type safety and consistency with Pydantic schemas. - Practical Example: verify California real estate license details with a typed output object. ## GLOSSARY + - act: perform UI actions from a prompt (type, click, navigate). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from web pages into validated objects. @@ -17,22 +19,24 @@ - structured scraping: extracting consistent, typed data that can flow into apps, CRMs, or compliance systems. ## QUICKSTART - 1) cd license-verification-template - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install -r requirements.txt - 5) pip install pydantic - 6) cp .env.example .env - 7) Add your Browserbase API key and Project ID to .env - 8) python main.py + +1. cd license-verification-template +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install -r requirements.txt +5. pip install pydantic +6. cp .env.example .env # Add your Browserbase API key and Project ID to .env +7. python main.py ## EXPECTED OUTPUT + - Navigates to California DRE license verification website - Fills in license ID and submits form - Extracts structured license data using Pydantic schema - Returns typed object with license verification details ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing API key: verify .env is loaded and file is not committed - Schema validation errors: ensure extracted data matches Pydantic schema structure @@ -40,18 +44,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข License & credential verification: Extract and validate professional license data from regulatory portals. โ€ข Compliance automation: Monitor status changes (active, expired, disciplinary) for risk and regulatory workflows. โ€ข Structured research: Collect validated datasets from government or industry registries for BI or due diligence. ## NEXT STEPS + โ€ข Expand schema coverage: Add more fields (disciplinary actions, broker info, historical data) for richer records. โ€ข Scale across sources: Point the same flow at other jurisdictions, databases, or professional directories. โ€ข Persist & integrate: Store structured results in a database or push directly into CRM/compliance systems. ## 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 + +๐Ÿ“š 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/license-verification/main.py b/python/license-verification/main.py index b0011c0..914f4ca 100644 --- a/python/license-verification/main.py +++ b/python/license-verification/main.py @@ -1,11 +1,12 @@ # Stagehand + Browserbase: Data Extraction with Structured Schemas - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field -from typing import Optional + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -23,8 +24,8 @@ async def main(): api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), verbose=1, - # 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.) + # 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_name="openai/gpt-4.1", model_api_key=os.environ.get("OPENAI_API_KEY"), @@ -33,59 +34,60 @@ async def main(): # Use async context manager for automatic resource management async with Stagehand(config) as stagehand: print("Stagehand Session Started") - + # Provide live session URL for debugging and monitoring extraction process. session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Watch live: https://browserbase.com/sessions/{session_id}") page = stagehand.page # Navigate to California DRE license verification website for data extraction. - print('Navigating to: https://www2.dre.ca.gov/publicasp/pplinfo.asp') - await page.goto('https://www2.dre.ca.gov/publicasp/pplinfo.asp') - + print("Navigating to: https://www2.dre.ca.gov/publicasp/pplinfo.asp") + await page.goto("https://www2.dre.ca.gov/publicasp/pplinfo.asp") + # Fill in license ID to search for specific real estate professional. - print( - f"Performing action: type {variables['input1']} into the License ID input field" - ) + print(f"Performing action: type {variables['input1']} into the License ID input field") await page.act(f"type {variables['input1']} into the License ID input field") - + # Submit search form to retrieve license verification data. print("Performing action: click the Find button") await page.act("click the Find button") - + # Define schema using Pydantic class LicenseData(BaseModel): - license_type: Optional[str] = Field(None, description="Type of real estate license") - name: Optional[str] = Field(None, description="License holder's full name") - mailing_address: Optional[str] = Field(None, description="Current mailing address") - license_id: Optional[str] = Field(None, description="Unique license identifier") - expiration_date: Optional[str] = Field(None, description="License expiration date") - license_status: Optional[str] = Field(None, description="Current status (active, expired, etc.)") - salesperson_license_issued: Optional[str] = Field(None, description="Date salesperson license was issued") - former_names: Optional[str] = Field(None, description="Any previous names used") - responsible_broker: Optional[str] = Field(None, description="Associated broker name") - broker_license_id: Optional[str] = Field(None, description="Broker's license ID") - broker_address: Optional[str] = Field(None, description="Broker's business address") - disciplinary_action: Optional[str] = Field(None, description="Any disciplinary actions taken") - other_comments: Optional[str] = Field(None, description="Additional relevant information") - + license_type: str | None = Field(None, description="Type of real estate license") + name: str | None = Field(None, description="License holder's full name") + mailing_address: str | None = Field(None, description="Current mailing address") + license_id: str | None = Field(None, description="Unique license identifier") + expiration_date: str | None = Field(None, description="License expiration date") + license_status: str | None = Field( + None, description="Current status (active, expired, etc.)" + ) + salesperson_license_issued: str | None = Field( + None, description="Date salesperson license was issued" + ) + former_names: str | None = Field(None, description="Any previous names used") + responsible_broker: str | None = Field(None, description="Associated broker name") + broker_license_id: str | None = Field(None, description="Broker's license ID") + broker_address: str | None = Field(None, description="Broker's business address") + disciplinary_action: str | None = Field( + None, description="Any disciplinary actions taken" + ) + other_comments: str | None = Field(None, description="Additional relevant information") + # Extract structured license data using Pydantic schema for type safety and validation. - print( - "Extracting: extract all the license verification details for DRE#02237476" - ) + print("Extracting: extract all the license verification details for DRE#02237476") extracted_data = await page.extract( - "extract all the license verification details for DRE#02237476", - schema=LicenseData + "extract all the license verification details for DRE#02237476", schema=LicenseData ) - print(f'Extracted: {extracted_data}') - + print(f"Extracted: {extracted_data}") + print("Session closed successfully") @@ -95,4 +97,3 @@ class LicenseData(BaseModel): except Exception as err: print(f"Error: {err}") exit(1) - diff --git a/python/nurse-verification/README.md b/python/nurse-verification/README.md index 796b021..a827337 100644 --- a/python/nurse-verification/README.md +++ b/python/nurse-verification/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Nurse License Verification ## AT A GLANCE + - Goal: automate verification of nurse licenses by filling forms and extracting structured results from verification sites. - Flow: loop through license records โ†’ navigate to verification site โ†’ fill form โ†’ search โ†’ extract verification results. - Benefits: quickly verify multiple licenses without manual form filling, structured data ready for compliance tracking or HR systems. Docs โ†’ https://docs.stagehand.dev/basics/act ## GLOSSARY + - act: perform UI actions from a prompt (type, click, fill forms). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from a page using AI and Pydantic schemas. @@ -16,15 +18,16 @@ - license verification: process of confirming the validity and status of professional licenses. ## QUICKSTART - 1) cd nurse-verification - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install stagehand python-dotenv pydantic - 5) cp .env.example .env - 6) Add your Browserbase API key, Project ID, and OpenAI API key to .env - 7) python main.py + +1. cd nurse-verification +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install stagehand python-dotenv pydantic +5. cp .env.example .env # Add your Browserbase API key, Project ID, and OpenAI API key to .env +6. python main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Loops through license records in LICENSE_RECORDS array - For each record: navigates to verification site, fills form, searches @@ -34,6 +37,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and OPENAI_API_KEY - No results found: check if license numbers are valid or if verification site structure has changed @@ -42,18 +46,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข HR compliance: Automate license verification for healthcare staff onboarding and annual reviews. โ€ข Healthcare staffing: Verify credentials of temporary or contract nurses before assignment. โ€ข Regulatory reporting: Collect license status data for compliance reporting and audits. ## NEXT STEPS + โ€ข Multi-site support: Add support for different license verification sites and adapt form filling logic. โ€ข Batch processing: Load license records from CSV/Excel files for large-scale verification. โ€ข Status monitoring: Set up scheduled runs to track license status changes and expiration dates. ## 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 \ No newline at end of file + +๐Ÿ“š 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/nurse-verification/main.py b/python/nurse-verification/main.py index a03d974..2bce22f 100644 --- a/python/nurse-verification/main.py +++ b/python/nurse-verification/main.py @@ -1,11 +1,12 @@ # Stagehand + Browserbase: Automated Nurse License Verification - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field -from typing import List + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -13,6 +14,7 @@ class LicenseRecord(BaseModel): """Single license verification record""" + name: str = Field(..., description="the name of the license holder") license_number: str = Field(..., description="the license number") status: str = Field(..., description="the status of the license") @@ -21,7 +23,10 @@ class LicenseRecord(BaseModel): class LicenseResults(BaseModel): """Collection of license verification results""" - list_of_licenses: List[LicenseRecord] = Field(..., description="array of license verification results") + + list_of_licenses: list[LicenseRecord] = Field( + ..., description="array of license verification results" + ) # License records to verify - add more records as needed @@ -63,11 +68,11 @@ async def main(): # Provide live session URL for debugging and monitoring session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Watch live: https://browserbase.com/sessions/{session_id}") @@ -75,20 +80,24 @@ async def main(): # Process each license record sequentially for license_record in LICENSE_RECORDS: - print(f"Verifying license for: {license_record['FirstName']} {license_record['LastName']}") + print( + f"Verifying license for: {license_record['FirstName']} {license_record['LastName']}" + ) # Navigate to license verification site print(f"Navigating to: {license_record['Site']}") - await page.goto(license_record['Site']) + await page.goto(license_record["Site"]) await page.wait_for_load_state("domcontentloaded") # Brief timeout to ensure form fields are interactive await page.wait_for_timeout(1000) # Fill in form fields with license information print("Filling in license information...") - await page.act(f"Type \"{license_record['FirstName']}\" into the first name field") - await page.act(f"Type \"{license_record['LastName']}\" into the last name field") - await page.act(f"Type \"{license_record['LicenseNumber']}\" into the license number field") + await page.act(f'Type "{license_record["FirstName"]}" into the first name field') + await page.act(f'Type "{license_record["LastName"]}" into the last name field') + await page.act( + f'Type "{license_record["LicenseNumber"]}" into the license number field' + ) # Submit search print("Clicking search button...") @@ -102,13 +111,14 @@ async def main(): print("Extracting license verification results...") results = await page.extract( "Extract ALL the license verification results from the page, including name, license number and status", - schema=LicenseResults + schema=LicenseResults, ) print("License verification results extracted:") - + # Display results in formatted JSON import json + print(json.dumps(results.model_dump(), indent=2)) print("Session closed successfully") @@ -132,4 +142,3 @@ async def main(): except Exception as err: print(f"Application error: {err}") exit(1) - diff --git a/python/pickleball/README.md b/python/pickleball/README.md index 1cc2b9e..2dd2212 100644 --- a/python/pickleball/README.md +++ b/python/pickleball/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: AI-Powered Court Booking Automation ## AT A GLANCE + - Goal: automate tennis and pickleball court bookings in San Francisco Recreation & Parks system. - AI Integration: Stagehand for UI interaction and data extraction. - Browser Automation: automates login, filtering, court selection, and booking confirmation. @@ -8,6 +9,7 @@ Docs โ†’ https://docs.browserbase.com/features/sessions ## GLOSSARY + - act: perform UI actions from a prompt (click, type, select) Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from pages using schemas @@ -19,17 +21,18 @@ - form validation: ensure user input meets booking system requirements ## QUICKSTART -1) Create an account with SF Recreation & Parks website -> https://www.rec.us/organizations/san-francisco-rec-park -2) cd pickleball-template -3) python -m venv venv -4) source venv/bin/activate # On Windows: venv\Scripts\activate -5) pip install -r requirements.txt -6) pip install InquirerPy pydantic -7) cp .env.example .env -8) Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env -9) python main.py + +1. Create an account with SF Recreation & Parks website -> https://www.rec.us/organizations/san-francisco-rec-park +2. cd pickleball-template +3. uv venv venv +4. source venv/bin/activate # On Windows: venv\Scripts\activate +5. pip install -r requirements.txt +6. pip install InquirerPy pydantic +7. cp .env.example .env # Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env +8. python main.py ## EXPECTED OUTPUT + - Prompts user for activity type (Tennis/Pickleball), date, and time - Automates login to SF Recreation & Parks booking system - Filters courts by activity, date, and time preferences @@ -38,6 +41,7 @@ - Confirms successful booking with details ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains all required API keys and SF Rec Park login - Login failures: check SF Rec Park credentials and account status @@ -46,6 +50,7 @@ - Import errors: activate your virtual environment if you created one ## FURTHER USE CASES + โ€ข Court Booking: Automate tennis and pickleball court reservations in San Francisco โ€ข Recreation & ticketing: courts, parks, events, museum passes, campsite reservations โ€ข Appointments & scheduling: DMV, healthcare visits, test centers, field service dispatch @@ -56,6 +61,7 @@ โ€ข Internal admin portals: hardware checkout, conference-room overflow, cafeteria or shift scheduling ## NEXT STEPS + โ€ข Swap the target site: point the script at a different booking or reservation portal (e.g., gyms, coworking, campsites) โ€ข Generalize filters: extend date/time/activity prompts to handle more categories or custom filters โ€ข Automate recurring bookings: wrap the script in a scheduler (cron/queue) to secure slots automatically @@ -68,8 +74,10 @@ โ€ข Template it: strip out "pickleball" wording and reuse as a boilerplate for any authenticate โ†’ filter โ†’ extract โ†’ book workflow ## 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 + +๐Ÿ“š 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/pickleball/main.py b/python/pickleball/main.py index 5b582a5..78ceea5 100644 --- a/python/pickleball/main.py +++ b/python/pickleball/main.py @@ -1,13 +1,14 @@ # SF Court Booking Automation - See README.md for full documentation -import os import asyncio +import os from datetime import datetime, timedelta -from typing import Optional, List + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from InquirerPy import inquirer from pydantic import BaseModel, Field +from stagehand import Stagehand, StagehandConfig + # Load environment variables load_dotenv() @@ -16,9 +17,9 @@ async def login_to_site(page, email: str, password: str) -> None: print("Logging in...") # Perform login sequence: each step is atomic to handle dynamic page changes. await page.act("Click the Login button") - await page.act(f"Fill in the email or username field with \"{email}\"") + await page.act(f'Fill in the email or username field with "{email}"') await page.act("Click the next, continue, or submit button to proceed") - await page.act(f"Fill in the password field with \"{password}\"") + await page.act(f'Fill in the password field with "{password}"') await page.act("Click the login, sign in, or submit button") print("Logged in") @@ -29,30 +30,30 @@ async def select_filters(page, activity: str, time_of_day: str, selected_date: s await page.act("Click the activites drop down menu") await page.act(f"Select the {activity} activity") await page.act("Click the Done button") - + print(f"Selecting date: {selected_date}") # Open calendar to select specific date for court booking. await page.act("Click the date picker or calendar") - + # Parse date string to extract day number for calendar selection. - date_parts = selected_date.split('-') + date_parts = selected_date.split("-") if len(date_parts) != 3: raise ValueError(f"Invalid date format: {selected_date}. Expected YYYY-MM-DD") - + day_number = int(date_parts[2]) if day_number < 1 or day_number > 31: raise ValueError(f"Invalid day number: {day_number} from date: {selected_date}") - + print(f"Looking for day number: {day_number} in calendar") # Click specific day number in calendar to select date. await page.act(f"Click on the number {day_number} in the calendar") - + print(f"Selecting time of day: {time_of_day}") # Filter by time period to find courts available during preferred hours. await page.act("Click the time filter or time selection dropdown") await page.act(f"Select {time_of_day} time period") await page.act("Click the Done button") - + # Apply additional filters to show only available courts that accept reservations. await page.act("Click Available Only button") await page.act("Click All Facilities dropdown list") @@ -62,87 +63,99 @@ async def select_filters(page, activity: str, time_of_day: str, selected_date: s async def check_and_extract_courts(page, time_of_day: str) -> None: print("Checking for available courts...") - + # First observe the page to find all available court booking options. - available_courts = await page.observe("Find all available court booking slots, time slots, or court reservation options") + available_courts = await page.observe( + "Find all available court booking slots, time slots, or court reservation options" + ) print(f"Found {len(available_courts)} available court options") - + # Define schema using Pydantic class Court(BaseModel): name: str = Field(..., description="the name or identifier of the court") - opening_times: str = Field(..., description="the opening hours or operating times of the court") + opening_times: str = Field( + ..., description="the opening hours or operating times of the court" + ) location: str = Field(..., description="the location or facility name") availability: str = Field(..., description="availability status or any restrictions") - duration: Optional[str] = Field(None, description="the duration of the court session in minutes") - + duration: str | None = Field( + None, description="the duration of the court session in minutes" + ) + class CourtData(BaseModel): - courts: List[Court] - + courts: list[Court] + # Extract structured court data using Pydantic schema for type safety and validation. court_data = await page.extract( "Extract all available court booking information including court names, time slots, locations, and any other relevant details", - schema=CourtData + schema=CourtData, ) - + # Check if any courts are actually available by filtering out unavailable status messages. has_available_courts = any( - 'no free spots' not in court.availability.lower() and - 'unavailable' not in court.availability.lower() and - 'next available' not in court.availability.lower() and - 'the next available reservation' not in court.availability.lower() + "no free spots" not in court.availability.lower() + and "unavailable" not in court.availability.lower() + and "next available" not in court.availability.lower() + and "the next available reservation" not in court.availability.lower() for court in court_data.courts ) - + # If no courts available for selected time, try alternative time periods as fallback. if len(available_courts) == 0 or not has_available_courts: print("No courts available for selected time. Trying different time periods...") - + # Generate alternative time periods to try if original selection has no availability. - alternative_times = ['Afternoon', 'Evening'] if time_of_day == 'Morning' else \ - ['Morning', 'Evening'] if time_of_day == 'Afternoon' else \ - ['Morning', 'Afternoon'] - + alternative_times = ( + ["Afternoon", "Evening"] + if time_of_day == "Morning" + else ["Morning", "Evening"] + if time_of_day == "Afternoon" + else ["Morning", "Afternoon"] + ) + for alt_time in alternative_times: print(f"Trying {alt_time} time period...") - + # Change time filter to alternative time period and check availability. - await page.act(f"Click the time filter dropdown that currently shows \"{time_of_day}\"") + await page.act(f'Click the time filter dropdown that currently shows "{time_of_day}"') await page.act(f"Select {alt_time} from the time period options") await page.act("Click the Done button") - - alt_available_courts = await page.observe("Find all available court booking slots, time slots, or court reservation options") + + alt_available_courts = await page.observe( + "Find all available court booking slots, time slots, or court reservation options" + ) print(f"Found {len(alt_available_courts)} available court options for {alt_time}") - + if len(alt_available_courts) > 0: alt_court_data = await page.extract( "Extract all available court booking information including court names, time slots, locations, and any other relevant details", - schema=CourtData + schema=CourtData, ) - + has_alt_available_courts = any( - 'no free spots' not in court.availability.lower() and - 'unavailable' not in court.availability.lower() and - 'next available' not in court.availability.lower() and - 'the next available reservation' not in court.availability.lower() + "no free spots" not in court.availability.lower() + and "unavailable" not in court.availability.lower() + and "next available" not in court.availability.lower() + and "the next available reservation" not in court.availability.lower() for court in alt_court_data.courts ) - + # If alternative time has available courts, use that data and stop searching. if has_alt_available_courts: print(f"Found actually available courts for {alt_time}!") court_data.courts = alt_court_data.courts has_available_courts = True break - + # If still no available courts found, extract final court data for display. if not has_available_courts: print("Extracting final court information...") final_court_data = await page.extract( "Extract all available court booking information including court names, time slots, locations, and any other relevant details", - schema=CourtData + schema=CourtData, ) court_data.courts = final_court_data.courts - + # Display all found court information to user for review and selection. print("Available Courts:") if court_data.courts and len(court_data.courts) > 0: @@ -160,55 +173,61 @@ class CourtData(BaseModel): async def book_court(page) -> None: print("Starting court booking process...") - + try: # Select the first available court time slot for booking. print("Clicking the top available time slot...") await page.act("Click the first available time slot or court booking option") - + # Select participant from dropdown - assumes only one participant is available. print("Opening participant dropdown...") await page.act("Click the participant dropdown menu or select participant field") await page.act("Click the only named participant in the dropdown!") - + # Complete booking process and trigger verification code request. print("Clicking the book button to complete reservation...") await page.act("Click the book, reserve, or confirm booking button") await page.act("Click the Send Code Button") - + # Prompt user for verification code received via SMS/email for booking confirmation. def validate_code(text): if not text.strip(): raise ValueError("Please enter a verification code") return True - + def get_verification_code(): return inquirer.text( message="Please enter the verification code you received:", validate=validate_code, ).execute() - + verification_code = await asyncio.to_thread(get_verification_code) - + print(f"Verification code: {verification_code}") - + # Enter verification code and confirm booking to complete reservation. - await page.act(f"Fill in the verification code field with \"{verification_code}\"") + await page.act(f'Fill in the verification code field with "{verification_code}"') await page.act("Click the confirm button") - + # Define schema using Pydantic class Confirmation(BaseModel): - confirmation_message: Optional[str] = Field(None, description="any confirmation or success message") - booking_details: Optional[str] = Field(None, description="booking details like time, court, etc.") - error_message: Optional[str] = Field(None, description="any error message if booking failed") - + confirmation_message: str | None = Field( + None, description="any confirmation or success message" + ) + booking_details: str | None = Field( + None, description="booking details like time, court, etc." + ) + error_message: str | None = Field( + None, description="any error message if booking failed" + ) + # Extract booking confirmation details to verify successful reservation. print("Checking for booking confirmation...") confirmation = await page.extract( "Extract any booking confirmation message, success notification, or reservation details", - schema=Confirmation + schema=Confirmation, ) - + # Display confirmation details if booking was successful. if confirmation.confirmation_message or confirmation.booking_details: print("Booking Confirmed!") @@ -216,12 +235,12 @@ class Confirmation(BaseModel): print(f"{confirmation.confirmation_message}") if confirmation.booking_details: print(f"{confirmation.booking_details}") - + # Display error message if booking failed. if confirmation.error_message: print("Booking Error:") print(confirmation.error_message) - + except Exception as error: print(f"Error during court booking: {error}") raise error @@ -234,11 +253,11 @@ def get_activity(): message="Please select an activity:", choices=[ {"name": "Tennis", "value": "Tennis"}, - {"name": "Pickleball", "value": "Pickleball"} + {"name": "Pickleball", "value": "Pickleball"}, ], - default="Tennis" + default="Tennis", ).execute() - + activity = await asyncio.to_thread(get_activity) print(f"Selected: {activity}") @@ -253,11 +272,11 @@ def get_time_of_day(): choices=[ {"name": "Morning (Before 12 PM)", "value": "Morning"}, {"name": "Afternoon (After 12 PM)", "value": "Afternoon"}, - {"name": "Evening (After 5 PM)", "value": "Evening"} + {"name": "Evening (After 5 PM)", "value": "Evening"}, ], - default="Morning" + default="Morning", ).execute() - + time_of_day = await asyncio.to_thread(get_time_of_day) print(f"Selected: {time_of_day}") @@ -268,35 +287,30 @@ async def select_date() -> str: # Generate date options for the next 7 days including today. today = datetime.now() date_options = [] - + for i in range(7): date = today + timedelta(days=i) - - day_name = date.strftime('%A') - month_day = date.strftime('%b %-d') - full_date = date.strftime('%Y-%m-%d') - + + day_name = date.strftime("%A") + month_day = date.strftime("%b %-d") + full_date = date.strftime("%Y-%m-%d") + display_name = f"{day_name}, {month_day} (Today)" if i == 0 else f"{day_name}, {month_day}" - - date_options.append({ - "name": display_name, - "value": full_date - }) + + date_options.append({"name": display_name, "value": full_date}) # Prompt user to select from available date options. def get_date(): return inquirer.select( - message="Please select a date:", - choices=date_options, - default=date_options[0]["value"] + message="Please select a date:", choices=date_options, default=date_options[0]["value"] ).execute() - + selected_date = await asyncio.to_thread(get_date) # Format selected date for display and return ISO date string. - selected_date_obj = datetime.strptime(selected_date, '%Y-%m-%d') - display_date = selected_date_obj.strftime('%A, %B %-d, %Y') - + selected_date_obj = datetime.strptime(selected_date, "%Y-%m-%d") + display_date = selected_date_obj.strftime("%A, %B %-d, %Y") + print(f"Selected: {display_date}") return selected_date @@ -307,16 +321,16 @@ async def book_tennis_paddle_court(): # Load credentials from environment variables for SF Rec & Parks login. email = os.environ.get("SF_REC_PARK_EMAIL") password = os.environ.get("SF_REC_PARK_PASSWORD") - + # Validate that required credentials are available before proceeding. if not email or not password: raise ValueError("Missing SF_REC_PARK_EMAIL or SF_REC_PARK_PASSWORD environment variables") - + # Collect user preferences for activity, date, and time selection. activity = await select_activity() selected_date = await select_date() time_of_day = await select_time_of_day() - + print(f"Booking {activity} courts in San Francisco for {time_of_day} on {selected_date}...") # Initialize Stagehand with Browserbase for AI-powered browser automation. @@ -326,16 +340,16 @@ async def book_tennis_paddle_court(): api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), verbose=1, - # 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.) + # 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_name="openai/gpt-4.1", model_api_key=os.environ.get("OPENAI_API_KEY"), browserbase_session_create_params={ "project_id": os.environ.get("BROWSERBASE_PROJECT_ID"), "timeout": 900, - "region": "us-west-2" - } + "region": "us-west-2", + }, ) try: @@ -343,11 +357,11 @@ async def book_tennis_paddle_court(): async with Stagehand(config) as stagehand: print("Browserbase Session Started") session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Watch live: https://browserbase.com/sessions/{session_id}") @@ -357,8 +371,8 @@ async def book_tennis_paddle_court(): print("Navigating to court booking site...") await page.goto( "https://www.rec.us/organizations/san-francisco-rec-park", - wait_until='domcontentloaded', - timeout=60000 + wait_until="domcontentloaded", + timeout=60000, ) # Execute booking workflow: login, filter, find courts, and complete booking. @@ -366,7 +380,7 @@ async def book_tennis_paddle_court(): await select_filters(page, activity, time_of_day, selected_date) await check_and_extract_courts(page, time_of_day) await book_court(page) - + print("\nBrowser session closed") except Exception as error: @@ -391,7 +405,7 @@ async def main(): try: # Execute the complete court booking automation workflow. await book_tennis_paddle_court() - + print("Court booking completed successfully!") print("Your court has been reserved. Check your email for confirmation details.") except Exception as error: @@ -406,4 +420,3 @@ async def main(): except Exception as err: print(f"Application error: {err}") exit(1) - diff --git a/python/polymarket-research/README.md b/python/polymarket-research/README.md index 6117e27..beb08de 100644 --- a/python/polymarket-research/README.md +++ b/python/polymarket-research/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Polymarket Prediction Market Research ## AT A GLANCE + - Goal: automate research of prediction markets on Polymarket to extract current odds, pricing, and volume data. - Flow: navigate to polymarket.com โ†’ search for market โ†’ select result โ†’ extract market data (odds, prices, volume, changes). - Benefits: quickly gather market intelligence on prediction markets without manual browsing, structured data ready for analysis or trading decisions. Docs โ†’ https://docs.stagehand.dev/v3/first-steps/introduction ## GLOSSARY + - act: perform UI actions from a prompt (click, type, search). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from a page using AI and Pydantic schemas. @@ -14,15 +16,16 @@ - prediction market: a market where participants trade contracts based on the outcome of future events. ## QUICKSTART - 1) cd polymarket-research - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install stagehand python-dotenv pydantic - 5) cp .env.example .env - 6) Add your Browserbase API key, Project ID, and OpenAI API key to .env - 7) python main.py + +1. cd polymarket-research +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install stagehand python-dotenv pydantic +5. cp .env.example .env # Add your Browserbase API key, Project ID, and OpenAI API key to .env +6. python main.py ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Polymarket website - Searches for "Elon Musk unfollow Trump" prediction market @@ -33,6 +36,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and OPENAI_API_KEY - No search results: check if the search query returns valid markets or try different search terms @@ -40,19 +44,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Market research: Track odds and sentiment on political events, sports outcomes, or business predictions. โ€ข Trading analysis: Monitor price movements, volume trends, and market efficiency for investment decisions. โ€ข News aggregation: Collect prediction market data to supplement traditional news sources with crowd-sourced forecasts. ## NEXT STEPS + โ€ข Multi-market tracking: Loop through multiple markets to build comprehensive prediction database. โ€ข Historical analysis: Track price changes over time to identify trends and patterns. โ€ข Automated alerts: Set up scheduled runs to detect significant market movements and send notifications. ## 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 +๐Ÿ“š 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/polymarket-research/main.py b/python/polymarket-research/main.py index 0ab7fc8..cb0bf91 100644 --- a/python/polymarket-research/main.py +++ b/python/polymarket-research/main.py @@ -1,11 +1,12 @@ # Stagehand + Browserbase: Polymarket prediction market research - See README.md for full documentation -import os import asyncio +import os + from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field -from typing import Optional + +from stagehand import Stagehand, StagehandConfig # Load environment variables load_dotenv() @@ -13,12 +14,13 @@ class MarketData(BaseModel): """Market data extracted from Polymarket prediction market""" - marketTitle: Optional[str] = Field(None, description="the title of the market") - currentOdds: Optional[str] = Field(None, description="the current odds or probability") - yesPrice: Optional[str] = Field(None, description="the yes price") - noPrice: Optional[str] = Field(None, description="the no price") - totalVolume: Optional[str] = Field(None, description="the total trading volume") - priceChange: Optional[str] = Field(None, description="the recent price change") + + marketTitle: str | None = Field(None, description="the title of the market") + currentOdds: str | None = Field(None, description="the current odds or probability") + yesPrice: str | None = Field(None, description="the yes price") + noPrice: str | None = Field(None, description="the no price") + totalVolume: str | None = Field(None, description="the total trading volume") + priceChange: str | None = Field(None, description="the recent price change") async def main(): @@ -37,8 +39,8 @@ async def main(): model_name="openai/gpt-4.1", model_api_key=os.environ.get("OPENAI_API_KEY"), verbose=1, - # 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.) + # 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 ) @@ -50,11 +52,11 @@ async def main(): # Provide live session URL for debugging and monitoring session_id = None - if hasattr(stagehand, 'session_id'): + if hasattr(stagehand, "session_id"): session_id = stagehand.session_id - elif hasattr(stagehand, 'browserbase_session_id'): + elif hasattr(stagehand, "browserbase_session_id"): session_id = stagehand.browserbase_session_id - + if session_id: print(f"Watch live: https://browserbase.com/sessions/{session_id}") @@ -83,27 +85,28 @@ async def main(): print("Extracting market information...") marketData = await page.extract( "Extract the current odds and market information for the prediction market", - schema=MarketData + schema=MarketData, ) print("Market data extracted successfully:") - + # Display results in formatted JSON import json + print(json.dumps(marketData.model_dump(), indent=2)) print("Session closed successfully") except Exception as error: print(f"Error during market research: {error}") - + # Provide helpful troubleshooting information print("\nCommon issues:") print("1. Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY") print("2. Verify OPENAI_API_KEY is set in environment") print("3. Ensure internet access and https://polymarket.com is accessible") print("4. Verify Browserbase account has sufficient credits") - + raise @@ -119,4 +122,3 @@ async def main(): print(" - Verify Browserbase account has sufficient credits") print("Docs: https://docs.stagehand.dev/v3/first-steps/introduction") exit(1) - diff --git a/python/proxies/README.md b/python/proxies/README.md index e6adfc4..4b100b3 100644 --- a/python/proxies/README.md +++ b/python/proxies/README.md @@ -1,24 +1,27 @@ # Browserbase Proxy Testing Script ## AT A GLANCE + - Goal: demonstrate different proxy configurations with Browserbase sessions. ## GLOSSARY + - Proxies: Browserbase's default proxy rotation for enhanced privacy Docs โ†’ https://docs.browserbase.com/features/proxies ## QUICKSTART - 1) cd proxies-template - 2) python -m venv venv - 3) source venv/bin/activate # On Windows: venv\Scripts\activate - 4) pip install -r requirements.txt - 5) pip install browserbase playwright - 6) playwright install chromium - 7) cp .env.example .env - 8) Add your Browserbase API key and Project ID to .env - 9) python main.py + +1. cd proxies-template +2. uv venv venv +3. source venv/bin/activate # On Windows: venv\Scripts\activate +4. pip install -r requirements.txt +5. pip install browserbase playwright +6. playwright install chromium +7. cp .env.example .env # Add your Browserbase API key and Project ID to .env +8. python main.py ## EXPECTED OUTPUT + - Tests built-in proxy rotation - Tests geolocation-specific proxies (New York) - Tests custom external proxies (commented out by default) @@ -26,6 +29,7 @@ - Shows how different proxy configurations affect your apparent location ## COMMON PITFALLS + - Browserbase Developer plan or higher is required to use proxies - "ModuleNotFoundError": ensure all dependencies are installed via pip - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY @@ -34,18 +38,22 @@ - Import errors: activate your virtual environment if you created one ## USE CASES + โ€ข Geo-testing: Verify location-specific content, pricing, or compliance banners. โ€ข Scraping at scale: Rotate IPs to reduce blocks and increase CAPTCHA success rates. โ€ข Custom routing: Mix built-in and external proxies, or apply domain-based rules for compliance. ## NEXT STEPS + โ€ข Add routing rules: Configure domainPattern to direct specific sites through targeted proxies. โ€ข Test multiple geos: Compare responses from different cities/countries and log differences. โ€ข Improve reliability: Add retries and fallbacks to handle proxy errors like ERR_TUNNEL_CONNECTION_FAILED. ## 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 + +๐Ÿ“š 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/proxies/main.py b/python/proxies/main.py index 5341a08..7ee2942 100644 --- a/python/proxies/main.py +++ b/python/proxies/main.py @@ -1,12 +1,14 @@ # Browserbase Proxy Testing Script - See README.md for full documentation -import os import asyncio -from playwright.async_api import async_playwright +import os + from browserbase import Browserbase -from stagehand import Stagehand, StagehandConfig -from pydantic import BaseModel, Field from dotenv import load_dotenv +from playwright.async_api import async_playwright +from pydantic import BaseModel, Field + +from stagehand import Stagehand, StagehandConfig load_dotenv() @@ -15,6 +17,7 @@ class GeoInfo(BaseModel): """Schema for IP information and geolocation data""" + ip: str = Field(..., description="The IP address") city: str = Field(..., description="The city name") region: str = Field(..., description="The state or region") @@ -47,10 +50,10 @@ async def create_session_with_geo_location(): "geolocation": { "city": "NEW_YORK", # Simulate traffic from New York for testing geo-specific content. "state": "NY", # See https://docs.browserbase.com/features/proxies for more geolocation options. - "country": "US" - } + "country": "US", + }, } - ] + ], ) return session @@ -67,14 +70,14 @@ async def create_session_with_custom_proxies(): "username": "user", # Authentication credentials for proxy access. "password": "pass", } - ] + ], ) return session async def test_session(session_function, session_name: str): print(f"\n=== Testing {session_name} ===") - + # Create session with specific proxy configuration to test different routing scenarios. session = await session_function() print(f"Session URL: https://browserbase.com/sessions/{session.id}") @@ -85,7 +88,7 @@ async def test_session(session_function, session_name: str): default_context = browser.contexts[0] if browser.contexts else None if not default_context: raise Exception("No default context found") - + page = default_context.pages[0] if default_context.pages else None if not page: raise Exception("No page found in default context") @@ -96,27 +99,27 @@ async def test_session(session_function, session_name: str): api_key=os.environ.get("BROWSERBASE_API_KEY"), project_id=os.environ.get("BROWSERBASE_PROJECT_ID"), verbose=1, - # 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.) + # 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_name="openai/gpt-4.1", model_api_key=os.environ.get("OPENAI_API_KEY"), browserbase_session_id=session.id, # Use the existing Browserbase session ) - + stagehand = Stagehand(stagehand_config) try: - # Initialize Stagehand + # Initialize Stagehand await stagehand.init() # Navigate to IP info service to verify proxy location and IP address. await stagehand.page.goto("https://ipinfo.io/json", wait_until="domcontentloaded") - + # Extract structured IP and location data using Stagehand and Pydantic schema geo_info = await stagehand.page.extract( instruction="Extract all IP information and geolocation data from the JSON response", - schema=GeoInfo + schema=GeoInfo, ) print("Geo Info:", geo_info.model_dump_json(indent=2)) @@ -134,10 +137,10 @@ async def test_session(session_function, session_name: str): async def main(): # Test 1: Built-in proxies - Verify default proxy rotation works and shows different IPs. await test_session(create_session_with_built_in_proxies, "Built-in Proxies") - + # Test 2: Geolocation proxies - Confirm traffic routes through specified location (New York). await test_session(create_session_with_geo_location, "Geolocation Proxies (New York)") - + # Test 3: Custom external proxies - Enable if you have a custom proxy server set up. # await test_session(create_session_with_custom_proxies, "Custom External Proxies") print("\n=== All tests completed ===") diff --git a/python/website-link-tester/README.md b/python/website-link-tester/README.md index 2a42aed..6a6b7c7 100644 --- a/python/website-link-tester/README.md +++ b/python/website-link-tester/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Website Link Tester (Python) ## AT A GLANCE + - **Goal**: Crawl a websiteโ€™s homepage, collect all links, and verify that each link loads successfully and matches its link text. - **Link extraction**: Uses `page.extract()` with a Pydantic schema to pull all links and their visible text from the homepage. - **Content verification**: Opens each link and uses AI to assess whether the page content matches what the link text suggests. @@ -8,6 +9,7 @@ - **Batch processing**: Processes links in batches controlled by `MAX_CONCURRENT_LINKS` (sequential by default, can be made concurrent). ## GLOSSARY + - **Stagehand (Python v2)**: Python client that wraps AI-powered browser automation on top of Browserbase. Docs โ†’ `https://docs.stagehand.dev/python` - **extract**: Extract structured data from web pages using natural language instructions and Pydantic models. @@ -16,10 +18,11 @@ Docs โ†’ `https://docs.browserbase.com/guides/concurrency-rate-limits` ## QUICKSTART + 1. **cd into the template** - `cd python/website-link-tester` 2. **Create & activate a virtual environment (optional but recommended)** - - `python -m venv venv` + - `uv venv venv` - `source venv/bin/activate` (macOS/Linux) `venv\Scripts\activate` (Windows) 3. **Install dependencies with uvx** @@ -33,6 +36,7 @@ - `python main.py` ## EXPECTED OUTPUT + - **Initial setup** - Initializes a Stagehand session with Browserbase. - Prints a live session link for monitoring the browser in real time (when available). @@ -57,6 +61,7 @@ - Always closes browser sessions cleanly via context managers. ## COMMON PITFALLS + - **Missing credentials** - Ensure `.env` contains `BROWSERBASE_PROJECT_ID`, `BROWSERBASE_API_KEY`, and `GOOGLE_API_KEY`. - **Concurrency limits** @@ -69,12 +74,14 @@ - Social links and complex redirect chains may succeed in loading but not be fully verifiable for content; these are marked as special cases. ## USE CASES + - **Regression testing**: Quickly verify that all key marketing and product links on your homepage still resolve correctly after a deployment. - **Content QA**: Detect mismatches between link text and destination page content (e.g., wrong page wired to a CTA). - **SEO and UX audits**: Find broken or misdirected links that can harm search rankings or user experience. - **Monitoring**: Run this periodically to flag link issues across your marketing site or documentation hub. ## TUNING BATCH SIZE & CONCURRENCY + - **`MAX_CONCURRENT_LINKS` in `main.py`** - Default: `1` โ†’ sequential link verification (works on all plans). - Set to `> 1` โ†’ more concurrent link verifications per batch (requires higher Browserbase concurrency limits). @@ -86,6 +93,7 @@ - Apply different limits for external vs internal links if desired. ## NEXT STEPS + - **Filter link scopes**: Limit verification to specific path prefixes (e.g., only `/docs` or `/blog`) or exclude certain domains. - **Recursive crawling**: Start from the homepage, follow internal links to secondary/tertiary pages, and cascade link discovery deeper into the site to build a more complete link map. - **Alerting & monitoring**: Integrate with Slack, email, or logging tools to notify when links start failing. @@ -93,10 +101,10 @@ - **Richer assessments**: Expand the extraction schema to capture additional metadata (e.g., HTTP status code, canonical URL, or key headings). ## HELPFUL RESOURCES + - ๐Ÿ“š **Stagehand Docs**: `https://docs.stagehand.dev/v2/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/website-link-tester/main.py b/python/website-link-tester/main.py index 8b9a677..1349feb 100644 --- a/python/website-link-tester/main.py +++ b/python/website-link-tester/main.py @@ -1,14 +1,14 @@ # Stagehand + Browserbase: Website Link Tester - See README.md for full documentation -import os import asyncio import json -from typing import List, Optional +import os from dotenv import load_dotenv -from stagehand import Stagehand, StagehandConfig from pydantic import BaseModel, Field, HttpUrl +from stagehand import Stagehand, StagehandConfig + # Load environment variables load_dotenv() @@ -33,7 +33,7 @@ class ExtractedLink(BaseModel): class ExtractedLinks(BaseModel): """Collection of extracted links""" - links: List[ExtractedLink] + links: list[ExtractedLink] class LinkVerificationResult(BaseModel): @@ -42,10 +42,10 @@ class LinkVerificationResult(BaseModel): link_text: str url: HttpUrl success: bool - page_title: Optional[str] = None - content_matches: Optional[bool] = None - assessment: Optional[str] = None - error: Optional[str] = None + page_title: str | None = None + content_matches: bool | None = None + assessment: str | None = None + error: str | None = None class PageVerificationSummary(BaseModel): @@ -89,12 +89,12 @@ def create_stagehand() -> Stagehand: return Stagehand(config) -def deduplicate_links(extracted_links: ExtractedLinks) -> List[ExtractedLink]: +def deduplicate_links(extracted_links: ExtractedLinks) -> list[ExtractedLink]: """ Removes duplicate links by URL while preserving the first occurrence. """ seen_urls: set[str] = set() - unique_links: List[ExtractedLink] = [] + unique_links: list[ExtractedLink] = [] for link in extracted_links.links: url = str(link.url) @@ -106,14 +106,14 @@ def deduplicate_links(extracted_links: ExtractedLinks) -> List[ExtractedLink]: return unique_links -async def collect_links_from_homepage() -> List[ExtractedLink]: +async def collect_links_from_homepage() -> list[ExtractedLink]: """ Opens the homepage and uses Stagehand `extract()` to collect all links. Returns a de-duplicated list of link objects that we will later verify. """ print("Collecting links from homepage...") - stagehand: Optional[Stagehand] = None + stagehand: Stagehand | None = None try: stagehand = create_stagehand() @@ -147,7 +147,7 @@ async def collect_links_from_homepage() -> List[ExtractedLink]: ) print( json.dumps( - {"links": [link.model_dump(mode='json') for link in unique_links]}, indent=2 + {"links": [link.model_dump(mode="json") for link in unique_links]}, indent=2 ) ) @@ -167,7 +167,7 @@ async def verify_single_link(link: ExtractedLink) -> LinkVerificationResult: """ print(f"\nChecking: {link.link_text} ({link.url})") - stagehand: Optional[Stagehand] = None + stagehand: Stagehand | None = None try: stagehand = create_stagehand() @@ -199,9 +199,7 @@ async def verify_single_link(link: ExtractedLink) -> LinkVerificationResult: # For social links, we consider a successful load good enough if is_social_link: - print( - f"[{link.link_text}] Social media link - skipping content verification" - ) + print(f"[{link.link_text}] Social media link - skipping content verification") return LinkVerificationResult( link_text=link.link_text, url=link.url, @@ -248,8 +246,8 @@ async def verify_single_link(link: ExtractedLink) -> LinkVerificationResult: async def verify_links_in_batches( - links: List[ExtractedLink], -) -> List[LinkVerificationResult]: + links: list[ExtractedLink], +) -> list[LinkVerificationResult]: """ Verifies all links in batches to avoid opening too many concurrent sessions. Returns a list of LinkVerificationResult objects for all processed links. @@ -257,32 +255,24 @@ async def verify_links_in_batches( max_concurrent = max(1, MAX_CONCURRENT_LINKS) print(f"\nVerifying links in batches of {max_concurrent}...") - results: List[LinkVerificationResult] = [] + results: list[LinkVerificationResult] = [] for i in range(0, len(links), max_concurrent): batch = links[i : i + max_concurrent] batch_number = i // max_concurrent + 1 total_batches = (len(links) + max_concurrent - 1) // max_concurrent - print( - f"\n=== Processing batch {batch_number}/{total_batches} ({len(batch)} links) ===" - ) + print(f"\n=== Processing batch {batch_number}/{total_batches} ({len(batch)} links) ===") - batch_results = await asyncio.gather( - *[verify_single_link(link) for link in batch] - ) + batch_results = await asyncio.gather(*[verify_single_link(link) for link in batch]) results.extend(batch_results) - print( - f"\nBatch {batch_number}/{total_batches} complete ({len(results)} total verified)" - ) + print(f"\nBatch {batch_number}/{total_batches} complete ({len(results)} total verified)") return results -def output_results( - results: List[LinkVerificationResult], label: str = "FINAL RESULTS" -) -> None: +def output_results(results: list[LinkVerificationResult], label: str = "FINAL RESULTS") -> None: """ Logs a JSON summary of all link verification results. Falls back to a brief textual summary if JSON serialization fails. @@ -295,7 +285,7 @@ def output_results( "total_links": len(results), "successful": len([r for r in results if r.success]), "failed": len([r for r in results if not r.success]), - "results": [r.model_dump(mode='json') for r in results], + "results": [r.model_dump(mode="json") for r in results], } try: @@ -319,7 +309,7 @@ async def main(): """ print("Starting Website Link Tester (Python)...") - results: List[LinkVerificationResult] = [] + results: list[LinkVerificationResult] = [] try: links = await collect_links_from_homepage() @@ -337,14 +327,10 @@ async def main(): print("\nError occurred during execution:", error) if results: - print( - f"\nOutputting partial results ({len(results)} links processed before error):" - ) + print(f"\nOutputting partial results ({len(results)} links processed before error):") output_results(results, "PARTIAL RESULTS (Error Occurred)") else: - print( - "No results to output - error occurred before any links were verified" - ) + print("No results to output - error occurred before any links were verified") raise @@ -359,4 +345,4 @@ async def main(): print(" - Verify GOOGLE_API_KEY is set") print(" - Ensure URL is reachable from Browserbase regions") print("Docs: https://docs.stagehand.dev/v3/first-steps/introduction") - raise SystemExit(1) \ No newline at end of file + raise SystemExit(1) diff --git a/typescript/business-lookup/README.md b/typescript/business-lookup/README.md index 5448251..fcd596e 100644 --- a/typescript/business-lookup/README.md +++ b/typescript/business-lookup/README.md @@ -1,24 +1,28 @@ # Stagehand + Browserbase: Business Lookup with Agent ## AT A GLANCE + - Goal: Automate business registry searches using an autonomous AI agent with computer-use capabilities. - Uses Stagehand Agent in CUA mode to navigate complex UI elements, apply filters, and extract structured business data. - Demonstrates extraction with Zod schema validation for consistent data retrieval. - Docs โ†’ https://docs.stagehand.dev/basics/agent ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent - extract: extract structured data from web pages using natural language instructions Docs โ†’ https://docs.stagehand.dev/basics/extract ## QUICKSTART -1) npm install -2) cp .env.example .env -3) Add required API keys/IDs to .env -4) npm start + +1. npm install +2. cp .env.example .env +3. Add required API keys/IDs to .env +4. npm start ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Displays live session link for monitoring - Navigates to SF Business Registry search page @@ -29,6 +33,7 @@ - Closes session cleanly ## COMMON PITFALLS + - Dependency install errors: ensure npm install completed - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_API_KEY - Google API access: ensure you have access to Google's gemini-2.5-computer-use-preview-10-2025 model @@ -36,18 +41,22 @@ - Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in ## USE CASES + โ€ข Business verification: Automate registration status checks, license validation, and compliance verification for multiple businesses. โ€ข Data enrichment: Collect structured business metadata (NAICS codes, addresses, ownership) for research or CRM updates. โ€ข Due diligence: Streamline background checks by autonomously searching and extracting business registration details from public registries. ## NEXT STEPS + โ€ข Parameterize search: Accept business names as command-line arguments or from a CSV file for batch processing. โ€ข Expand extraction: Add support for additional fields like tax status, licenses, or historical registration changes. โ€ข Multi-registry support: Extend agent to search across multiple city or state business registries with routing logic. ## 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 + +๐Ÿ“š 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/business-lookup/index.ts b/typescript/business-lookup/index.ts index e049c9d..aa168d5 100644 --- a/typescript/business-lookup/index.ts +++ b/typescript/business-lookup/index.ts @@ -19,7 +19,9 @@ async function main() { // Initialize browser session to start automation. await stagehand.init(); console.log("Stagehand initialized successfully!"); - console.log(`Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`); + console.log( + `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, + ); const page = stagehand.context.pages()[0]; @@ -32,9 +34,10 @@ async function main() { cua: true, // Enable Computer Use Agent mode model: { modelName: "google/gemini-2.5-computer-use-preview-10-2025", - apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY - }, - systemPrompt: "You are a helpful assistant that can use a web browser to search for business information.", + apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, + }, + systemPrompt: + "You are a helpful assistant that can use a web browser to search for business information.", }); console.log(`Searching for business: ${businessName}`); @@ -68,7 +71,6 @@ async function main() { console.log("Business Information:"); console.log(JSON.stringify(businessInfo, null, 2)); - } catch (error) { console.error("Error during business lookup:", error); } finally { @@ -86,4 +88,3 @@ main().catch((err) => { console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); process.exit(1); }); - diff --git a/typescript/company-address-finder/README.md b/typescript/company-address-finder/README.md index b23deb1..b288fb7 100644 --- a/typescript/company-address-finder/README.md +++ b/typescript/company-address-finder/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Company Address Finder ## AT A GLANCE + - Goal: Automate discovery of company legal information and physical addresses from Terms of Service and Privacy Policy pages. - CUA Agent: Uses autonomous computer-use agent to search for company homepages via Google and navigate to legal documents. - Data Extraction: Extracts structured data including homepage URLs, ToS/Privacy Policy links, and physical mailing addresses. @@ -9,6 +10,7 @@ - Scalable: Supports both sequential and concurrent processing (concurrent requires Startup/Developer plan or higher). ## GLOSSARY + - agent: autonomous AI agent with computer-use capabilities that can navigate websites like a human Docs โ†’ https://docs.stagehand.dev/basics/agent - extract: pull structured data from web pages using natural language instructions and Zod schemas @@ -20,14 +22,16 @@ - exponential backoff: retry strategy that increases wait time between attempts for reliability ## QUICKSTART -1) cd company-address-finder -2) npm install -3) cp .env.example .env -4) Add your Browserbase API key, Project ID, and Google Generative AI API key to .env -5) Edit COMPANY_NAMES array in index.ts to specify which companies to process -6) npm start + +1. cd company-address-finder +2. npm install +3. cp .env.example .env +4. Add your Browserbase API key, Project ID, and Google Generative AI API key to .env +5. Edit COMPANY_NAMES array in index.ts to specify which companies to process +6. npm start ## EXPECTED OUTPUT + - Initializes browser session for each company with live view link - Agent navigates to Google and searches for company homepage - Extracts Terms of Service and Privacy Policy links from homepage @@ -37,6 +41,7 @@ - Displays processing status and session closure for each company ## COMMON PITFALLS + - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_GENERATIVE_AI_API_KEY - Google API access: ensure you have access to google/gemini-2.5-computer-use-preview-10-2025 model - Concurrent processing: MAX_CONCURRENT > 1 requires Browserbase Startup or Developer plan or higher (default is 1 for sequential) @@ -45,12 +50,14 @@ - Session timeouts: long-running batches may hit 900s timeout (adjust browserbaseSessionCreateParams if needed) ## USE CASES + โ€ข Legal compliance research: Collect company addresses and legal document URLs for due diligence, vendor verification, or compliance audits. โ€ข Business intelligence: Build datasets of company locations and legal information for market research or competitive analysis. โ€ข Contact data enrichment: Augment CRM or database records with verified physical addresses extracted from official company documents. โ€ข Multi-company batch processing: Process lists of companies (investors, partners, clients) to gather standardized location data at scale. ## NEXT STEPS + โ€ข Parameterize inputs: Accept company names from CSV files, command-line arguments, or API endpoints for dynamic batch processing. โ€ข Expand extraction: Add support for additional fields like contact emails, phone numbers, business registration numbers, or founding dates. โ€ข Multi-source validation: Cross-reference addresses from multiple pages (About, Contact, Footer) to improve accuracy and confidence. @@ -58,9 +65,10 @@ โ€ข Error handling: Implement more granular error categorization (not found vs. no address vs. extraction failure) for better reporting. ## 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 +๐Ÿ“š 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/company-address-finder/index.ts b/typescript/company-address-finder/index.ts index e9d9fee..8bf2f20 100644 --- a/typescript/company-address-finder/index.ts +++ b/typescript/company-address-finder/index.ts @@ -25,10 +25,10 @@ async function withRetry( fn: () => Promise, description: string, maxRetries: number = 3, - delayMs: number = 2000 + delayMs: number = 2000, ): Promise { let lastError: Error | null = null; - + for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await fn(); @@ -36,11 +36,11 @@ async function withRetry( lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < maxRetries) { console.log(`${description} - Attempt ${attempt} failed, retrying in ${delayMs}ms...`); - await new Promise(resolve => setTimeout(resolve, delayMs)); + await new Promise((resolve) => setTimeout(resolve, delayMs)); } } } - + throw new Error(`${description} - Failed after ${maxRetries} attempts: ${lastError?.message}`); } @@ -49,9 +49,9 @@ async function withRetry( // Falls back to Privacy Policy if address not found in Terms of Service async function processCompany(companyName: string): Promise { console.log(`\nProcessing: ${companyName}`); - + let stagehand: Stagehand | null = null; - + try { // Initialize Stagehand with Browserbase stagehand = new Stagehand({ @@ -75,25 +75,22 @@ async function processCompany(companyName: string): Promise { console.log(`[${companyName}] Initializing browser session...`); await stagehand.init(); const sessionId = stagehand.browserbaseSessionId; - + if (!sessionId) { throw new Error(`Failed to initialize browser session for ${companyName}`); } - + console.log(`[${companyName}] Live View Link: https://browserbase.com/sessions/${sessionId}`); const page = stagehand.context.pages()[0]; // Navigate to Google as starting point for CUA agent to search and find company homepage console.log(`[${companyName}] Navigating to Google...`); - await withRetry( - async () => { - await page.goto("https://www.google.com/", { - waitUntil: "domcontentloaded", - }); - }, - `[${companyName}] Initial navigation to Google` - ); + await withRetry(async () => { + await page.goto("https://www.google.com/", { + waitUntil: "domcontentloaded", + }); + }, `[${companyName}] Initial navigation to Google`); // Create CUA agent for autonomous navigation // Agent can interact with the browser like a human: search, click, scroll, and navigate @@ -109,16 +106,13 @@ async function processCompany(companyName: string): Promise { }); console.log(`[${companyName}] Finding company homepage using CUA agent...`); - await withRetry( - async () => { - await agent.execute({ - instruction: `Navigate to the ${companyName} website`, - maxSteps: 5, - highlightCursor: true, - }); - }, - `[${companyName}] Navigation to website` - ); + await withRetry(async () => { + await agent.execute({ + instruction: `Navigate to the ${companyName} website`, + maxSteps: 5, + highlightCursor: true, + }); + }, `[${companyName}] Navigation to website`); const homepageUrl = page.url(); console.log(`[${companyName}] Homepage found: ${homepageUrl}`); @@ -130,13 +124,13 @@ async function processCompany(companyName: string): Promise { "extract the link to the Terms of Service page (may also be labeled as Terms of Use, Terms and Conditions, or similar equivalent names)", z.object({ termsOfServiceLink: z.string().url(), - }) + }), ), stagehand.extract( "extract the link to the Privacy Policy page (may also be labeled as Privacy Notice, Privacy Statement, or similar equivalent names)", z.object({ privacyPolicyLink: z.string().url(), - }) + }), ), ]); @@ -158,19 +152,16 @@ async function processCompany(companyName: string): Promise { // Try Terms of Service first - most likely to contain physical address for legal/contact purposes if (termsOfServiceLink) { console.log(`[${companyName}] Extracting address from Terms of Service...`); - await withRetry( - async () => { - await page.goto(termsOfServiceLink); - }, - `[${companyName}] Navigate to Terms of Service` - ); + await withRetry(async () => { + await page.goto(termsOfServiceLink); + }, `[${companyName}] Navigate to Terms of Service`); try { const addressResult = await stagehand.extract( "Extract the physical company mailing address (street, city, state, postal code, and country if present) from the Terms of Service page. Ignore phone numbers or email addresses.", z.object({ companyAddress: z.string(), - }) + }), ); const companyAddress = addressResult.companyAddress || ""; @@ -179,26 +170,25 @@ async function processCompany(companyName: string): Promise { console.log(`[${companyName}] Address found in Terms of Service: ${address}`); } } catch (error) { - console.log(`[${companyName}] Could not extract address from Terms of Service page`); + console.log(`[${companyName}] Could not extract address from Terms of Service page: ${error}`); } } // Fallback: check Privacy Policy if address not found in Terms of Service if (!address && privacyPolicyLink) { - console.log(`[${companyName}] Address not found in Terms of Service, trying Privacy Policy...`); - await withRetry( - async () => { - await page.goto(privacyPolicyLink); - }, - `[${companyName}] Navigate to Privacy Policy` + console.log( + `[${companyName}] Address not found in Terms of Service, trying Privacy Policy...`, ); + await withRetry(async () => { + await page.goto(privacyPolicyLink); + }, `[${companyName}] Navigate to Privacy Policy`); try { const addressResult = await stagehand.extract( "Extract the physical company mailing address(street, city, state, postal code, and country if present) from the Privacy Policy page. Ignore phone numbers or email addresses.", z.object({ companyAddress: z.string(), - }) + }), ); const companyAddress = addressResult.companyAddress || ""; @@ -207,7 +197,7 @@ async function processCompany(companyName: string): Promise { console.log(`[${companyName}] Address found in Privacy Policy: ${address}`); } } catch (error) { - console.log(`[${companyName}] Could not extract address from Privacy Policy page`); + console.log(`[${companyName}] Could not extract address from Privacy Policy page: ${error}`); } } @@ -226,10 +216,9 @@ async function processCompany(companyName: string): Promise { console.log(`[${companyName}] Successfully processed`); return result; - } catch (error) { console.error(`[${companyName}] Error:`, error); - + return { companyName, homepageUrl: "", @@ -260,7 +249,9 @@ async function main(): Promise { const companyCount = companyNames.length; const isSequential = maxConcurrent === 1; - console.log(`\nProcessing ${companyCount} ${companyCount === 1 ? 'company' : 'companies'} ${isSequential ? 'sequentially' : `concurrently (batch size: ${maxConcurrent})`}...`); + console.log( + `\nProcessing ${companyCount} ${companyCount === 1 ? "company" : "companies"} ${isSequential ? "sequentially" : `concurrently (batch size: ${maxConcurrent})`}...`, + ); const allResults: CompanyData[] = []; @@ -282,8 +273,10 @@ async function main(): Promise { const batchPromises = batch.map((companyName) => processCompany(companyName)); const batchResults = await Promise.all(batchPromises); allResults.push(...batchResults); - - console.log(`Batch ${batchNumber}/${totalBatches} completed: ${batchResults.length} companies processed`); + + console.log( + `Batch ${batchNumber}/${totalBatches} completed: ${batchResults.length} companies processed`, + ); } } @@ -304,4 +297,4 @@ main().catch((err) => { console.error(" - Ensure COMPANY_NAMES is configured in the config section"); console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/company-value-prop-generator/README.md b/typescript/company-value-prop-generator/README.md index 958ed8c..6e5cf78 100644 --- a/typescript/company-value-prop-generator/README.md +++ b/typescript/company-value-prop-generator/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Value Prop One-Liner Generator ## AT A GLANCE + - Goal: Automatically extract and format website value propositions into concise one-liners for email personalization - Demonstrates Stagehand's `extract` method with Zod schemas to pull structured data from landing pages - Shows direct LLM API usage via `stagehand.llmClient` to transform extracted content with custom prompts @@ -8,18 +9,21 @@ - Docs โ†’ https://docs.browserbase.com/stagehand/extract ## GLOSSARY + - Extract: Stagehand method that uses AI to pull structured data from pages using natural language instructions Docs โ†’ https://docs.browserbase.com/stagehand/extract - Value Proposition: The core benefit or unique selling point a company communicates to customers ## QUICKSTART -1) cd typescript/company-value-prop-generator -2) npm install -3) cp .env.example .env -4) Add required API keys/IDs to .env -5) npm start + +1. cd typescript/company-value-prop-generator +2. npm install +3. cp .env.example .env +4. Add required API keys/IDs to .env +5. npm start ## EXPECTED OUTPUT + - Stagehand initializes and creates a Browserbase session - Displays live session link for monitoring - Navigates to target domain and waits for page load @@ -31,6 +35,7 @@ - Closes browser session ## COMMON PITFALLS + - Dependency install errors: ensure npm install completed - Missing credentials: - BROWSERBASE_PROJECT_ID (required for browser automation) @@ -41,17 +46,21 @@ - Slow-loading sites: 5-minute timeout configured, but extremely slow sites may still timeout ## USE CASES + โ€ข Generate personalized email openers by extracting value props from prospect domains โ€ข Build prospecting tools that automatically understand what companies do from their websites โ€ข Create dynamic messaging systems that adapt content based on extracted company information ## NEXT STEPS + โ€ข Batch process multiple domains by iterating over a list and aggregating results โ€ข Extract additional metadata like company description, industry tags, or key features alongside value prop โ€ข Add caching layer to avoid re-extracting value props for previously analyzed domains ## HELPFUL RESOURCES -๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction -๐ŸŽฎ Browserbase: https://www.browserbase.com -๐Ÿ’ก Templates: https://www.browserbase.com/templates -๐Ÿ“ง Need help? support@browserbase.com \ No newline at end of file + +๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +๐ŸŽฎ Browserbase: https://www.browserbase.com +๐Ÿ’ก Templates: https://www.browserbase.com/templates +๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ’ฌ Discord: http://stagehand.dev/discord diff --git a/typescript/company-value-prop-generator/index.ts b/typescript/company-value-prop-generator/index.ts index 484ddcf..1cc3392 100644 --- a/typescript/company-value-prop-generator/index.ts +++ b/typescript/company-value-prop-generator/index.ts @@ -12,70 +12,72 @@ const targetDomain = "www.browserbase.com"; // Or extract from email: email.spli * Extracts the value prop using Stagehand, then uses an LLM to format it into a short phrase starting with "your". */ async function generateOneLiner(domain: string): Promise { - const stagehand = new Stagehand({ - env: "BROWSERBASE", - model: "openai/gpt-4.1", - verbose: 0, // 0 = errors only, 1 = info, 2 = debug - browserbaseSessionCreateParams: { - projectId: process.env.BROWSERBASE_PROJECT_ID!, - }, - }); - - try { - await stagehand.init(); - console.log("Stagehand initialized successfully!"); - console.log(`Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`); - - const page = stagehand.context.pages()[0]; - - // Navigate to domain - console.log(`๐ŸŒ Navigating to https://${domain}...`); - // 5min timeout to handle slow-loading sites or network issues - await page.goto(`https://${domain}/`, { - waitUntil: "domcontentloaded", - timeoutMs: 300000, - }); - - console.log(`โœ… Successfully loaded ${domain}`); - - // Extract value proposition from landing page - console.log(`๐Ÿ“ Extracting value proposition for ${domain}...`); - const valueProp = await stagehand.extract( - "extract the value proposition from the landing page", - z.object({ - value_prop: z.string(), - }), - ); - - console.log(`๐Ÿ“Š Extracted value prop for ${domain}:`, valueProp.value_prop); - - // Validate extraction returned meaningful content - if ( - !valueProp.value_prop || - valueProp.value_prop.toLowerCase() === "null" || - valueProp.value_prop.toLowerCase() === "undefined" - ) { - console.error(`โš ๏ธ Value prop extraction returned empty or invalid result`); - throw new Error(`No value prop found for ${domain}`); - } - - // Generate one-liner using OpenAI - // Prompt uses few-shot examples to guide LLM toward concise, "your X" format - // System prompt enforces constraints (9 words max, no quotes, must start with "your") - console.log(`๐Ÿค– Generating email one-liner for ${domain}...`); - - const response = await stagehand.llmClient.createChatCompletion({ - logger: () => {}, // Suppress verbose LLM logs - options: { - messages: [ - { - role: "system", - content: - "You are an expert at generating concise, unique descriptions of companies. Generate ONLY a concise description (no greetings or extra text). Don't use generic adjectives like 'comprehensive', 'innovative', or 'powerful'. Keep it short and concise, no more than 9 words. DO NOT USE QUOTES. Only use English. You MUST start the response with 'your'.", - }, - { - role: "user", - content: `The response will be inserted into this template: "{response}" + const stagehand = new Stagehand({ + env: "BROWSERBASE", + model: "openai/gpt-4.1", + verbose: 0, // 0 = errors only, 1 = info, 2 = debug + browserbaseSessionCreateParams: { + projectId: process.env.BROWSERBASE_PROJECT_ID!, + }, + }); + + try { + await stagehand.init(); + console.log("Stagehand initialized successfully!"); + console.log( + `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, + ); + + const page = stagehand.context.pages()[0]; + + // Navigate to domain + console.log(`๐ŸŒ Navigating to https://${domain}...`); + // 5min timeout to handle slow-loading sites or network issues + await page.goto(`https://${domain}/`, { + waitUntil: "domcontentloaded", + timeoutMs: 300000, + }); + + console.log(`โœ… Successfully loaded ${domain}`); + + // Extract value proposition from landing page + console.log(`๐Ÿ“ Extracting value proposition for ${domain}...`); + const valueProp = await stagehand.extract( + "extract the value proposition from the landing page", + z.object({ + value_prop: z.string(), + }), + ); + + console.log(`๐Ÿ“Š Extracted value prop for ${domain}:`, valueProp.value_prop); + + // Validate extraction returned meaningful content + if ( + !valueProp.value_prop || + valueProp.value_prop.toLowerCase() === "null" || + valueProp.value_prop.toLowerCase() === "undefined" + ) { + console.error(`โš ๏ธ Value prop extraction returned empty or invalid result`); + throw new Error(`No value prop found for ${domain}`); + } + + // Generate one-liner using OpenAI + // Prompt uses few-shot examples to guide LLM toward concise, "your X" format + // System prompt enforces constraints (9 words max, no quotes, must start with "your") + console.log(`๐Ÿค– Generating email one-liner for ${domain}...`); + + const response = await stagehand.llmClient.createChatCompletion({ + logger: () => {}, // Suppress verbose LLM logs + options: { + messages: [ + { + role: "system", + content: + "You are an expert at generating concise, unique descriptions of companies. Generate ONLY a concise description (no greetings or extra text). Don't use generic adjectives like 'comprehensive', 'innovative', or 'powerful'. Keep it short and concise, no more than 9 words. DO NOT USE QUOTES. Only use English. You MUST start the response with 'your'.", + }, + { + role: "user", + content: `The response will be inserted into this template: "{response}" Examples: Value prop: "Supercharge your investment team with AI-powered research" @@ -86,66 +88,63 @@ Response: "your video-first approach to food delivery" Value prop: "${valueProp.value_prop}" Response:`, - }, - ], - }, - }); - - const oneLiner = String( - response.choices?.[0]?.message?.content || "", - ).trim(); - - // Validate LLM response is usable (not empty, not generic placeholder) - console.log(`๐Ÿ” Validating generated one-liner...`); - if ( - !oneLiner || - oneLiner.toLowerCase() === "null" || - oneLiner.toLowerCase() === "undefined" || - oneLiner.toLowerCase() === "your company" - ) { - console.error(`โš ๏ธ LLM generated invalid or placeholder response: "${oneLiner}"`); - throw new Error( - `No valid one-liner generated for ${domain}. AI response: "${oneLiner}"`, - ); - } - - console.log(`โœจ Generated one-liner for ${domain}:`, oneLiner); - return oneLiner; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`โŒ Generation failed for ${domain}: ${errorMessage}`); - throw error; - } finally { - await stagehand.close(); - console.log("Session closed successfully"); - } + }, + ], + }, + }); + + const oneLiner = String(response.choices?.[0]?.message?.content || "").trim(); + + // Validate LLM response is usable (not empty, not generic placeholder) + console.log(`๐Ÿ” Validating generated one-liner...`); + if ( + !oneLiner || + oneLiner.toLowerCase() === "null" || + oneLiner.toLowerCase() === "undefined" || + oneLiner.toLowerCase() === "your company" + ) { + console.error(`โš ๏ธ LLM generated invalid or placeholder response: "${oneLiner}"`); + throw new Error(`No valid one-liner generated for ${domain}. AI response: "${oneLiner}"`); + } + + console.log(`โœจ Generated one-liner for ${domain}:`, oneLiner); + return oneLiner; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`โŒ Generation failed for ${domain}: ${errorMessage}`); + throw error; + } finally { + await stagehand.close(); + console.log("Session closed successfully"); + } } /** * Main entry point: generates a one-liner value proposition for the target domain. */ async function main() { - console.log("Starting One-Liner Generator..."); - - try { - const oneLiner = await generateOneLiner(targetDomain); - console.log("\nโœ… Success!"); - console.log(`One-liner: ${oneLiner}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`\nโŒ Error: ${errorMessage}`); - console.error("\nCommon issues:"); - console.error(" - Check .env file has OPENAI_API_KEY set (required for LLM generation)"); - console.error(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY set (required for browser automation)"); - console.error(" - Ensure the domain is accessible and not a placeholder/maintenance page"); - console.error(" - Verify internet connectivity and that the target site is reachable"); - console.error("Docs: https://docs.browserbase.com/stagehand"); - process.exit(1); - } + console.log("Starting One-Liner Generator..."); + + try { + const oneLiner = await generateOneLiner(targetDomain); + console.log("\nโœ… Success!"); + console.log(`One-liner: ${oneLiner}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`\nโŒ Error: ${errorMessage}`); + console.error("\nCommon issues:"); + console.error(" - Check .env file has OPENAI_API_KEY set (required for LLM generation)"); + console.error( + " - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY set (required for browser automation)", + ); + console.error(" - Ensure the domain is accessible and not a placeholder/maintenance page"); + console.error(" - Verify internet connectivity and that the target site is reachable"); + console.error("Docs: https://docs.browserbase.com/stagehand"); + process.exit(1); + } } main().catch((err) => { - console.error("Fatal error:", err); - process.exit(1); + console.error("Fatal error:", err); + process.exit(1); }); - diff --git a/typescript/context/README.md b/typescript/context/README.md index 970ed91..47286cb 100644 --- a/typescript/context/README.md +++ b/typescript/context/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Context Authentication Example ## AT A GLANCE + - Goal: demonstrate persistent authentication using Browserbase **contexts** that survive across sessions. - Flow: create context โ†’ log in once โ†’ persist cookies/tokens โ†’ reuse context in a new session โ†’ extract data โ†’ clean up. - Benefits: skip re-auth on subsequent runs, reduce MFA prompts, speed up protected flows, and keep state stable across retries. Docs โ†’ https://docs.browserbase.com/features/contexts ## GLOSSARY + - context: a persistent browser state (cookies, localStorage, cache) stored server-side and reusable by new sessions. Docs โ†’ https://docs.browserbase.com/features/contexts - persist: when true, any state changes during a session are written back to the context for future reuse. @@ -14,37 +16,44 @@ Docs โ†’ https://docs.stagehand.dev/basics/act ## QUICKSTART - 1) cd context-template - 2) npm install - 3) npm install axios - 4) cp .env.example .env - 5) Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env - 6) npm start + +1. cd context-template +2. npm install +3. npm install axios +4. cp .env.example .env +5. Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env +6. npm start ## EXPECTED OUTPUT + - Creates context, performs login, saves auth state - Reuses context in new session to access authenticated pages - Extracts user data using structured schemas - Cleans up context after completion ## COMMON PITFALLS + - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains all required variables - Context persistence: ensure persist: true is set to save login state ## USE CASES + โ€ข Persistent login sessions: Automate workflows that require authentication without re-logging in every run. โ€ข Access to gated content: Crawl or extract data from behind login walls (e.g., booking portals, dashboards, intranets). โ€ข Multi-step workflows: Maintain cookies/tokens across different automation steps or scheduled jobs. ## NEXT STEPS + โ€ข Extend to multiple apps: Reuse the same context across different authenticated websites within one session. โ€ข Add session validation: Extract and verify account info (e.g., username, profile details) to confirm successful auth. โ€ข Secure lifecycle: Rotate, refresh, and delete contexts programmatically to enforce security policies. ## 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 + +๐Ÿ“š 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/context/index.ts b/typescript/context/index.ts index e202d03..6c5977e 100644 --- a/typescript/context/index.ts +++ b/typescript/context/index.ts @@ -7,15 +7,15 @@ import { z } from "zod"; import axios from "axios"; async function createSessionContextID() { - console.log("Creating new Browserbase context..."); + console.log("Creating new Browserbase context..."); // First create a context using Browserbase SDK to get a context ID. const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY! }); const context = await bb.contexts.create({ projectId: process.env.BROWSERBASE_PROJECT_ID!, }); - + console.log("Created context ID:", context.id); - + // Create a single session using the context ID to perform initial login. console.log("Creating session for initial login..."); const session = await bb.sessions.create({ @@ -28,7 +28,7 @@ async function createSessionContextID() { }, }); console.log("Live view: https://browserbase.com/sessions/" + session.id); - + // Connect Stagehand to the existing session (no new session created). console.log("Connecting Stagehand to session..."); const stagehand = new Stagehand({ @@ -39,16 +39,16 @@ async function createSessionContextID() { }); await stagehand.init(); // Connect to existing session for login process. - + const page = stagehand.context.pages()[0]; const email = process.env.SF_REC_PARK_EMAIL; const password = process.env.SF_REC_PARK_PASSWORD; - + // Navigate to login page with extended timeout for slow-loading sites. console.log("Navigating to SF Rec & Park login page..."); await page.goto("https://www.rec.us/organizations/san-francisco-rec-park", { - waitUntil: 'domcontentloaded', - timeout: 60000 + waitUntil: "domcontentloaded", + timeout: 60000, }); // Perform login sequence: each step is atomic to handle dynamic page changes. @@ -59,11 +59,10 @@ async function createSessionContextID() { await page.act(`Fill in the password field with "${password}"`); await page.act("Click the login, sign in, or submit button"); console.log("Login sequence completed!"); - - + await stagehand.close(); console.log("Authentication state saved to context"); - + // Return the context ID for reuse in future sessions. return { id: context.id }; } @@ -73,25 +72,23 @@ async function deleteContext(contextId: string) { console.log("Cleaning up context:", contextId); // Delete context via Browserbase API to clean up stored authentication data. // This prevents accumulation of unused contexts and ensures security cleanup. - const response = await axios.delete( - `https://api.browserbase.com/v1/contexts/${contextId}`, - { - headers: { - "X-BB-API-Key": process.env.BROWSERBASE_API_KEY, - }, + const response = await axios.delete(`https://api.browserbase.com/v1/contexts/${contextId}`, { + headers: { + "X-BB-API-Key": process.env.BROWSERBASE_API_KEY, }, - ); + }); console.log("Context deleted successfully (status:", response.status + ")"); - } catch (error: any) { - console.error("Error deleting context:", error.response?.data || error.message || error); + } catch (error: unknown) { + const err = error as { response?: { data?: unknown }; message?: string }; + console.error("Error deleting context:", err.response?.data || err.message || error); } } async function main() { - console.log("Starting Context Authentication Example..."); + console.log("Starting Context Authentication Example..."); // Create context with login state for reuse in authenticated sessions. const contextId = await createSessionContextID(); - + // Initialize new session using existing context to inherit authentication state. // persist: true ensures any new changes (cookies, cache) are saved back to context. const stagehand = new Stagehand({ @@ -118,13 +115,13 @@ async function main() { // Navigate to authenticated area - should skip login due to persisted cookies. console.log("Navigating to authenticated area (should skip login)..."); await page.goto("https://www.rec.us/organizations/san-francisco-rec-park", { - waitUntil: 'domcontentloaded', - timeout: 60000 + waitUntil: "domcontentloaded", + timeout: 60000, }); // Navigate to user-specific area to access personal data. await page.act("Click on the reservations button"); - + // Extract structured user data using Zod schema for type safety. // Schema ensures consistent data format and validates extracted content. console.log("Extracting user profile data..."); @@ -132,17 +129,16 @@ async function main() { instruction: "Extract the user's full name and address", schema: z.object({ fullName: z.string().describe("the user's full name"), - address: z.string().describe("the user's address") + address: z.string().describe("the user's address"), }), }); - - console.log("Extracted user data:", userData); + console.log("Extracted user data:", userData); // Always close session to release resources and save any context changes. await stagehand.close(); console.log("Session closed successfully"); - + // Clean up context to prevent accumulation and ensure security. await deleteContext(contextId.id); } @@ -156,4 +152,3 @@ main().catch((err) => { console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); process.exit(1); }); - diff --git a/typescript/council-events/README.md b/typescript/council-events/README.md index 3592f47..1393eb8 100644 --- a/typescript/council-events/README.md +++ b/typescript/council-events/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Council Events Automation ## AT A GLANCE + - Goal: demonstrate how to automate event information extraction from Philadelphia Council. - Navigation & Search: automate website navigation, calendar selection, and year filtering. - Data Extraction: extract structured event data with validated output using Zod schemas. - Practical Example: extract council events with name, date, and time information. ## GLOSSARY + - act: perform UI actions from a natural language prompt (type, click, navigate). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from web pages into validated objects. @@ -17,13 +19,15 @@ - structured data extraction: convert unstructured web content into typed, validated objects. ## QUICKSTART - 1) cd councilEvents - 2) npm install - 3) cp .env.example .env (or create .env with BROWSERBASE_API_KEY) - 4) Add your Browserbase API key to .env - 5) npm start + +1. cd councilEvents +2. npm install +3. cp .env.example .env (or create .env with BROWSERBASE_API_KEY) +4. Add your Browserbase API key to .env +5. npm start ## EXPECTED OUTPUT + - Navigates to Philadelphia Council website - Clicks calendar from the navigation menu - Selects 2025 from the month dropdown @@ -31,27 +35,31 @@ - Returns typed object with event information ## COMMON PITFALLS + - "Cannot find module 'dotenv'": ensure npm install ran successfully - Missing API key: verify .env is loaded and file is not committed - Events not found: check if the website structure has changed or if no events exist for the selected period - Schema validation errors: ensure extracted data matches Zod schema structure ## USE CASES + โ€ข Event tracking: automate monitoring of council meetings and public events. โ€ข Calendar aggregation: collect event information for integration with calendar systems. โ€ข Public information access: extract structured event data for citizens and researchers. โ€ข Meeting scheduling: track upcoming council meetings for attendance planning. ## NEXT STEPS + โ€ข Date filtering: make the year or date selection configurable via environment variables or prompts. โ€ข Multi-year extraction: extend the flow to extract events from multiple years in parallel. โ€ข Calendar integration: add logic to export extracted events to calendar formats (iCal, Google Calendar). โ€ข Event notifications: add logic to send alerts for upcoming meetings or important events. ## 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 +๐Ÿ“š 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/council-events/index.ts b/typescript/council-events/index.ts index 4336397..7d9967c 100644 --- a/typescript/council-events/index.ts +++ b/typescript/council-events/index.ts @@ -8,79 +8,81 @@ import { z } from "zod"; * Uses AI-powered browser automation to navigate and interact with the site. */ async function main() { - console.log("Starting Philadelphia Council Events automation..."); + console.log("Starting Philadelphia Council Events automation..."); - // Initialize Stagehand with Browserbase for cloud-based browser automation - const stagehand = new Stagehand({ - env: "BROWSERBASE", - verbose: 1, - // 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: "openai/gpt-4.1", - }); + // Initialize Stagehand with Browserbase for cloud-based browser automation + const stagehand = new Stagehand({ + env: "BROWSERBASE", + verbose: 1, + // 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: "openai/gpt-4.1", + }); - try { - // Initialize browser session - console.log("Initializing browser session..."); - await stagehand.init(); - console.log("Stagehand session started successfully"); + try { + // Initialize browser session + console.log("Initializing browser session..."); + await stagehand.init(); + console.log("Stagehand session started successfully"); - // Provide live session URL for debugging and monitoring - console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`); + // Provide live session URL for debugging and monitoring + console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`); - const page = stagehand.context.pages()[0]; + const page = stagehand.context.pages()[0]; - // Navigate to Philadelphia Council - console.log("Navigating to: https://phila.legistar.com/"); - await page.goto("https://phila.legistar.com/"); - console.log("Page loaded successfully"); + // Navigate to Philadelphia Council + console.log("Navigating to: https://phila.legistar.com/"); + await page.goto("https://phila.legistar.com/"); + console.log("Page loaded successfully"); - // Click calendar from the navigation menu - console.log("Clicking calendar from the navigation menu"); - await stagehand.act("click calendar from the navigation menu"); + // Click calendar from the navigation menu + console.log("Clicking calendar from the navigation menu"); + await stagehand.act("click calendar from the navigation menu"); - // Select 2025 from the month dropdown - console.log("Selecting 2025 from the month dropdown"); - await stagehand.act("select 2025 from the month dropdown"); + // Select 2025 from the month dropdown + console.log("Selecting 2025 from the month dropdown"); + await stagehand.act("select 2025 from the month dropdown"); - // Extract event data using AI to parse the structured information - console.log("Extracting event information..."); - const results = await stagehand.extract( - "Extract the table with the name, date and time of the events", - z.object({ - results: z.array(z.object({ - name: z.string(), - date: z.string(), - time: z.string(), - })), - }), - ); + // Extract event data using AI to parse the structured information + console.log("Extracting event information..."); + const results = await stagehand.extract( + "Extract the table with the name, date and time of the events", + z.object({ + results: z.array( + z.object({ + name: z.string(), + date: z.string(), + time: z.string(), + }), + ), + }), + ); - console.log(`Found ${results.results.length} events`); - console.log("Event data extracted successfully:"); - console.log(JSON.stringify(results, null, 2)); - } catch (error) { - console.error("Error during event extraction:", error); + console.log(`Found ${results.results.length} events`); + console.log("Event data extracted successfully:"); + console.log(JSON.stringify(results, null, 2)); + } catch (error) { + console.error("Error during event extraction:", error); - // Provide helpful troubleshooting information - console.error("\nCommon issues:"); - console.error("1. Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); - console.error("2. Verify OPENAI_API_KEY is set in environment"); - console.error("3. Ensure internet access and https://phila.legistar.com is accessible"); - console.error("4. Verify Browserbase account has sufficient credits"); - console.error("5. Check if the calendar page structure has changed"); + // Provide helpful troubleshooting information + console.error("\nCommon issues:"); + console.error("1. Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); + console.error("2. Verify OPENAI_API_KEY is set in environment"); + console.error("3. Ensure internet access and https://phila.legistar.com is accessible"); + console.error("4. Verify Browserbase account has sufficient credits"); + console.error("5. Check if the calendar page structure has changed"); - throw error; - } finally { - // Clean up browser session - console.log("Closing browser session..."); - await stagehand.close(); - console.log("Session closed successfully"); - } + throw error; + } finally { + // Clean up browser session + console.log("Closing browser session..."); + await stagehand.close(); + console.log("Session closed successfully"); + } } main().catch((err) => { - console.error("Application error:", err); - process.exit(1); -}); \ No newline at end of file + console.error("Application error:", err); + process.exit(1); +}); diff --git a/typescript/download-financial-statements/README.md b/typescript/download-financial-statements/README.md index d453bb5..df9bd13 100644 --- a/typescript/download-financial-statements/README.md +++ b/typescript/download-financial-statements/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Download Apple's Quarterly Financial Statements ## AT A GLANCE + - Goal: automate downloading Apple's quarterly financial statements (PDFs) from their investor relations site. - Download Handling: Browserbase automatically captures PDFs opened during the session and bundles them into a ZIP file. - Retry Logic: polls Browserbase downloads API with configurable timeout to ensure files are ready before retrieval. - Live Debugging: displays live view URL for real-time session monitoring. ## GLOSSARY + - act: perform UI actions from a prompt (click, scroll, navigate) Docs โ†’ https://docs.stagehand.dev/basics/act - downloads API: retrieve files downloaded during a Browserbase session as a ZIP archive @@ -15,13 +17,15 @@ Docs โ†’ https://docs.browserbase.com/features/session-live-view ## QUICKSTART - 1) cd download-financial-statements - 2) npm install - 3) cp .env.example .env - 4) Add your Browserbase API key and Project ID to .env - 5) npm start + +1. cd download-financial-statements +2. npm install +3. cp .env.example .env +4. Add your Browserbase API key and Project ID to .env +5. npm start ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Apple.com โ†’ Investors section - Locates Q1-Q4 2025 quarterly earnings reports @@ -31,6 +35,7 @@ - Displays session history and closes cleanly ## COMMON PITFALLS + - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY - Download timeout: increase `retryForSeconds` parameter if downloads take longer than 45 seconds @@ -38,19 +43,22 @@ - Network issues: check internet connection and Apple website accessibility ## USE CASES + โ€ข Financial reporting automation: Download quarterly/annual reports from investor relations sites for analysis, archiving, or compliance. โ€ข Document batch retrieval: Collect multiple PDFs (contracts, invoices, statements) from web portals without manual clicking. โ€ข Scheduled data collection: Run on cron/Lambda to automatically fetch latest financial filings or regulatory documents. ## NEXT STEPS + โ€ข Generalize for other sites: Extract URL patterns, adapt act() prompts, and support multiple companies/document types. โ€ข Parse downloaded PDFs: Unzip, OCR/parse text (PyPDF2/pdfplumber), and load into structured format (CSV/DB/JSON). โ€ข Add validation: Check file count, sizes, naming conventions; alert on failures; retry missing quarters. ## 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 +๐Ÿ“š 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/download-financial-statements/index.ts b/typescript/download-financial-statements/index.ts index 00df760..d139edf 100644 --- a/typescript/download-financial-statements/index.ts +++ b/typescript/download-financial-statements/index.ts @@ -1,134 +1,137 @@ // Stagehand + Browserbase: Download Apple's Quarterly Financial Statements - See README.md for full documentation -import { Browserbase } from '@browserbasehq/sdk'; -import { Stagehand } from '@browserbasehq/stagehand'; -import 'dotenv/config'; -import fs from 'fs'; +import { Browserbase } from "@browserbasehq/sdk"; +import { Stagehand } from "@browserbasehq/stagehand"; +import "dotenv/config"; +import fs from "fs"; /** * Polls Browserbase API for downloads with timeout handling. * Retries every 2 seconds until downloads are ready or timeout is reached. */ async function saveDownloadsWithRetry( - bb: Browserbase, - sessionId: string, - retryForSeconds: number = 30 + bb: Browserbase, + sessionId: string, + retryForSeconds: number = 30, ): Promise { - return new Promise((resolve, reject) => { - console.log(`Waiting up to ${retryForSeconds} seconds for downloads to complete...`); - let poller: NodeJS.Timeout; - - // Set timeout to prevent infinite polling if downloads never complete - const timeout: NodeJS.Timeout = setTimeout(() => { - if (poller) { - clearInterval(poller); - reject(new Error("Download timeout exceeded")); - } - }, retryForSeconds * 1000); - - async function fetchDownloads(): Promise { - try { - console.log("Checking for downloads..."); - const response = await bb.sessions.downloads.list(sessionId); - const downloadBuffer: ArrayBuffer = await response.arrayBuffer(); - - if (downloadBuffer.byteLength > 0) { - console.log(`Downloads ready! File size: ${downloadBuffer.byteLength} bytes`); - fs.writeFileSync('downloaded_files.zip', Buffer.from(downloadBuffer)); - console.log("Files saved as: downloaded_files.zip"); - - clearInterval(poller); - clearTimeout(timeout); - resolve(downloadBuffer.byteLength); - } else { - console.log("Downloads not ready yet, retrying..."); - } - } catch (e: unknown) { - console.error('Error fetching downloads:', e); - clearInterval(poller); - clearTimeout(timeout); - reject(e); - } + return new Promise((resolve, reject) => { + console.log(`Waiting up to ${retryForSeconds} seconds for downloads to complete...`); + + const intervals = { + poller: undefined as NodeJS.Timeout | undefined, + timeout: undefined as NodeJS.Timeout | undefined, + }; + + async function fetchDownloads(): Promise { + try { + console.log("Checking for downloads..."); + const response = await bb.sessions.downloads.list(sessionId); + const downloadBuffer: ArrayBuffer = await response.arrayBuffer(); + + if (downloadBuffer.byteLength > 0) { + console.log(`Downloads ready! File size: ${downloadBuffer.byteLength} bytes`); + fs.writeFileSync("downloaded_files.zip", Buffer.from(downloadBuffer)); + console.log("Files saved as: downloaded_files.zip"); + + if (intervals.poller) clearInterval(intervals.poller); + if (intervals.timeout) clearTimeout(intervals.timeout); + resolve(downloadBuffer.byteLength); + } else { + console.log("Downloads not ready yet, retrying..."); } + } catch (e: unknown) { + console.error("Error fetching downloads:", e); + if (intervals.poller) clearInterval(intervals.poller); + if (intervals.timeout) clearTimeout(intervals.timeout); + reject(e); + } + } - // Poll every 2 seconds to check if downloads are ready - poller = setInterval(fetchDownloads, 2000); - }); + // Set timeout to prevent infinite polling if downloads never complete + intervals.timeout = setTimeout(() => { + if (intervals.poller) { + clearInterval(intervals.poller); + } + reject(new Error("Download timeout exceeded")); + }, retryForSeconds * 1000); + + // Poll every 2 seconds to check if downloads are ready + intervals.poller = setInterval(fetchDownloads, 2000); + }); } async function main(): Promise { - console.log("Starting Apple Financial Statements Download Automation..."); - - console.log("Initializing Browserbase client..."); - const bb: Browserbase = new Browserbase({ - apiKey: process.env.BROWSERBASE_API_KEY as string - }); - - // Initialize Stagehand with Browserbase for cloud-based browser automation - const stagehand: 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 - logger: console.log, - disablePino: true - }); - - try { - // Initialize browser session to start automation - await stagehand.init(); - console.log("Stagehand initialized successfully!"); - const context = stagehand.context; - const page = context.pages()[0]; - - // Display live view URL for debugging and monitoring - const liveViewLinks = await bb.sessions.debug(stagehand.browserbaseSessionId!); - console.log(`Live View Link: ${liveViewLinks.debuggerFullscreenUrl}`); - - // Navigate to Apple homepage with extended timeout for slow-loading sites - console.log("Navigating to Apple.com..."); - await page.goto("https://www.apple.com/", { timeoutMs: 60000 }); - - // Navigate to investor relations section - console.log("Navigating to Investors section..."); - await stagehand.act("Click the 'Investors' button at the bottom of the page'"); - await stagehand.act("Scroll down to the Financial Data section of the page"); - await stagehand.act("Under Quarterly Earnings Reports, click on '2025'"); - - // Download all quarterly financial statements - // When a URL of a PDF is opened, Browserbase automatically downloads and stores the PDF - // See https://docs.browserbase.com/features/screenshots#pdfs for more info - console.log("Downloading quarterly financial statements..."); - await stagehand.act("Click the 'Financial Statements' link under Q4"); - await stagehand.act("Click the 'Financial Statements' link under Q3"); - await stagehand.act("Click the 'Financial Statements' link under Q2"); - await stagehand.act("Click the 'Financial Statements' link under Q1"); - - // Retrieve all downloads triggered during this session from Browserbase API - console.log("Retrieving downloads from Browserbase..."); - await saveDownloadsWithRetry(bb, stagehand.browserbaseSessionId!, 45); - console.log("All downloads completed successfully!"); - - console.log("\nStagehand History:"); - console.log(stagehand.history); - - } catch (error) { - console.error("Error during automation:", error); - throw error; - } finally { - // Always close session to release resources and clean up - await stagehand.close(); - console.log("Session closed successfully"); - } + console.log("Starting Apple Financial Statements Download Automation..."); + + console.log("Initializing Browserbase client..."); + const bb: Browserbase = new Browserbase({ + apiKey: process.env.BROWSERBASE_API_KEY as string, + }); + + // Initialize Stagehand with Browserbase for cloud-based browser automation + const stagehand: 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 + logger: console.log, + disablePino: true, + }); + + try { + // Initialize browser session to start automation + await stagehand.init(); + console.log("Stagehand initialized successfully!"); + const context = stagehand.context; + const page = context.pages()[0]; + + // Display live view URL for debugging and monitoring + const liveViewLinks = await bb.sessions.debug(stagehand.browserbaseSessionId!); + console.log(`Live View Link: ${liveViewLinks.debuggerFullscreenUrl}`); + + // Navigate to Apple homepage with extended timeout for slow-loading sites + console.log("Navigating to Apple.com..."); + await page.goto("https://www.apple.com/", { timeoutMs: 60000 }); + + // Navigate to investor relations section + console.log("Navigating to Investors section..."); + await stagehand.act("Click the 'Investors' button at the bottom of the page'"); + await stagehand.act("Scroll down to the Financial Data section of the page"); + await stagehand.act("Under Quarterly Earnings Reports, click on '2025'"); + + // Download all quarterly financial statements + // When a URL of a PDF is opened, Browserbase automatically downloads and stores the PDF + // See https://docs.browserbase.com/features/screenshots#pdfs for more info + console.log("Downloading quarterly financial statements..."); + await stagehand.act("Click the 'Financial Statements' link under Q4"); + await stagehand.act("Click the 'Financial Statements' link under Q3"); + await stagehand.act("Click the 'Financial Statements' link under Q2"); + await stagehand.act("Click the 'Financial Statements' link under Q1"); + + // Retrieve all downloads triggered during this session from Browserbase API + console.log("Retrieving downloads from Browserbase..."); + await saveDownloadsWithRetry(bb, stagehand.browserbaseSessionId!, 45); + console.log("All downloads completed successfully!"); + + console.log("\nStagehand History:"); + console.log(stagehand.history); + } catch (error) { + console.error("Error during automation:", error); + throw error; + } finally { + // Always close session to release resources and clean up + await stagehand.close(); + console.log("Session closed successfully"); + } } main().catch((err) => { - console.error("Application error:", err); - console.error("Common issues:"); - console.error(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); - console.error(" - Verify internet connection and Apple website accessibility"); - console.error(" - Ensure sufficient timeout for slow-loading pages"); - console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); - process.exit(1); -}); \ No newline at end of file + console.error("Application error:", err); + console.error("Common issues:"); + console.error(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); + console.error(" - Verify internet connection and Apple website accessibility"); + console.error(" - Ensure sufficient timeout for slow-loading pages"); + console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); + process.exit(1); +}); diff --git a/typescript/form-filling/README.md b/typescript/form-filling/README.md index e0a1ac5..f72d10c 100644 --- a/typescript/form-filling/README.md +++ b/typescript/form-filling/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Form Filling Automation ## AT A GLANCE + - Goal: showcase how to automate form filling with Stagehand and Browserbase. - Smart Form Automation: dynamically fill contact forms with variable-driven data. - Field Detection: analyze page structure with `observe` before interacting with fields. @@ -8,6 +9,7 @@ Docs โ†’ https://docs.browserbase.com/features/sessions ## GLOSSARY + - act: perform UI actions from a prompt (type, click, fill forms) Docs โ†’ https://docs.stagehand.dev/basics/act - observe: analyze a page and return selectors or action plans before executing @@ -15,13 +17,15 @@ - variable substitution: inject dynamic values into actions using `%variable%` syntax ## QUICKSTART - 1) cd form-fill-template - 2) npm install - 3) cp .env.example .env - 4) Add your Browserbase API key, Project ID, and OpenAI API key to .env - 5) npm start + +1. cd form-fill-template +2. npm install +3. cp .env.example .env +4. Add your Browserbase API key, Project ID, and OpenAI API key to .env +5. npm start ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to contact form page - Analyzes available form fields using observe @@ -30,6 +34,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains all required API keys - Form detection: ensure target page has fillable form fields @@ -37,18 +42,22 @@ - Network issues: check internet connection and website accessibility ## USE CASES + โ€ข Lead & intake automation: Auto-fill contact/quote/request forms from CRM or CSV to speed up inbound/outbound workflows. โ€ข QA & regression testing: Validate form fields, required rules, and error states across releases/environments. โ€ข Bulk registrations & surveys: Programmatically complete repeatable sign-ups or survey passes for pilots and internal ops. ## NEXT STEPS + โ€ข Wire in data sources: Load variables from CSV/JSON/CRM, map fields via observe, and support per-site field aliases. โ€ข Submit & verify: Enable submit, capture success toasts/emails, take screenshots, and retry on validation errors. โ€ข Handle complex widgets: Add file uploads, multi-step flows, dropdown/radio/datepickers, and basic anti-bot tactics (delays/proxies). ## 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 + +๐Ÿ“š 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/form-filling/index.ts b/typescript/form-filling/index.ts index 489e38e..7329b1b 100644 --- a/typescript/form-filling/index.ts +++ b/typescript/form-filling/index.ts @@ -10,88 +10,90 @@ const lastName = "Johnson"; const company = "TechCorp Solutions"; const jobTitle = "Software Developer"; const email = "alex.johnson@techcorp.com"; -const message = "Hello, I'm interested in learning more about your services and would like to schedule a demo."; +const message = + "Hello, I'm interested in learning more about your services and would like to schedule a demo."; async function main() { - console.log("Starting Form Filling Example..."); - - // Initialize Stagehand with Browserbase for cloud-based browser automation. - const stagehand = new Stagehand({ - env: "BROWSERBASE", - model: "openai/gpt-4.1", - verbose: 1, - browserbaseSessionCreateParams: { - projectId: process.env.BROWSERBASE_PROJECT_ID!, - } - }); + console.log("Starting Form Filling Example..."); - try { - // Initialize browser session to start automation. - await stagehand.init(); - console.log("Stagehand initialized successfully!"); - console.log(`Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`); + // Initialize Stagehand with Browserbase for cloud-based browser automation. + const stagehand = new Stagehand({ + env: "BROWSERBASE", + model: "openai/gpt-4.1", + verbose: 1, + browserbaseSessionCreateParams: { + projectId: process.env.BROWSERBASE_PROJECT_ID!, + }, + }); - const page = stagehand.context.pages()[0]; + try { + // Initialize browser session to start automation. + await stagehand.init(); + console.log("Stagehand initialized successfully!"); + console.log( + `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`, + ); - // Navigate to contact page with extended timeout for slow-loading sites. - console.log("Navigating to Browserbase contact page..."); - await page.goto('https://www.browserbase.com/contact', { - waitUntil: 'domcontentloaded', // Wait for DOM to be ready before proceeding. - timeout: 60000 // Extended timeout for reliable page loading. - }); + const page = stagehand.context.pages()[0]; - // Single observe call to plan all form filling - const formFields = await stagehand.observe( - "Find form fields for: first name, last name, company, job title, email, message" - ); + // Navigate to contact page with extended timeout for slow-loading sites. + console.log("Navigating to Browserbase contact page..."); + await page.goto("https://www.browserbase.com/contact", { + waitUntil: "domcontentloaded", // Wait for DOM to be ready before proceeding. + timeout: 60000, // Extended timeout for reliable page loading. + }); - // Execute all actions without LLM calls - for (const field of formFields) { - // Match field to data based on description - let value = ''; - const desc = field.description.toLowerCase(); - - if (desc.includes('first name')) value = firstName; - else if (desc.includes('last name')) value = lastName; - else if (desc.includes('company')) value = company; - else if (desc.includes('job title')) value = jobTitle; - else if (desc.includes('email')) value = email; - else if (desc.includes('message')) value = message; - - if (value) { - await stagehand.act({ - ...field, - arguments: [value] - }); - } - } - - // Language choice in Stagehand act() is crucial for reliable automation. - // Use "click" for dropdown interactions rather than "select" - await stagehand.act("Click on the How Can we help? dropdown"); - await stagehand.act("Click on the first option from the dropdown"); - // await stagehand.act("Select the first option from the dropdown"); // Less reliable than "click" + // Single observe call to plan all form filling + const formFields = await stagehand.observe( + "Find form fields for: first name, last name, company, job title, email, message", + ); - // Uncomment the line below if you want to submit the form - // await stagehand.act("Click the submit button"); - - console.log("Form filled successfully! Waiting 3 seconds..."); - await page.waitForTimeout(30000); + // Execute all actions without LLM calls + for (const field of formFields) { + // Match field to data based on description + let value = ""; + const desc = field.description.toLowerCase(); - } catch (error) { - console.error(`Error during form filling: ${error}`); - } finally { - // Always close session to release resources and clean up. - await stagehand.close(); - console.log("Session closed successfully"); - } + if (desc.includes("first name")) value = firstName; + else if (desc.includes("last name")) value = lastName; + else if (desc.includes("company")) value = company; + else if (desc.includes("job title")) value = jobTitle; + else if (desc.includes("email")) value = email; + else if (desc.includes("message")) value = message; + + if (value) { + await stagehand.act({ + ...field, + arguments: [value], + }); + } + } + + // Language choice in Stagehand act() is crucial for reliable automation. + // Use "click" for dropdown interactions rather than "select" + await stagehand.act("Click on the How Can we help? dropdown"); + await stagehand.act("Click on the first option from the dropdown"); + // await stagehand.act("Select the first option from the dropdown"); // Less reliable than "click" + + // Uncomment the line below if you want to submit the form + // await stagehand.act("Click the submit button"); + + console.log("Form filled successfully! Waiting 3 seconds..."); + await page.waitForTimeout(30000); + } catch (error) { + console.error(`Error during form filling: ${error}`); + } finally { + // Always close session to release resources and clean up. + await stagehand.close(); + console.log("Session closed successfully"); + } } main().catch((err) => { - console.error("Error in form filling example:", err); - console.error("Common issues:"); - console.error(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); - console.error(" - Ensure form fields are available on the contact page"); - console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); - process.exit(1); + console.error("Error in form filling example:", err); + console.error("Common issues:"); + console.error(" - Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); + console.error(" - Ensure form fields are available on the contact page"); + console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); + process.exit(1); }); diff --git a/typescript/gemini-cua/README.md b/typescript/gemini-cua/README.md index ad72e30..89bb7ae 100644 --- a/typescript/gemini-cua/README.md +++ b/typescript/gemini-cua/README.md @@ -1,21 +1,25 @@ # Stagehand + Browserbase: Computer Use Agent (CUA) Example ## AT A GLANCE + - Goal: demonstrate autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase. - Uses Stagehand Agent to automate complex workflows with AI powered browser agents - Leverages Google's computer-use-preview model for autonomous web interaction and decision-making. ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent ## QUICKSTART - 1) npm install - 2) cp .env.example .env - 3) Add your Browserbase API key, Project ID, and Google API key to .env - 4) npm start + +1. npm install +2. cp .env.example .env +3. Add your Browserbase API key, Project ID, and Google API key to .env +4. npm start ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Google search engine - Executes autonomous search and data extraction task @@ -24,23 +28,28 @@ - Closes session cleanly ## COMMON PITFALLS + - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_API_KEY - Google API access: ensure you have access to Google's computer-use-preview model ## USE CASES + โ€ข Autonomous research: Let AI agents independently research topics, gather information, and compile reports without manual intervention. โ€ข Complex web workflows: Automate multi-step processes that require decision-making, form filling, and data extraction across multiple pages. โ€ข Content discovery: Search for specific information, verify data accuracy, and cross-reference sources autonomously. ## NEXT STEPS + โ€ข Customize instructions: Modify the instruction variable to test different autonomous tasks and scenarios. โ€ข Add error handling: Implement retry logic, fallback strategies, and better error recovery for failed agent actions. โ€ข Extend capabilities: Add support for file downloads, form submissions, and more complex interaction patterns. ## 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 + +๐Ÿ“š 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/gemini-cua/index.ts b/typescript/gemini-cua/index.ts index 26412d6..868467d 100644 --- a/typescript/gemini-cua/index.ts +++ b/typescript/gemini-cua/index.ts @@ -21,13 +21,12 @@ const instruction = `Search for the next visible solar eclipse in North America // ============================================================================ async function main() { - const stagehand = new Stagehand({ env: "BROWSERBASE", // model: "google/gemini-2.5-pro", // this is the model stagehand uses in act, observe, extract (not agent) verbose: 1, - // 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.) + // 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 browserbaseSessionCreateParams: { projectId: process.env.BROWSERBASE_PROJECT_ID!, @@ -37,8 +36,8 @@ async function main() { blockAds: true, viewport: { width: 1288, - height: 711 - } + height: 711, + }, }, }, }); @@ -47,13 +46,15 @@ async function main() { // Initialize browser session to start automation. await stagehand.init(); console.log("Stagehand initialized successfully!"); - console.log(`Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`); + console.log( + `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, + ); const page = stagehand.context.pages()[0]; // Navigate to search engine with extended timeout for slow-loading sites. await page.goto("https://www.google.com/", { - waitUntil: 'domcontentloaded', + waitUntil: "domcontentloaded", }); // Create agent with computer use capabilities for autonomous web browsing. @@ -61,18 +62,18 @@ async function main() { cua: true, model: { modelName: "google/gemini-2.5-computer-use-preview-10-2025", - apiKey: process.env.GOOGLE_API_KEY + apiKey: process.env.GOOGLE_API_KEY, }, systemPrompt: `You are a helpful assistant that can use a web browser. You are currently on the following page: ${page.url()}. - Do not ask follow up questions, the user will trust your judgement. If you are getting blocked on google, try another search engine.` + Do not ask follow up questions, the user will trust your judgement. If you are getting blocked on google, try another search engine.`, }); console.log("Executing instruction:", instruction); const result = await agent.execute({ instruction: instruction, maxSteps: 30, - highlightCursor: true + highlightCursor: true, }); if (result.success === true) { @@ -81,7 +82,6 @@ async function main() { } else { console.log("Task failed or was incomplete"); } - } catch (error) { console.error("Error executing computer use agent:", error); } finally { @@ -97,4 +97,4 @@ main().catch((err) => { console.error(" - Verify GOOGLE_API_KEY is set for the agent"); console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/gift-finder/README.md b/typescript/gift-finder/README.md index 0537c27..e32826a 100644 --- a/typescript/gift-finder/README.md +++ b/typescript/gift-finder/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: AI-Powered Gift Finder ## AT A GLANCE + - Goal: find personalized gift recommendations using AI-generated search queries and intelligent product scoring. - AI Integration: Stagehand for AI-generated search queries and score products based on recipient profile. - Concurrent Sessions: runs multiple browser sessions simultaneously to search different queries in parallel. - Proxies: uses Browserbase proxies with UK geolocation for European website access (Firebox.eu). ## GLOSSARY + - act: perform UI actions from a prompt (search, click, type) Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from pages using schemas @@ -17,14 +19,16 @@ Docs โ†’ https://docs.browserbase.com/features/proxies ## QUICKSTART - 1) cd gift-finder-template - 2) npm install - 3) npm install inquirer openai - 4) cp .env.example .env - 5) Add your Browserbase API key, Project ID, and OpenAI API key to .env - 6) npm start + +1. cd gift-finder-template +2. npm install +3. npm install inquirer openai +4. cp .env.example .env +5. Add your Browserbase API key, Project ID, and OpenAI API key to .env +6. npm start ## EXPECTED OUTPUT + - Prompts user for recipient and description - Generates 3 search queries using OpenAI - Runs concurrent browser sessions to search Firebox.eu @@ -33,24 +37,29 @@ - Displays top 3 personalized gift recommendations ## COMMON PITFALLS + - Browserbase Developer plan or higher is required to use proxies (they have been commented out in the code) - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains all required API keys - Search failures: check internet connection and website accessibility ## USE CASES + โ€ข Multi-retailer product discovery: Generate smart queries, browse in parallel, and extract structured results across sites (with geo-specific proxies when needed). โ€ข Personalized gifting/recommendations: Score items against a recipient profile for gift lists, concierge shopping, or corporate gifting portals. โ€ข Assortment & market checks: Rapidly sample categories to compare price/availability/ratings across regions or competitors. ## NEXT STEPS + โ€ข Add site adapters: Plug in more retailers with per-site extract schemas, result normalization, and de-duplication (canonical URL matching). โ€ข Upgrade ranking: Blend AI scores with signals (price, reviews, shipping, stock), and persist results to JSON/CSV/DB for re-scoring and audits. โ€ข Scale & geo-test: Fan out more concurrent sessions and run a geo matrix via proxies (e.g., UK/EU/US) to compare localized inventory and pricing. ## 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 + +๐Ÿ“š 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/gift-finder/index.ts b/typescript/gift-finder/index.ts index ba8209c..56e160c 100644 --- a/typescript/gift-finder/index.ts +++ b/typescript/gift-finder/index.ts @@ -154,7 +154,10 @@ IMPORTANT: // Map AI scores back to products using index matching const scoredProducts = allProducts.map((product, index) => { - const scoreInfo = scoresData.find((s: any) => s.productIndex === index + 1); + const scoreInfo = scoresData.find( + (s: { productIndex: number; score: number; reason: string }) => + s.productIndex === index + 1, + ); return { ...product, aiScore: scoreInfo?.score || 0, @@ -186,7 +189,9 @@ async function getUserInput(): Promise { // Validate description length if (CONFIG.description.trim().length < 5) { - throw new Error("Description must be at least 5 characters long. Please update the CONFIG at the top of the file."); + throw new Error( + "Description must be at least 5 characters long. Please update the CONFIG at the top of the file.", + ); } return CONFIG; @@ -226,8 +231,8 @@ async function main(): Promise { const sessionStagehand = new Stagehand({ env: "BROWSERBASE", verbose: 1, - // 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.) + // 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: "openai/gpt-4.1", browserbaseSessionCreateParams: { @@ -294,7 +299,7 @@ async function main(): Promise { ) .max(3) .describe("array of the first 3 products from search results"), - }) + }), ); console.log( @@ -379,4 +384,4 @@ async function main(): Promise { main().catch((err) => { console.error("Application error:", err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/job-application/README.md b/typescript/job-application/README.md index 492a337..b25fa40 100644 --- a/typescript/job-application/README.md +++ b/typescript/job-application/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: Automated Job Application Agent ## AT A GLANCE + - Goal: Automate job applications by discovering job listings and submitting applications with unique agent identifiers. - Concurrent Processing: applies to multiple jobs in parallel with configurable concurrency limits based on Browserbase project settings. - Dynamic Data Generation: generates unique agent IDs and email addresses for each application. @@ -8,6 +9,7 @@ - Docs โ†’ https://docs.stagehand.dev/basics/agent ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent - act: perform UI actions from a prompt (click, type, fill forms) @@ -19,12 +21,14 @@ - semaphore: concurrency control mechanism to limit parallel job applications based on project limits ## QUICKSTART -1) npm install -2) cp .env.example .env -3) Add required API keys/IDs to .env (BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, GOOGLE_GENERATIVE_AI_API_KEY) -4) npm start + +1. npm install +2. cp .env.example .env +3. Add required API keys/IDs to .env (BROWSERBASE_API_KEY, BROWSERBASE_PROJECT_ID, GOOGLE_GENERATIVE_AI_API_KEY) +4. npm start ## EXPECTED OUTPUT + - Fetches project concurrency limit from Browserbase (maxed at 5) - Initializes main Stagehand session with Browserbase - Displays live session link for monitoring @@ -47,6 +51,7 @@ - Displays completion message when all applications are finished ## COMMON PITFALLS + - Dependency install errors: ensure npm install completed - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and GOOGLE_GENERATIVE_AI_API_KEY - Google API access: ensure you have access to Google's gemini-2.5-flash model @@ -57,12 +62,14 @@ - Find more information on your Browserbase dashboard -> https://www.browserbase.com/sign-in ## USE CASES + โ€ข Bulk job applications: Automate applying to multiple job postings simultaneously with unique credentials for each application. โ€ข Agent deployment automation: Streamline the process of deploying multiple AI agents by automating the application and registration workflow. โ€ข Testing & QA: Validate job application forms and workflows across multiple listings to ensure consistent functionality. โ€ข Recruitment automation: Scale agent recruitment processes by programmatically submitting applications with generated identifiers. ## NEXT STEPS + โ€ข Add filtering: Implement job filtering by title keywords, location, or other criteria before applying. โ€ข Error handling: Add retry logic for failed applications and better error reporting with job-specific logs. โ€ข Resume customization: Support multiple resume versions or dynamic resume generation based on job requirements. @@ -71,9 +78,10 @@ โ€ข Multi-site support: Extend to support multiple job boards with site-specific form field mappings. ## 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 +๐Ÿ“š 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/job-application/index.ts b/typescript/job-application/index.ts index b1a6f3c..dda95e5 100644 --- a/typescript/job-application/index.ts +++ b/typescript/job-application/index.ts @@ -75,7 +75,7 @@ async function applyToJob(jobInfo: JobInfo, semaphore: () => Promise, rele model: { modelName: "google/gemini-2.5-flash", apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, - } + }, }); try { @@ -84,7 +84,7 @@ async function applyToJob(jobInfo: JobInfo, semaphore: () => Promise, rele console.log(`[${jobInfo.title}] Session Started`); console.log( - `[${jobInfo.title}] Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}` + `[${jobInfo.title}] Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, ); const page = stagehand.context.pages()[0]; @@ -114,7 +114,7 @@ async function applyToJob(jobInfo: JobInfo, semaphore: () => Promise, rele // Upload agent profile/resume file // Using observe() to find the upload button, then setting files programmatically - const [ uploadAction ] = await stagehand.observe("find the file upload button for agent profile"); + const [uploadAction] = await stagehand.observe("find the file upload button for agent profile"); if (uploadAction) { const uploadSelector = uploadAction.selector; if (uploadSelector) { @@ -171,16 +171,14 @@ async function main() { model: { modelName: "google/gemini-2.5-flash", apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, - } + }, }); // Initialize browser session to start automation await stagehand.init(); console.log(`Main Stagehand Session Started`); - console.log( - `Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}` - ); + console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`); const page = stagehand.context.pages()[0]; @@ -196,7 +194,7 @@ async function main() { // Using extract() with Zod schema ensures consistent data extraction const jobsData = await stagehand.extract( "extract all job listings with their titles and URLs", - z.array(JobInfoSchema) + z.array(JobInfoSchema), ); console.log(`Found ${jobsData.length} jobs`); @@ -209,7 +207,9 @@ async function main() { // Apply to all jobs in parallel with concurrency control // Using Promise.all() to run all applications concurrently - console.log(`Starting to apply to ${jobsData.length} jobs with max concurrency of ${maxConcurrency}`); + console.log( + `Starting to apply to ${jobsData.length} jobs with max concurrency of ${maxConcurrency}`, + ); const applicationPromises = jobsData.map((job) => applyToJob(job, semaphore, release)); @@ -226,4 +226,4 @@ main().catch((err) => { console.error(" - Verify GOOGLE_GENERATIVE_AI_API_KEY is set"); console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/license-verification/README.md b/typescript/license-verification/README.md index 474273d..a4340f9 100644 --- a/typescript/license-verification/README.md +++ b/typescript/license-verification/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Data Extraction with Structured Schemas ## AT A GLANCE + - Goal: show how to extract structured, validated data from websites using Stagehand + Zod. - Data Extraction: automate navigation, form submission, and structured scraping in one flow. - Schema Validation: enforce type safety and consistency with Zod schemas. - Practical Example: verify California real estate license details with a typed output object. ## GLOSSARY + - act: perform UI actions from a prompt (type, click, navigate). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from web pages into validated objects. @@ -17,37 +19,44 @@ - structured scraping: extracting consistent, typed data that can flow into apps, CRMs, or compliance systems. ## QUICKSTART - 1) cd license-verification-template - 2) npm install - 3) cp .env.example .env - 4) Add your Browserbase API key and Project ID to .env - 5) npm start + +1. cd license-verification-template +2. npm install +3. cp .env.example .env +4. Add your Browserbase API key and Project ID to .env +5. npm start ## EXPECTED OUTPUT + - Navigates to California DRE license verification website - Fills in license ID and submits form - Extracts structured license data using Zod schema - Returns typed object with license verification details ## COMMON PITFALLS + - "Cannot find module 'dotenv'": ensure pnpm install ran successfully - Missing API key: verify .env is loaded and file is not committed - Schema validation errors: ensure extracted data matches Zod schema structure - Form submission failures: check if website structure has changed ## USE CASES + โ€ข License & credential verification: Extract and validate professional license data from regulatory portals. โ€ข Compliance automation: Monitor status changes (active, expired, disciplinary) for risk and regulatory workflows. โ€ข Structured research: Collect validated datasets from government or industry registries for BI or due diligence. ## NEXT STEPS + โ€ข Expand schema coverage: Add more fields (disciplinary actions, broker info, historical data) for richer records. โ€ข Scale across sources: Point the same flow at other jurisdictions, databases, or professional directories. โ€ข Persist & integrate: Store structured results in a database or push directly into CRM/compliance systems. ## 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 + +๐Ÿ“š 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/license-verification/index.ts b/typescript/license-verification/index.ts index 9778a9f..8075824 100644 --- a/typescript/license-verification/index.ts +++ b/typescript/license-verification/index.ts @@ -6,7 +6,7 @@ import { z } from "zod"; // License verification variables const variables = { - input1: "02237476" // DRE License ID to search for + input1: "02237476", // DRE License ID to search for }; async function main() { @@ -15,38 +15,34 @@ async function main() { env: "BROWSERBASE", // Use Browserbase cloud browsers for reliable automation. verbose: 1, model: "openai/gpt-4.1", - // 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.) + // 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 }); // Initialize browser session to start data extraction process. await stagehand.init(); console.log(`Stagehand Session Started`); - + // Provide live session URL for debugging and monitoring extraction process. console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`); const page = stagehand.context.pages()[0]; // Navigate to California DRE license verification website for data extraction. - console.log('Navigating to: https://www2.dre.ca.gov/publicasp/pplinfo.asp'); - await page.goto('https://www2.dre.ca.gov/publicasp/pplinfo.asp'); - + console.log("Navigating to: https://www2.dre.ca.gov/publicasp/pplinfo.asp"); + await page.goto("https://www2.dre.ca.gov/publicasp/pplinfo.asp"); + // Fill in license ID to search for specific real estate professional. - console.log( - `Performing action: type ${variables.input1} into the License ID input field`, - ); - await stagehand.act(`type ${variables.input1} into the License ID input field`); - + console.log(`Performing action: type ${variables.input1} into the License ID input field`); + await stagehand.act(`type ${variables.input1} into the License ID input field`); + // Submit search form to retrieve license verification data. console.log(`Performing action: click the Find button`); - await stagehand.act(`click the Find button`); - + await stagehand.act(`click the Find button`); + // Extract structured license data using Zod schema for type safety and validation. - console.log( - `Extracting: extract all the license verification details for DRE#02237476`, - ); + console.log(`Extracting: extract all the license verification details for DRE#02237476`); const extractedData4 = await stagehand.extract( `extract all the license verification details for DRE#02237476`, z.object({ @@ -63,9 +59,9 @@ async function main() { brokerAddress: z.string().optional(), // Broker's business address disciplinaryAction: z.string().optional(), // Any disciplinary actions taken otherComments: z.string().optional(), // Additional relevant information - }) + }), ); - console.log('Extracted:', extractedData4); + console.log("Extracted:", extractedData4); // Always close session to release resources and clean up. await stagehand.close(); diff --git a/typescript/microsoft-cua/README.md b/typescript/microsoft-cua/README.md index 7f11654..c0c4f05 100644 --- a/typescript/microsoft-cua/README.md +++ b/typescript/microsoft-cua/README.md @@ -1,21 +1,25 @@ # Stagehand + Browserbase: Computer Use Agent (CUA) Example ## AT A GLANCE + - Goal: demonstrate autonomous web browsing using Microsoft's Computer Use Agent with Stagehand and Browserbase. - Uses Stagehand Agent to automate complex workflows with AI powered browser agents - Leverages Microsoft's fara-7b model for autonomous web interaction and decision-making. ## GLOSSARY + - agent: create an autonomous AI agent that can execute complex multi-step tasks Docs โ†’ https://docs.stagehand.dev/basics/agent#what-is-agent ## QUICKSTART - 1) npm install - 2) cp .env.example .env - 3) Add your Browserbase API key, Project ID, Azure API key, and Azure endpoint to .env - 4) npm start + +1. npm install +2. cp .env.example .env +3. Add your Browserbase API key, Project ID, Azure API key, and Azure endpoint to .env +4. npm start ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Navigates to Google search engine - Executes autonomous search and data extraction task @@ -24,24 +28,28 @@ - Closes session cleanly ## COMMON PITFALLS + - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, AZURE_API_KEY, and AZURE_ENDPOINT - Microsoft API access: ensure you have access to Microsoft's fara-7b model via Azure or Fireworks ## USE CASES + โ€ข Autonomous research: Let AI agents independently research topics, gather information, and compile reports without manual intervention. โ€ข Complex web workflows: Automate multi-step processes that require decision-making, form filling, and data extraction across multiple pages. โ€ข Content discovery: Search for specific information, verify data accuracy, and cross-reference sources autonomously. ## NEXT STEPS + โ€ข Customize instructions: Modify the instruction variable to test different autonomous tasks and scenarios. โ€ข Add error handling: Implement retry logic, fallback strategies, and better error recovery for failed agent actions. โ€ข Extend capabilities: Add support for file downloads, form submissions, and more complex interaction patterns. ## 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 +๐Ÿ“š 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/microsoft-cua/index.ts b/typescript/microsoft-cua/index.ts index 5dfe9ec..01ad794 100644 --- a/typescript/microsoft-cua/index.ts +++ b/typescript/microsoft-cua/index.ts @@ -21,13 +21,12 @@ const instruction = `Search for the next visible solar eclipse in North America // ============================================================================ async function main() { - const stagehand = new Stagehand({ env: "BROWSERBASE", // model: "google/gemini-2.5-pro", // this is the model stagehand uses in act, observe, extract (not agent) verbose: 1, - // 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.) + // 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 browserbaseSessionCreateParams: { projectId: process.env.BROWSERBASE_PROJECT_ID!, @@ -37,8 +36,8 @@ async function main() { blockAds: true, viewport: { width: 1288, - height: 711 - } + height: 711, + }, }, }, }); @@ -47,13 +46,15 @@ async function main() { // Initialize browser session to start automation. await stagehand.init(); console.log("Stagehand initialized successfully!"); - console.log(`Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`); + console.log( + `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, + ); const page = stagehand.context.pages()[0]; // Navigate to search engine with extended timeout for slow-loading sites. await page.goto("https://www.google.com/", { - waitUntil: 'domcontentloaded', + waitUntil: "domcontentloaded", }); // Create agent with computer use capabilities for autonomous web browsing. @@ -71,14 +72,14 @@ async function main() { }, systemPrompt: `You are a helpful assistant that can use a web browser. You are currently on the following page: ${page.url()}. - Do not ask follow up questions, the user will trust your judgement. If you are getting blocked on google, try another search engine.` + Do not ask follow up questions, the user will trust your judgement. If you are getting blocked on google, try another search engine.`, }); console.log("Executing instruction:", instruction); const result = await agent.execute({ instruction: instruction, maxSteps: 30, - highlightCursor: true + highlightCursor: true, }); if (result.success === true) { @@ -87,7 +88,6 @@ async function main() { } else { console.log("Task failed or was incomplete"); } - } catch (error) { console.error("Error executing computer use agent:", error); } finally { @@ -103,4 +103,4 @@ main().catch((err) => { console.error(" - Verify AZURE_API_KEY is set for the agent"); console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/nurse-verification/README.md b/typescript/nurse-verification/README.md index 746bd2f..270f06c 100644 --- a/typescript/nurse-verification/README.md +++ b/typescript/nurse-verification/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Nurse License Verification ## AT A GLANCE + - Goal: automate verification of nurse licenses by filling forms and extracting structured results from verification sites. - Flow: loop through license records โ†’ navigate to verification site โ†’ fill form โ†’ search โ†’ extract verification results. - Benefits: quickly verify multiple licenses without manual form filling, structured data ready for compliance tracking or HR systems. Docs โ†’ https://docs.stagehand.dev/basics/act ## GLOSSARY + - act: perform UI actions from a prompt (type, click, fill forms). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from a page using AI and Zod schemas. @@ -16,13 +18,15 @@ - license verification: process of confirming the validity and status of professional licenses. ## QUICKSTART -1) cd nurse-verification -2) npm install -3) cp .env.example .env -4) Add your Browserbase API key, Project ID, and OpenAI API key to .env -5) npm start + +1. cd nurse-verification +2. npm install +3. cp .env.example .env +4. Add your Browserbase API key, Project ID, and OpenAI API key to .env +5. npm start ## EXPECTED OUTPUT + - Initializes Stagehand session with Browserbase - Loops through license records in LicenseRecords array - For each record: navigates to verification site, fills form, searches @@ -32,6 +36,7 @@ - Closes session cleanly ## COMMON PITFALLS + - "Cannot find module 'dotenv'": ensure npm install ran successfully - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID, BROWSERBASE_API_KEY, and OPENAI_API_KEY - No results found: check if license numbers are valid or if verification site structure has changed @@ -39,17 +44,20 @@ - Schema validation errors: ensure extracted data matches Zod schema structure ## USE CASES + โ€ข HR compliance: Automate license verification for healthcare staff onboarding and annual reviews. โ€ข Healthcare staffing: Verify credentials of temporary or contract nurses before assignment. โ€ข Regulatory reporting: Collect license status data for compliance reporting and audits. ## NEXT STEPS + โ€ข Multi-site support: Add support for different license verification sites and adapt form filling logic. โ€ข Batch processing: Load license records from CSV/Excel files for large-scale verification. โ€ข Status monitoring: Set up scheduled runs to track license status changes and expiration dates. ## HELPFUL RESOURCES -๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction -๐ŸŽฎ Browserbase: https://www.browserbase.com -๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +๐ŸŽฎ Browserbase: https://www.browserbase.com +๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ’ฌ Discord: http://stagehand.dev/discord diff --git a/typescript/nurse-verification/index.ts b/typescript/nurse-verification/index.ts index f5392b1..a36126e 100644 --- a/typescript/nurse-verification/index.ts +++ b/typescript/nurse-verification/index.ts @@ -34,33 +34,24 @@ async function main() { console.log("Stagehand session started successfully"); // Provide live session URL for debugging and monitoring - console.log( - `Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`, - ); + console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`); const page = stagehand.context.pages()[0]; // Process each license record sequentially for (const LicenseRecord of LicenseRecords) { - console.log( - `Verifying license for: ${LicenseRecord.FirstName} ${LicenseRecord.LastName}`, - ); + console.log(`Verifying license for: ${LicenseRecord.FirstName} ${LicenseRecord.LastName}`); // Navigate to license verification site console.log(`Navigating to: ${LicenseRecord.Site}`); await page.goto(LicenseRecord.Site); await page.waitForLoadState("domcontentloaded"); - // Fill in form fields with license information console.log("Filling in license information..."); - await stagehand.act( - `Type "${LicenseRecord.FirstName}" into the first name field`, - ); + await stagehand.act(`Type "${LicenseRecord.FirstName}" into the first name field`); await stagehand.act(`Type "${LicenseRecord.LastName}" into the last name field`); - await stagehand.act( - `Type "${LicenseRecord.LicenseNumber}" into the license number field`, - ); + await stagehand.act(`Type "${LicenseRecord.LicenseNumber}" into the license number field`); // Submit search console.log("Clicking search button..."); @@ -110,4 +101,4 @@ async function main() { main().catch((err) => { console.error("Application error:", err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/pickleball/README.md b/typescript/pickleball/README.md index 4e49b18..34bbbac 100644 --- a/typescript/pickleball/README.md +++ b/typescript/pickleball/README.md @@ -1,6 +1,7 @@ # Stagehand + Browserbase: AI-Powered Court Booking Automation ## AT A GLANCE + - Goal: automate tennis and pickleball court bookings in San Francisco Recreation & Parks system. - AI Integration: Stagehand for UI interaction and data extraction. - Browser Automation: automates login, filtering, court selection, and booking confirmation. @@ -8,6 +9,7 @@ Docs โ†’ https://docs.browserbase.com/features/sessions ## GLOSSARY + - act: perform UI actions from a prompt (click, type, select) Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from pages using schemas @@ -19,15 +21,17 @@ - form validation: ensure user input meets booking system requirements ## QUICKSTART -1) Create an account with SF Recreation & Parks website -> https://www.rec.us/organizations/san-francisco-rec-park -2) cd pickleball-template -3) npm install -4) npm install inquirer -5) cp .env.example .env -6) Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env -7) npm start + +1. Create an account with SF Recreation & Parks website -> https://www.rec.us/organizations/san-francisco-rec-park +2. cd pickleball-template +3. npm install +4. npm install inquirer +5. cp .env.example .env +6. Add your Browserbase API key, Project ID, and SF Rec Park credentials to .env +7. npm start ## EXPECTED OUTPUT + - Prompts user for activity type (Tennis/Pickleball), date, and time - Automates login to SF Recreation & Parks booking system - Filters courts by activity, date, and time preferences @@ -36,6 +40,7 @@ - Confirms successful booking with details ## COMMON PITFALLS + - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains all required API keys and SF Rec Park login - Login failures: check SF Rec Park credentials and account status @@ -43,6 +48,7 @@ - Verification codes: ensure you can receive SMS/email codes for booking confirmation ## FURTHER USE CASES + โ€ข Court Booking: Automate tennis and pickleball court reservations in San Francisco โ€ข Recreation & ticketing: courts, parks, events, museum passes, campsite reservations โ€ข Appointments & scheduling: DMV, healthcare visits, test centers, field service dispatch @@ -53,6 +59,7 @@ โ€ข Internal admin portals: hardware checkout, conference-room overflow, cafeteria or shift scheduling ## NEXT STEPS + โ€ข Swap the target site: point the script at a different booking or reservation portal (e.g., gyms, coworking, campsites) โ€ข Generalize filters: extend date/time/activity prompts to handle more categories or custom filters โ€ข Automate recurring bookings: wrap the script in a scheduler (cron/queue) to secure slots automatically @@ -65,8 +72,10 @@ โ€ข Template it: strip out "pickleball" wording and reuse as a boilerplate for any authenticate โ†’ filter โ†’ extract โ†’ book workflow ## 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 + +๐Ÿ“š 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/pickleball/index.ts b/typescript/pickleball/index.ts index 98c7693..e513eca 100644 --- a/typescript/pickleball/index.ts +++ b/typescript/pickleball/index.ts @@ -1,8 +1,8 @@ // SF Court Booking Automation - See README.md for full documentation import "dotenv/config"; -import { Stagehand, StagehandPage } from "@browserbasehq/stagehand"; -import inquirer from 'inquirer'; -import { z } from 'zod'; +import { Stagehand } from "@browserbasehq/stagehand"; +import inquirer from "inquirer"; +import { z } from "zod"; async function loginToSite(stagehand: Stagehand, email: string, password: string): Promise { console.log("Logging in..."); @@ -15,38 +15,43 @@ async function loginToSite(stagehand: Stagehand, email: string, password: string console.log("Logged in"); } -async function selectFilters(stagehand: Stagehand, activity: string, timeOfDay: string, selectedDate: string): Promise { +async function selectFilters( + stagehand: Stagehand, + activity: string, + timeOfDay: string, + selectedDate: string, +): Promise { console.log("Selecting the activity"); // Filter by activity type first to narrow down available courts. await stagehand.act(`Click the activites drop down menu`); await stagehand.act(`Select the ${activity} activity`); await stagehand.act(`Click the Done button`); - + console.log(`Selecting date: ${selectedDate}`); // Open calendar to select specific date for court booking. await stagehand.act(`Click the date picker or calendar`); - + // Parse date string to extract day number for calendar selection. - const dateParts = selectedDate.split('-'); + const dateParts = selectedDate.split("-"); if (dateParts.length !== 3) { throw new Error(`Invalid date format: ${selectedDate}. Expected YYYY-MM-DD`); } - + const dayNumber = parseInt(dateParts[2], 10); if (isNaN(dayNumber) || dayNumber < 1 || dayNumber > 31) { throw new Error(`Invalid day number: ${dayNumber} from date: ${selectedDate}`); } - + console.log(`Looking for day number: ${dayNumber} in calendar`); // Click specific day number in calendar to select date. await stagehand.act(`Click on the number ${dayNumber} in the calendar`); - + console.log(`Selecting time of day: ${timeOfDay}`); // Filter by time period to find courts available during preferred hours. await stagehand.act(`Click the time filter or time selection dropdown`); await stagehand.act(`Select ${timeOfDay} time period`); await stagehand.act(`Click the Done button`); - + // Apply additional filters to show only available courts that accept reservations. await stagehand.act(`Click Available Only button`); await stagehand.act(`Click All Facilities dropdown list`); @@ -56,74 +61,92 @@ async function selectFilters(stagehand: Stagehand, activity: string, timeOfDay: async function checkAndExtractCourts(stagehand: Stagehand, timeOfDay: string): Promise { console.log("Checking for available courts..."); - + // First observe the page to find all available court booking options. - const availableCourts = await stagehand.observe("Find all available court booking slots, time slots, or court reservation options"); - console.log(`Found ${availableCourts.length} available court options`); - + const availableCourts = await stagehand.observe( + "Find all available court booking slots, time slots, or court reservation options", + ); + console.log(`Found ${availableCourts.length} available court options`); + // Extract structured court data using Zod schema for type safety and validation. const courtData = await stagehand.extract( "Extract all available court booking information including court names, time slots, locations, and any other relevant details", z.object({ - courts: z.array(z.object({ - name: z.string().describe("the name or identifier of the court"), - openingTimes: z.string().describe("the opening hours or operating times of the court"), - location: z.string().describe("the location or facility name"), - availability: z.string().describe("availability status or any restrictions"), - duration: z.string().nullable().describe("the duration of the court session in minutes") - })) - }) + courts: z.array( + z.object({ + name: z.string().describe("the name or identifier of the court"), + openingTimes: z.string().describe("the opening hours or operating times of the court"), + location: z.string().describe("the location or facility name"), + availability: z.string().describe("availability status or any restrictions"), + duration: z.string().nullable().describe("the duration of the court session in minutes"), + }), + ), + }), ); - + // Check if any courts are actually available by filtering out unavailable status messages. - let hasAvailableCourts = courtData.courts.some((court: any) => - !court.availability.toLowerCase().includes('no free spots') && - !court.availability.toLowerCase().includes('unavailable') && - !court.availability.toLowerCase().includes('next available') && - !court.availability.toLowerCase().includes('the next available reservation') + let hasAvailableCourts = courtData.courts.some( + (court: { availability: string }) => + !court.availability.toLowerCase().includes("no free spots") && + !court.availability.toLowerCase().includes("unavailable") && + !court.availability.toLowerCase().includes("next available") && + !court.availability.toLowerCase().includes("the next available reservation"), ); - + // If no courts available for selected time, try alternative time periods as fallback. if (availableCourts.length === 0 || !hasAvailableCourts) { console.log("No courts available for selected time. Trying different time periods..."); - + // Generate alternative time periods to try if original selection has no availability. - const alternativeTimes = timeOfDay === 'Morning' ? ['Afternoon', 'Evening'] : - timeOfDay === 'Afternoon' ? ['Morning', 'Evening'] : - ['Morning', 'Afternoon']; - + const alternativeTimes = + timeOfDay === "Morning" + ? ["Afternoon", "Evening"] + : timeOfDay === "Afternoon" + ? ["Morning", "Evening"] + : ["Morning", "Afternoon"]; + for (const altTime of alternativeTimes) { console.log(`Trying ${altTime} time period...`); - + // Change time filter to alternative time period and check availability. await stagehand.act(`Click the time filter dropdown that currently shows "${timeOfDay}"`); await stagehand.act(`Select ${altTime} from the time period options`); await stagehand.act(`Click the Done button`); - - const altAvailableCourts = await stagehand.observe("Find all available court booking slots, time slots, or court reservation options"); + + const altAvailableCourts = await stagehand.observe( + "Find all available court booking slots, time slots, or court reservation options", + ); console.log(`Found ${altAvailableCourts.length} available court options for ${altTime}`); - + if (altAvailableCourts.length > 0) { const altCourtData = await stagehand.extract( "Extract all available court booking information including court names, time slots, locations, and any other relevant details", z.object({ - courts: z.array(z.object({ - name: z.string().describe("the name or identifier of the court"), - openingTimes: z.string().describe("the opening hours or operating times of the court"), - location: z.string().describe("the location or facility name"), - availability: z.string().describe("availability status or any restrictions"), - duration: z.string().nullable().describe("the duration of the court session in minutes") - })) - }) + courts: z.array( + z.object({ + name: z.string().describe("the name or identifier of the court"), + openingTimes: z + .string() + .describe("the opening hours or operating times of the court"), + location: z.string().describe("the location or facility name"), + availability: z.string().describe("availability status or any restrictions"), + duration: z + .string() + .nullable() + .describe("the duration of the court session in minutes"), + }), + ), + }), ); - - const hasAltAvailableCourts = altCourtData.courts.some((court: any) => - !court.availability.toLowerCase().includes('no free spots') && - !court.availability.toLowerCase().includes('unavailable') && - !court.availability.toLowerCase().includes('next available') && - !court.availability.toLowerCase().includes('the next available reservation') + + const hasAltAvailableCourts = altCourtData.courts.some( + (court: { availability: string }) => + !court.availability.toLowerCase().includes("no free spots") && + !court.availability.toLowerCase().includes("unavailable") && + !court.availability.toLowerCase().includes("next available") && + !court.availability.toLowerCase().includes("the next available reservation"), ); - + // If alternative time has available courts, use that data and stop searching. if (hasAltAvailableCourts) { console.log(`Found actually available courts for ${altTime}!`); @@ -134,29 +157,44 @@ async function checkAndExtractCourts(stagehand: Stagehand, timeOfDay: string): P } } } - + // If still no available courts found, extract final court data for display. if (!hasAvailableCourts) { console.log("Extracting final court information..."); const finalCourtData = await stagehand.extract( "Extract all available court booking information including court names, time slots, locations, and any other relevant details", z.object({ - courts: z.array(z.object({ - name: z.string().describe("the name or identifier of the court"), - openingTimes: z.string().describe("the opening hours or operating times of the court"), - location: z.string().describe("the location or facility name"), - availability: z.string().describe("availability status or any restrictions"), - duration: z.string().nullable().describe("the duration of the court session in minutes") - })) - }) + courts: z.array( + z.object({ + name: z.string().describe("the name or identifier of the court"), + openingTimes: z.string().describe("the opening hours or operating times of the court"), + location: z.string().describe("the location or facility name"), + availability: z.string().describe("availability status or any restrictions"), + duration: z + .string() + .nullable() + .describe("the duration of the court session in minutes"), + }), + ), + }), ); courtData.courts = finalCourtData.courts; } - + // Display all found court information to user for review and selection. console.log("Available Courts:"); if (courtData.courts && courtData.courts.length > 0) { - courtData.courts.forEach((court: any, index: number) => { + courtData.courts.forEach( + ( + court: { + name: string; + openingTimes: string; + location: string; + availability: string; + duration: string | null; + }, + index: number, + ) => { console.log(`${index + 1}. ${court.name}`); console.log(` Opening Times: ${court.openingTimes}`); console.log(` Location: ${court.location}`); @@ -173,43 +211,45 @@ async function checkAndExtractCourts(stagehand: Stagehand, timeOfDay: string): P async function bookCourt(stagehand: Stagehand): Promise { console.log("Starting court booking process..."); - + try { // Select the first available court time slot for booking. console.log("Clicking the top available time slot..."); await stagehand.act("Click the first available time slot or court booking option"); - + // Select participant from dropdown - assumes only one participant is available. console.log("Opening participant dropdown..."); await stagehand.act("Click the participant dropdown menu or select participant field"); await stagehand.act("Click the only named participant in the dropdown!"); - + // Complete booking process and trigger verification code request. console.log("Clicking the book button to complete reservation..."); await stagehand.act("Click the book, reserve, or confirm booking button"); await stagehand.act("Click the Send Code Button"); - + // Prompt user for verification code received via SMS/email for booking confirmation. const codeAnswer = await inquirer.prompt([ { - type: 'input', - name: 'verificationCode', - message: 'Please enter the verification code you received:', + type: "input", + name: "verificationCode", + message: "Please enter the verification code you received:", validate: (input: string) => { if (!input.trim()) { - return 'Please enter a verification code'; + return "Please enter a verification code"; } return true; - } - } + }, + }, ]); - + console.log(`Verification code: ${codeAnswer.verificationCode}`); - + // Enter verification code and confirm booking to complete reservation. - await stagehand.act(`Fill in the verification code field with "${codeAnswer.verificationCode}"`); + await stagehand.act( + `Fill in the verification code field with "${codeAnswer.verificationCode}"`, + ); await stagehand.act("Click the confirm button"); - + // Extract booking confirmation details to verify successful reservation. console.log("Checking for booking confirmation..."); const confirmation = await stagehand.extract( @@ -217,10 +257,10 @@ async function bookCourt(stagehand: Stagehand): Promise { z.object({ confirmationMessage: z.string().nullable().describe("any confirmation or success message"), bookingDetails: z.string().nullable().describe("booking details like time, court, etc."), - errorMessage: z.string().nullable().describe("any error message if booking failed") - }) + errorMessage: z.string().nullable().describe("any error message if booking failed"), + }), ); - + // Display confirmation details if booking was successful. if (confirmation.confirmationMessage || confirmation.bookingDetails) { console.log("Booking Confirmed!"); @@ -231,13 +271,12 @@ async function bookCourt(stagehand: Stagehand): Promise { console.log(`${confirmation.bookingDetails}`); } } - + // Display error message if booking failed. if (confirmation.errorMessage) { console.log("Booking Error:"); console.log(confirmation.errorMessage); } - } catch (error) { console.error("Error during court booking:", error); throw error; @@ -248,15 +287,15 @@ async function selectActivity(): Promise { // Prompt user to select between Tennis and Pickleball activities. const answers = await inquirer.prompt([ { - type: 'list', - name: 'activity', - message: 'Please select an activity:', + type: "list", + name: "activity", + message: "Please select an activity:", choices: [ - { name: 'Tennis', value: 'Tennis' }, - { name: 'Pickleball', value: 'Pickleball' } + { name: "Tennis", value: "Tennis" }, + { name: "Pickleball", value: "Pickleball" }, ], - default: 0 - } + default: 0, + }, ]); console.log(`Selected: ${answers.activity}`); @@ -267,16 +306,16 @@ async function selectTimeOfDay(): Promise { // Prompt user to select preferred time period for court booking. const answers = await inquirer.prompt([ { - type: 'list', - name: 'timeOfDay', - message: 'Please select the time of day:', + type: "list", + name: "timeOfDay", + message: "Please select the time of day:", choices: [ - { name: 'Morning (Before 12 PM)', value: 'Morning' }, - { name: 'Afternoon (After 12 PM)', value: 'Afternoon' }, - { name: 'Evening (After 5 PM)', value: 'Evening' } + { name: "Morning (Before 12 PM)", value: "Morning" }, + { name: "Afternoon (After 12 PM)", value: "Afternoon" }, + { name: "Evening (After 5 PM)", value: "Evening" }, ], - default: 0 - } + default: 0, + }, ]); console.log(`Selected: ${answers.timeOfDay}`); @@ -287,43 +326,46 @@ async function selectDate(): Promise { // Generate date options for the next 7 days including today. const today = new Date(); const dateOptions: { name: string; value: string }[] = []; - + for (let i = 0; i < 7; i++) { const date = new Date(today); date.setDate(today.getDate() + i); - - const dayName = date.toLocaleDateString('en-US', { weekday: 'long' }); - const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - const fullDate = date.toISOString().split('T')[0]; - + + const dayName = date.toLocaleDateString("en-US", { weekday: "long" }); + const monthDay = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const fullDate = date.toISOString().split("T")[0]; + const displayName = i === 0 ? `${dayName}, ${monthDay} (Today)` : `${dayName}, ${monthDay}`; - + dateOptions.push({ name: displayName, - value: fullDate + value: fullDate, }); } // Prompt user to select from available date options. const answers = await inquirer.prompt([ { - type: 'list', - name: 'selectedDate', - message: 'Please select a date:', + type: "list", + name: "selectedDate", + message: "Please select a date:", choices: dateOptions, - default: 0 - } + default: 0, + }, ]); // Format selected date for display and return ISO date string. const selectedDate = new Date(answers.selectedDate); - const displayDate = selectedDate.toLocaleDateString('en-US', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' + const displayDate = selectedDate.toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", }); - + console.log(`Selected: ${displayDate}`); return answers.selectedDate; } @@ -334,13 +376,13 @@ async function bookTennisPaddleCourt() { // Load credentials from environment variables for SF Rec & Parks login. const email = process.env.SF_REC_PARK_EMAIL; const password = process.env.SF_REC_PARK_PASSWORD; - const debugMode = process.env.DEBUG === "true"; - + const _debugMode = process.env.DEBUG === "true"; + // Collect user preferences for activity, date, and time selection. const activity = await selectActivity(); const selectedDate = await selectDate(); const timeOfDay = await selectTimeOfDay(); - + console.log(`Booking ${activity} courts in San Francisco for ${timeOfDay} on ${selectedDate}...`); // Validate that required credentials are available before proceeding. @@ -353,21 +395,21 @@ async function bookTennisPaddleCourt() { const stagehand = new Stagehand({ env: "BROWSERBASE", verbose: 1, - // 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.) + // 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: "openai/gpt-4.1", browserbaseSessionCreateParams: { projectId: process.env.BROWSERBASE_PROJECT_ID!, timeout: 900, - region: "us-west-2" - } + region: "us-west-2", + }, }); try { // Start browser session and connect to SF Rec & Parks booking system. await stagehand.init(); - + console.log("Browserbase Session Started"); console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`); @@ -376,8 +418,8 @@ async function bookTennisPaddleCourt() { // Navigate to SF Rec & Parks booking site with extended timeout for slow loading. console.log("Navigating to court booking site..."); await page.goto("https://www.rec.us/organizations/san-francisco-rec-park", { - waitUntil: 'domcontentloaded', - timeout: 60000 + waitUntil: "domcontentloaded", + timeout: 60000, }); // Execute booking workflow: login, filter, find courts, and complete booking. @@ -385,7 +427,6 @@ async function bookTennisPaddleCourt() { await selectFilters(stagehand, activity, timeOfDay, selectedDate); await checkAndExtractCourts(stagehand, timeOfDay); await bookCourt(stagehand); - } catch (error) { console.error("Error during court booking:", error); throw error; @@ -413,7 +454,7 @@ async function main() { try { // Execute the complete court booking automation workflow. await bookTennisPaddleCourt(); - + console.log("Court booking completed successfully!"); console.log("Your court has been reserved. Check your email for confirmation details."); } catch (error) { @@ -428,4 +469,3 @@ main().catch((err) => { console.log("Check your environment variables"); process.exit(1); }); - diff --git a/typescript/polymarket-research/README.md b/typescript/polymarket-research/README.md index 4e1122c..aa5ccd8 100644 --- a/typescript/polymarket-research/README.md +++ b/typescript/polymarket-research/README.md @@ -1,12 +1,14 @@ # Stagehand + Browserbase: Market Research Automation ## AT A GLANCE + - Goal: demonstrate how to automate market research on prediction markets using Stagehand. - Navigation & Search: automate website navigation, search interactions, and result selection. - Data Extraction: extract structured market data with validated output using Zod schemas. - Practical Example: research and extract current odds from Polymarket prediction markets. ## GLOSSARY + - act: perform UI actions from a natural language prompt (type, click, navigate). Docs โ†’ https://docs.stagehand.dev/basics/act - extract: pull structured data from web pages into validated objects. @@ -17,13 +19,15 @@ - structured data extraction: convert unstructured web content into typed, validated objects. ## QUICKSTART - 1) cd polymarket-research - 2) npm install - 3) cp ../../.env.example .env (or create .env with BROWSERBASE_API_KEY) - 4) Add your Browserbase API key to .env - 5) npm start + +1. cd polymarket-research +2. npm install +3. cp ../../.env.example .env (or create .env with BROWSERBASE_API_KEY) +4. Add your Browserbase API key to .env +5. npm start ## EXPECTED OUTPUT + - Navigates to Polymarket prediction market website - Searches for specified market query - Selects the first search result @@ -31,25 +35,29 @@ - Returns typed object with market information ## COMMON PITFALLS + - "Cannot find module 'dotenv'": ensure npm install ran successfully - Missing API key: verify .env is loaded and file is not committed - Search results not found: check if the market exists or if website structure has changed - Schema validation errors: ensure extracted data matches Zod schema structure ## USE CASES + โ€ข Market tracking: automate monitoring of prediction market odds for specific events or topics. โ€ข Research aggregation: collect current prices and volume data from multiple prediction markets. โ€ข Trading automation: extract structured market data for integration with trading or analysis systems. โ€ข Sentiment analysis: track how prediction markets assess the likelihood of future events. ## NEXT STEPS + โ€ข Parameterize search queries: make the search term configurable via environment variables or prompts. โ€ข Multi-market extraction: extend the flow to search and extract data from multiple markets in parallel. โ€ข Historical tracking: persist extracted data over time to track market movement and trends. โ€ข Price alerts: add logic to monitor specific price thresholds and send notifications. ## HELPFUL RESOURCES -๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction -๐ŸŽฎ Browserbase: https://www.browserbase.com -๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ“š Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +๐ŸŽฎ Browserbase: https://www.browserbase.com +๐Ÿ“ง Need help? support@browserbase.com +๐Ÿ’ฌ Discord: http://stagehand.dev/discord diff --git a/typescript/polymarket-research/index.ts b/typescript/polymarket-research/index.ts index b57b4b3..aafee64 100644 --- a/typescript/polymarket-research/index.ts +++ b/typescript/polymarket-research/index.ts @@ -16,8 +16,8 @@ async function main() { const stagehand = new Stagehand({ env: "BROWSERBASE", verbose: 1, - // 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.) + // 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: "openai/gpt-4.1", }); @@ -70,14 +70,14 @@ async function main() { console.log(JSON.stringify(marketData, null, 2)); } catch (error) { console.error("Error during market research:", error); - + // Provide helpful troubleshooting information console.error("\nCommon issues:"); console.error("1. Check .env file has BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY"); console.error("2. Verify OPENAI_API_KEY is set in environment"); console.error("3. Ensure internet access and https://polymarket.com is accessible"); console.error("4. Verify Browserbase account has sufficient credits"); - + throw error; } finally { // Clean up browser session @@ -90,4 +90,4 @@ async function main() { main().catch((err) => { console.error("Application error:", err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/typescript/proxies/README.md b/typescript/proxies/README.md index a77e08a..9b8d737 100644 --- a/typescript/proxies/README.md +++ b/typescript/proxies/README.md @@ -1,21 +1,25 @@ # Browserbase Proxy Testing Script ## AT A GLANCE + - Goal: demonstrate different proxy configurations with Browserbase sessions. ## GLOSSARY + - Proxies: Browserbase's default proxy rotation for enhanced privacy Docs โ†’ https://docs.browserbase.com/features/proxies ## QUICKSTART - 1) cd proxies-template - 2) npm install - 3) npm install @browserbasehq/sdk playwright-core - 4) cp .env.example .env - 5) Add your Browserbase API key and Project ID to .env - 6) npm start + +1. cd proxies-template +2. npm install +3. npm install @browserbasehq/sdk playwright-core +4. cp .env.example .env +5. Add your Browserbase API key and Project ID to .env +6. npm start ## EXPECTED OUTPUT + - Tests built-in proxy rotation - Tests geolocation-specific proxies (New York) - Tests custom external proxies (commented out by default) @@ -23,24 +27,29 @@ - Shows how different proxy configurations affect your apparent location ## COMMON PITFALLS + - Browserbase Developer plan or higher is required to use proxies - "Cannot find module": ensure all dependencies are installed - Missing credentials: verify .env contains BROWSERBASE_PROJECT_ID and BROWSERBASE_API_KEY - Custom proxy errors: verify external proxy server credentials and availability ## USE CASES + โ€ข Geo-testing: Verify location-specific content, pricing, or compliance banners. โ€ข Scraping at scale: Rotate IPs to reduce blocks and increase CAPTCHA success rates. โ€ข Custom routing: Mix built-in and external proxies, or apply domain-based rules for compliance. ## NEXT STEPS + โ€ข Add routing rules: Configure domainPattern to direct specific sites through targeted proxies. โ€ข Test multiple geos: Compare responses from different cities/countries and log differences. โ€ข Improve reliability: Add retries and fallbacks to handle proxy errors like ERR_TUNNEL_CONNECTION_FAILED. ## 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 \ No newline at end of file + +๐Ÿ“š 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/proxies/index.ts b/typescript/proxies/index.ts index e0dd7b4..0d39424 100644 --- a/typescript/proxies/index.ts +++ b/typescript/proxies/index.ts @@ -25,13 +25,13 @@ async function createSessionWithGeoLocation() { projectId: process.env.BROWSERBASE_PROJECT_ID!, proxies: [ { - "type": "browserbase", // Use Browserbase's managed proxy infrastructure. - "geolocation": { - "city": "NEW_YORK", // Simulate traffic from New York for testing geo-specific content. - "state": "NY", // See https://docs.browserbase.com/features/proxies for more geolocation options. - "country": "US" - } - } + type: "browserbase", // Use Browserbase's managed proxy infrastructure. + geolocation: { + city: "NEW_YORK", // Simulate traffic from New York for testing geo-specific content. + state: "NY", // See https://docs.browserbase.com/features/proxies for more geolocation options. + country: "US", + }, + }, ], }); return session; @@ -43,20 +43,22 @@ async function createSessionWithCustomProxies() { projectId: process.env.BROWSERBASE_PROJECT_ID!, proxies: [ { - "type": "external", // Connect to your own proxy server infrastructure. - "server": "http://...", // Your proxy server endpoint. - "username": "user", // Authentication credentials for proxy access. - "password": "pass", - } - ] + type: "external", // Connect to your own proxy server infrastructure. + server: "http://...", // Your proxy server endpoint. + username: "user", // Authentication credentials for proxy access. + password: "pass", + }, + ], }); return session; } - -async function testSession(sessionFunction: () => Promise, sessionName: string) { +async function testSession( + sessionFunction: () => Promise<{ id: string; connectUrl: string }>, + sessionName: string, +) { console.log(`\n=== Testing ${sessionName} ===`); - + // Create session with specific proxy configuration to test different routing scenarios. const session = await sessionFunction(); console.log("Session URL: https://browserbase.com/sessions/" + session.id); @@ -76,22 +78,24 @@ async function testSession(sessionFunction: () => Promise, sessionName: str const stagehand = new Stagehand({ env: "BROWSERBASE", verbose: 1, - // 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.) + // 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: "openai/gpt-4.1", browserbaseSessionID: session.id, // Use the existing Browserbase session }); try { - // Initialize Stagehand + // Initialize Stagehand await stagehand.init(); const stagehandPage = stagehand.context.pages()[0]; // Navigate to IP info service to verify proxy location and IP address. - await stagehandPage.goto("https://ipinfo.io/json", { waitUntil: "domcontentloaded" }); - + await stagehandPage.goto("https://ipinfo.io/json", { + waitUntil: "domcontentloaded", + }); + // Extract structured IP and location data using Stagehand and Zod schema const geoInfo = await stagehand.extract( "Extract all IP information and geolocation data from the JSON response", @@ -102,10 +106,10 @@ async function testSession(sessionFunction: () => Promise, sessionName: str country: z.string().optional().describe("The country code"), loc: z.string().optional().describe("The latitude and longitude coordinates"), timezone: z.string().optional().describe("The timezone"), - org: z.string().optional().describe("The organization or ISP"), + org: z.string().optional().describe("The organization or ISP"), postal: z.string().optional().describe("The postal code"), - hostname: z.string().optional().describe("The hostname if available") - }) + hostname: z.string().optional().describe("The hostname if available"), + }), ); console.log("Geo Info:", JSON.stringify(geoInfo, null, 2)); @@ -124,13 +128,13 @@ async function testSession(sessionFunction: () => Promise, sessionName: str async function main() { // Test 1: Built-in proxies - Verify default proxy rotation works and shows different IPs. await testSession(createSessionWithBuiltInProxies, "Built-in Proxies"); - + // Test 2: Geolocation proxies - Confirm traffic routes through specified location (New York). await testSession(createSessionWithGeoLocation, "Geolocation Proxies (New York)"); - + // Test 3: Custom external proxies - Enable if you have a custom proxy server set up. // await testSession(createSessionWithCustomProxies, "Custom External Proxies"); console.log("\n=== All tests completed ==="); } -main(); \ No newline at end of file +main(); diff --git a/typescript/website-link-tester/README.md b/typescript/website-link-tester/README.md index 69d6e3a..89c2073 100644 --- a/typescript/website-link-tester/README.md +++ b/typescript/website-link-tester/README.md @@ -1,6 +1,7 @@ ## Stagehand + Browserbase: Website Link Tester ### AT A GLANCE + - **Goal**: Crawl a websiteโ€™s homepage, collect all links, and verify that each link loads successfully and matches its link text. - **Link extraction**: Uses `Stagehand.extract()` with a Zod schema to pull all links and their visible text from the homepage. - **Content verification**: Opens each link and uses AI to assess whether the page content matches what the link text suggests. @@ -8,12 +9,14 @@ - **Batch processing**: Processes links in batches controlled by `MAX_CONCURRENT_LINKS` (sequential by default, can be made concurrent). ### GLOSSARY + - **extract**: extract structured data from web pages using natural language instructions Docs โ†’ `https://docs.stagehand.dev/basics/extract` - **concurrent sessions**: run multiple browser sessions at the same time for faster batch processing Docs โ†’ `https://docs.browserbase.com/guides/concurrency-rate-limits` ### QUICKSTART + 1. **cd into the template** - `cd typescript/website-link-tester` 2. **Install dependencies** @@ -27,6 +30,7 @@ - `npm start` ### EXPECTED OUTPUT + - **Initial setup** - Initializes a Stagehand session with Browserbase - Prints a live session link for monitoring the browser in real time @@ -51,6 +55,7 @@ - Always closes browser sessions cleanly ### COMMON PITFALLS + - **Missing credentials** - Ensure `.env` contains `BROWSERBASE_PROJECT_ID` and `BROWSERBASE_API_KEY` - **Concurrency limits** @@ -63,12 +68,14 @@ - Social links and complex redirect chains may succeed in loading but not be fully verifiable for content; these are marked as special cases ### USE CASES + - **Regression testing**: Quickly verify that all key marketing and product links on your homepage still resolve correctly after a deployment. - **Content QA**: Detect mismatches between link text and destination page content (e.g., wrong page wired to a CTA). - **SEO and UX audits**: Find broken or misdirected links that can harm search rankings or user experience. - **Monitoring**: Run this periodically to flag link issues across your marketing site or documentation hub. ### TUNING BATCH SIZE & CONCURRENCY + - **`MAX_CONCURRENT_LINKS` in `index.ts`** - Default: `1` โ†’ sequential link verification (works on all plans) - Set to `> 1` โ†’ more concurrent link verifications per batch (requires higher Browserbase concurrency limits) @@ -80,6 +87,7 @@ - Apply different limits for external vs internal links if desired ### NEXT STEPS + - **Filter link scopes**: Limit verification to specific path prefixes (e.g., only `/docs` or `/blog`) or exclude certain domains. - **Recursive crawling**: Start from the homepage, follow internal links to secondary/tertiary pages, and cascade link discovery deeper into the site to build a more complete link map. - **Alerting & monitoring**: Integrate with Slack, email, or logging tools to notify when links start failing. @@ -87,10 +95,10 @@ - **Richer assessments**: Expand the extraction schema to capture additional metadata (e.g., HTTP status code, canonical URL, or key headings). ### 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/website-link-tester/index.ts b/typescript/website-link-tester/index.ts index cad3e0b..106c61a 100644 --- a/typescript/website-link-tester/index.ts +++ b/typescript/website-link-tester/index.ts @@ -78,9 +78,7 @@ async function collectLinksFromHomepage(): Promise { // Start a fresh browser session for link collection await stagehand.init(); - console.log( - `Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, - ); + console.log(`Watch live: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`); const page = stagehand.context.pages()[0]; @@ -141,9 +139,7 @@ async function verifySingleLink(link: Link): Promise { const page = browser.context.pages()[0]; // Detect if this is a social link (we treat those differently) - const isSocialLink = SOCIAL_DOMAINS.some((domain) => - link.url.includes(domain) - ); + const isSocialLink = SOCIAL_DOMAINS.some((domain) => link.url.includes(domain)); await page.goto(link.url, { timeoutMs: 30000 }); await page.waitForLoadState("domcontentloaded"); @@ -159,9 +155,7 @@ async function verifySingleLink(link: Link): Promise { // For social links, we consider a successful load good enough if (isSocialLink) { - console.log( - `[${link.linkText}] Social media link - skipping content verification`, - ); + console.log(`[${link.linkText}] Social media link - skipping content verification`); return { linkText: link.linkText, @@ -169,8 +163,7 @@ async function verifySingleLink(link: Link): Promise { success: true, pageTitle: "Social Media Link", contentMatches: true, - assessment: - "Social media link loaded successfully (content verification skipped)", + assessment: "Social media link loaded successfully (content verification skipped)", }; } @@ -186,9 +179,7 @@ async function verifySingleLink(link: Link): Promise { console.log(`[${link.linkText}] Page Title: ${verification.pageTitle}`); console.log( - `[${link.linkText}] Content Matches: ${ - verification.contentMatches ? "YES" : "NO" - }`, + `[${link.linkText}] Content Matches: ${verification.contentMatches ? "YES" : "NO"}`, ); console.log(`[${link.linkText}] Assessment: ${verification.assessment}`); @@ -203,9 +194,7 @@ async function verifySingleLink(link: Link): Promise { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error( - `Failed to verify link "${link.linkText}": ${errorMessage}`, - ); + console.error(`Failed to verify link "${link.linkText}": ${errorMessage}`); // On failure, return a structured result capturing the error message return { @@ -239,9 +228,7 @@ async function verifyLinksInBatches(links: Link[]): Promise { `\n=== Processing batch ${Math.floor(i / MAX_CONCURRENT_LINKS) + 1} (${batch.length} links) ===`, ); - const batchResults = await Promise.all( - batch.map((link) => verifySingleLink(link)), - ); + const batchResults = await Promise.all(batch.map((link) => verifySingleLink(link))); results.push(...batchResults); @@ -261,14 +248,14 @@ function outputResults(results: LinkCheckResult[], label: string = "FINAL RESULT console.log("\n" + "=".repeat(80)); console.log(label); console.log("=".repeat(80)); - + const finalReport = { totalLinks: results.length, successful: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length, results, }; - + try { console.log(JSON.stringify(finalReport, null, 2)); } catch (stringifyError) { @@ -278,7 +265,7 @@ function outputResults(results: LinkCheckResult[], label: string = "FINAL RESULT console.log(`Successful: ${finalReport.successful}`); console.log(`Failed: ${finalReport.failed}`); } - + console.log("\n" + "=".repeat(80)); } @@ -290,31 +277,31 @@ function outputResults(results: LinkCheckResult[], label: string = "FINAL RESULT */ async function main() { console.log("Starting main function..."); - + let results: LinkCheckResult[] = []; - + try { const links = await collectLinksFromHomepage(); console.log(`Collected ${links.length} links, starting verification...`); results = await verifyLinksInBatches(links); - + console.log("\nโœ“ All links verified!"); console.log(`Results array length: ${results.length}`); outputResults(results); - + console.log("Script completed successfully"); } catch (error) { console.error("\nError occurred during execution:", error); - + if (results.length > 0) { console.log(`\nOutputting partial results (${results.length} links processed before error):`); outputResults(results, "PARTIAL RESULTS (Error Occurred)"); } else { console.log("No results to output - error occurred before any links were verified"); } - + throw error; } } @@ -322,4 +309,4 @@ async function main() { main().catch((err) => { console.error("Application error:", err); process.exit(1); -}); \ No newline at end of file +});