From b5008af3c2356d83bf5913642502f55443e2b831 Mon Sep 17 00:00:00 2001 From: jordan-extend Date: Thu, 3 Sep 2026 13:11:06 -0400 Subject: [PATCH 1/2] extend-browserbase: update Extend SDK and model versions - Bump extend-ai to ^2.2.0 (TS) and >=1.19.0 (Python) - Pin extraction_light baseVersion 3.5.1 and parse_performance engineVersion 2.0.0 (latest stable per the Extend model changelogs) - Drop config fields removed from the current Extend API (parseConfig.advancedOptions.engine/agenticOcrEnabled/pageBreaksEnabled, advancedOptions.advancedFigureParsingEnabled) and empty no-op options --- python/extend-browserbase/main.py | 10 ++-------- python/extend-browserbase/pyproject.toml | 2 +- typescript/extend-browserbase/index.ts | 10 ++-------- typescript/extend-browserbase/package.json | 2 +- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/python/extend-browserbase/main.py b/python/extend-browserbase/main.py index e6770b9b..446217e3 100644 --- a/python/extend-browserbase/main.py +++ b/python/extend-browserbase/main.py @@ -23,7 +23,7 @@ # Uses extraction_light base extractor with parse_performance engine for low latency RECEIPT_EXTRACTION_CONFIG = { "baseProcessor": "extraction_light", - "baseVersion": "3.4.0", + "baseVersion": "3.5.1", "parseConfig": { "engine": "parse_performance", "target": "markdown", @@ -43,11 +43,8 @@ "figureImageClippingEnabled": False, }, }, - "engineVersion": "1.0.1", + "engineVersion": "2.0.0", "advancedOptions": { - "engine": "parse_performance", - "agenticOcrEnabled": False, - "pageBreaksEnabled": True, "pageRotationEnabled": False, "verticalGroupingThreshold": 1, }, @@ -150,9 +147,6 @@ "advancedMultimodalEnabled": False, "citationsEnabled": True, "arrayCitationStrategy": "item", - "pageRanges": [], - "chunkingOptions": {}, - "advancedFigureParsingEnabled": True, }, } diff --git a/python/extend-browserbase/pyproject.toml b/python/extend-browserbase/pyproject.toml index 85810c07..d485edd5 100644 --- a/python/extend-browserbase/pyproject.toml +++ b/python/extend-browserbase/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.11,<3.14" dependencies = [ "browserbase>=1.7.0", - "extend-ai>=1.0.0", + "extend-ai>=1.19.0", "python-dotenv>=1.2.1", "stagehand==4.0.0", ] diff --git a/typescript/extend-browserbase/index.ts b/typescript/extend-browserbase/index.ts index 67e0380b..604f6c04 100644 --- a/typescript/extend-browserbase/index.ts +++ b/typescript/extend-browserbase/index.ts @@ -86,7 +86,7 @@ function extractFilesFromZip(zipPath: string, outputDir: string = "output/docume // Uses extraction_light base extractor with parse_performance engine for low latency const receiptExtractionConfig = { baseProcessor: "extraction_light", - baseVersion: "3.4.0", + baseVersion: "3.5.1", parseConfig: { engine: "parse_performance", target: "markdown", @@ -106,11 +106,8 @@ const receiptExtractionConfig = { figureImageClippingEnabled: false, }, }, - engineVersion: "1.0.1", + engineVersion: "2.0.0", advancedOptions: { - engine: "parse_performance", - agenticOcrEnabled: false, - pageBreaksEnabled: true, pageRotationEnabled: false, verticalGroupingThreshold: 1, }, @@ -213,9 +210,6 @@ const receiptExtractionConfig = { advancedMultimodalEnabled: false, citationsEnabled: true, arrayCitationStrategy: "item", - pageRanges: [], - chunkingOptions: {}, - advancedFigureParsingEnabled: true, }, }; diff --git a/typescript/extend-browserbase/package.json b/typescript/extend-browserbase/package.json index 12591d9f..1e28e052 100644 --- a/typescript/extend-browserbase/package.json +++ b/typescript/extend-browserbase/package.json @@ -12,7 +12,7 @@ "@browserbasehq/stagehand": "4.0.0", "adm-zip": "^0.5.16", "dotenv": "^17.2.4", - "extend-ai": "^1.0.2", + "extend-ai": "^2.2.0", "open": "^11.0.0" }, "devDependencies": { From 29a1c39de180a5123e19dee634bc839d777e8e25 Mon Sep 17 00:00:00 2001 From: jordan-extend Date: Thu, 3 Sep 2026 13:20:27 -0400 Subject: [PATCH 2/2] extend-browserbase: add Go template and index it in the README - New go/extend-browserbase mirroring the TS/Python flow with the Stagehand Go SDK v4 and the Extend Go SDK v0.6.0 (typed inline extraction config) - Downloads are fetched per-file via the Browserbase Downloads REST API since there is no Browserbase Go SDK - Add the GO link to the extend-browserbase row in the root README - Point the TS/Python READMEs at docs.extend.ai (docs.extend.app now redirects) --- README.md | 90 ++-- go/extend-browserbase/README.md | 79 ++++ go/extend-browserbase/go.mod | 21 + go/extend-browserbase/go.sum | 42 ++ go/extend-browserbase/main.go | 599 ++++++++++++++++++++++++ python/extend-browserbase/README.md | 6 +- typescript/extend-browserbase/README.md | 6 +- 7 files changed, 792 insertions(+), 51 deletions(-) create mode 100644 go/extend-browserbase/README.md create mode 100644 go/extend-browserbase/go.mod create mode 100644 go/extend-browserbase/go.sum create mode 100644 go/extend-browserbase/main.go diff --git a/README.md b/README.md index 593d93f3..55869bb4 100644 --- a/README.md +++ b/README.md @@ -8,51 +8,51 @@ Ready-to-use automation templates for Stagehand and Browserbase. Each template h ## All Templates -| Template | TS | PY | GO | Description | -| -------------------------------- | ------------------------------------------------- | --------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | -| agent-with-human-in-loop | [TS](typescript/agent-with-human-in-loop) | - | - | Build an AI agent that can pause and ask a human for input mid-task | -| amazon-global-price-comparison | [TS](typescript/amazon-global-price-comparison) | [PY](python/amazon-global-price-comparison) | - | Compare Amazon product prices across multiple countries using geolocation proxies | -| amazon-product-scraping | [TS](typescript/amazon-product-scraping) | [PY](python/amazon-product-scraping) | - | Scrape the first 3 Amazon search results for a given query and return structured product data | -| basic-caching | [TS](typescript/basic-caching) | [PY](python/basic-caching) | - | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | -| basic-recaptcha | [TS](typescript/basic-recaptcha) | [PY](python/basic-recaptcha) | - | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | -| browser-agent-demo | [TS](typescript/browser-agent-demo) | - | - | Browser agent that searches the web, fetches page content, and autonomously extracts information | -| browserbase-reducto | [TS](typescript/browserbase-reducto) | [PY](python/browserbase-reducto) | - | Download financial PDFs from websites and extract structured data using AI-powered document parsing | -| business-lookup | [TS](typescript/business-lookup) | [PY](python/business-lookup) | - | Research business registry records with a bring-your-own agent and Stagehand code mode | -| cartesia-form-filling | - | [PY](python/cartesia-form-filling) | - | Voice agent that conducts phone questionnaires while automatically filling out web forms | -| cerebras-docs-checker | - | [PY](python/cerebras-docs-checker) | - | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase | -| company-address-finder | [TS](typescript/company-address-finder) | [PY](python/company-address-finder) | - | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | -| company-value-prop-generator | [TS](typescript/company-value-prop-generator) | [PY](python/company-value-prop-generator) | - | Extract and format website value propositions into concise one-liners for email personalization | -| context | [TS](typescript/context) | [PY](python/context) | - | Persistent authentication using Browserbase contexts that survive across sessions | -| council-events | [TS](typescript/council-events) | [PY](python/council-events) | - | Automate event information extraction from Philadelphia Council | -| download-financial-statements | [TS](typescript/download-financial-statements) | [PY](python/download-financial-statements) | - | Download Apple's quarterly financial statements (PDFs) from their investor relations site | -| dynamic-form-filling | [TS](typescript/dynamic-form-filling) | - | - | Fill dynamic forms with a Vercel AI SDK agent and Stagehand's code_execute browser tool | -| exa-browserbase | [TS](typescript/exa-browserbase) | [PY](python/exa-browserbase) | - | Automate job applications with AI that writes smart, tailored responses for each role | -| extend-browserbase | [TS](typescript/extend-browserbase) | [PY](python/extend-browserbase) | - | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | -| form-filling | [TS](typescript/form-filling) | [PY](python/form-filling) | - | Automate form filling with Stagehand and Browserbase | -| gemini-3-flash | [TS](typescript/gemini-3-flash) | - | - | Browser research with a Gemini 3 Flash agent and Stagehand code mode | -| gemini-cua | [TS](typescript/gemini-cua) | [PY](python/gemini-cua) | - | Browser research with a bring-your-own Gemini agent and Stagehand code mode | -| getting-started-with-browserbase | [TS](typescript/getting-started-with-browserbase) | [PY](python/getting-started-with-browserbase) | - | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | -| gift-finder | [TS](typescript/gift-finder) | [PY](python/gift-finder) | - | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | -| google-trends | [TS](typescript/google-trends) | [PY](python/google-trends) | - | Extract trending search keywords from Google Trends for any country with structured JSON output | -| hackernews | - | - | [GO](go/hackernews) | Demonstrate Stagehand's core browser automation features through a complete Hacker News workflow | -| image-url-download | [TS](typescript/image-url-download) | [PY](python/image-url-download) | - | Extract all image URLs from a page and download each image through the browser's direct connection | -| job-application | [TS](typescript/job-application) | [PY](python/job-application) | - | Automate job applications by discovering job listings and submitting applications | -| license-verification | [TS](typescript/license-verification) | [PY](python/license-verification) | - | Extract structured, validated data from websites using Stagehand + Zod | -| manual-mfa-with-contexts | [TS](typescript/manual-mfa-with-contexts) | [PY](python/manual-mfa-with-contexts) | - | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | -| mfa-handling | [TS](typescript/mfa-handling) | [PY](python/mfa-handling) | - | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | -| microsoft-cua | [TS](typescript/microsoft-cua) | - | - | Browser research with a bring-your-own OpenAI agent and Stagehand code mode | -| nurse-verification | [TS](typescript/nurse-verification) | [PY](python/nurse-verification) | - | Automate verification of nurse licenses by filling forms and extracting structured results | -| pickleball | [TS](typescript/pickleball) | [PY](python/pickleball) | - | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | -| playwright | [TS](typescript/playwright) | [PY](python/playwright) | - | Raw Playwright usage with Browserbase (no Stagehand) | -| playwright-mfa-handling | [TS](typescript/playwright-mfa-handling) | [PY](python/playwright-mfa-handling) | - | Automate MFA completion using TOTP with raw Playwright and Browserbase | -| polymarket-research | [TS](typescript/polymarket-research) | [PY](python/polymarket-research) | - | Automate market research on prediction markets using Stagehand | -| proxies | [TS](typescript/proxies) | [PY](python/proxies) | - | Demonstrate different proxy configurations with Browserbase sessions | -| proxies-weather | [TS](typescript/proxies-weather) | [PY](python/proxies-weather) | - | Geolocation proxies fetching location-specific weather data from multiple cities | -| puppeteer | [TS](typescript/puppeteer) | - | - | Raw Puppeteer usage with Browserbase | -| sec-filing-research | [TS](typescript/sec-filing-research) | [PY](python/sec-filing-research) | - | Search SEC EDGAR for a company and extract recent filing metadata | -| selenium | [TS](typescript/selenium) | [PY](python/selenium) | - | Raw Selenium usage with Browserbase | -| smart-fetch-scraper | [TS](typescript/smart-fetch-scraper) | [PY](python/smart-fetch-scraper) | - | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | -| website-link-tester | [TS](typescript/website-link-tester) | [PY](python/website-link-tester) | - | Crawl a website's homepage, collect all links, and verify each link loads successfully | +| Template | TS | PY | GO | Description | +| -------------------------------- | ------------------------------------------------- | --------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- | +| agent-with-human-in-loop | [TS](typescript/agent-with-human-in-loop) | - | - | Build an AI agent that can pause and ask a human for input mid-task | +| amazon-global-price-comparison | [TS](typescript/amazon-global-price-comparison) | [PY](python/amazon-global-price-comparison) | - | Compare Amazon product prices across multiple countries using geolocation proxies | +| amazon-product-scraping | [TS](typescript/amazon-product-scraping) | [PY](python/amazon-product-scraping) | - | Scrape the first 3 Amazon search results for a given query and return structured product data | +| basic-caching | [TS](typescript/basic-caching) | [PY](python/basic-caching) | - | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | +| basic-recaptcha | [TS](typescript/basic-recaptcha) | [PY](python/basic-recaptcha) | - | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | +| browser-agent-demo | [TS](typescript/browser-agent-demo) | - | - | Browser agent that searches the web, fetches page content, and autonomously extracts information | +| browserbase-reducto | [TS](typescript/browserbase-reducto) | [PY](python/browserbase-reducto) | - | Download financial PDFs from websites and extract structured data using AI-powered document parsing | +| business-lookup | [TS](typescript/business-lookup) | [PY](python/business-lookup) | - | Research business registry records with a bring-your-own agent and Stagehand code mode | +| cartesia-form-filling | - | [PY](python/cartesia-form-filling) | - | Voice agent that conducts phone questionnaires while automatically filling out web forms | +| cerebras-docs-checker | - | [PY](python/cerebras-docs-checker) | - | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase | +| company-address-finder | [TS](typescript/company-address-finder) | [PY](python/company-address-finder) | - | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | +| company-value-prop-generator | [TS](typescript/company-value-prop-generator) | [PY](python/company-value-prop-generator) | - | Extract and format website value propositions into concise one-liners for email personalization | +| context | [TS](typescript/context) | [PY](python/context) | - | Persistent authentication using Browserbase contexts that survive across sessions | +| council-events | [TS](typescript/council-events) | [PY](python/council-events) | - | Automate event information extraction from Philadelphia Council | +| download-financial-statements | [TS](typescript/download-financial-statements) | [PY](python/download-financial-statements) | - | Download Apple's quarterly financial statements (PDFs) from their investor relations site | +| dynamic-form-filling | [TS](typescript/dynamic-form-filling) | - | - | Fill dynamic forms with a Vercel AI SDK agent and Stagehand's code_execute browser tool | +| exa-browserbase | [TS](typescript/exa-browserbase) | [PY](python/exa-browserbase) | - | Automate job applications with AI that writes smart, tailored responses for each role | +| extend-browserbase | [TS](typescript/extend-browserbase) | [PY](python/extend-browserbase) | [GO](go/extend-browserbase) | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | +| form-filling | [TS](typescript/form-filling) | [PY](python/form-filling) | - | Automate form filling with Stagehand and Browserbase | +| gemini-3-flash | [TS](typescript/gemini-3-flash) | - | - | Browser research with a Gemini 3 Flash agent and Stagehand code mode | +| gemini-cua | [TS](typescript/gemini-cua) | [PY](python/gemini-cua) | - | Browser research with a bring-your-own Gemini agent and Stagehand code mode | +| getting-started-with-browserbase | [TS](typescript/getting-started-with-browserbase) | [PY](python/getting-started-with-browserbase) | - | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | +| gift-finder | [TS](typescript/gift-finder) | [PY](python/gift-finder) | - | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | +| google-trends | [TS](typescript/google-trends) | [PY](python/google-trends) | - | Extract trending search keywords from Google Trends for any country with structured JSON output | +| hackernews | - | - | [GO](go/hackernews) | Demonstrate Stagehand's core browser automation features through a complete Hacker News workflow | +| image-url-download | [TS](typescript/image-url-download) | [PY](python/image-url-download) | - | Extract all image URLs from a page and download each image through the browser's direct connection | +| job-application | [TS](typescript/job-application) | [PY](python/job-application) | - | Automate job applications by discovering job listings and submitting applications | +| license-verification | [TS](typescript/license-verification) | [PY](python/license-verification) | - | Extract structured, validated data from websites using Stagehand + Zod | +| manual-mfa-with-contexts | [TS](typescript/manual-mfa-with-contexts) | [PY](python/manual-mfa-with-contexts) | - | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | +| mfa-handling | [TS](typescript/mfa-handling) | [PY](python/mfa-handling) | - | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | +| microsoft-cua | [TS](typescript/microsoft-cua) | - | - | Browser research with a bring-your-own OpenAI agent and Stagehand code mode | +| nurse-verification | [TS](typescript/nurse-verification) | [PY](python/nurse-verification) | - | Automate verification of nurse licenses by filling forms and extracting structured results | +| pickleball | [TS](typescript/pickleball) | [PY](python/pickleball) | - | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | +| playwright | [TS](typescript/playwright) | [PY](python/playwright) | - | Raw Playwright usage with Browserbase (no Stagehand) | +| playwright-mfa-handling | [TS](typescript/playwright-mfa-handling) | [PY](python/playwright-mfa-handling) | - | Automate MFA completion using TOTP with raw Playwright and Browserbase | +| polymarket-research | [TS](typescript/polymarket-research) | [PY](python/polymarket-research) | - | Automate market research on prediction markets using Stagehand | +| proxies | [TS](typescript/proxies) | [PY](python/proxies) | - | Demonstrate different proxy configurations with Browserbase sessions | +| proxies-weather | [TS](typescript/proxies-weather) | [PY](python/proxies-weather) | - | Geolocation proxies fetching location-specific weather data from multiple cities | +| puppeteer | [TS](typescript/puppeteer) | - | - | Raw Puppeteer usage with Browserbase | +| sec-filing-research | [TS](typescript/sec-filing-research) | [PY](python/sec-filing-research) | - | Search SEC EDGAR for a company and extract recent filing metadata | +| selenium | [TS](typescript/selenium) | [PY](python/selenium) | - | Raw Selenium usage with Browserbase | +| smart-fetch-scraper | [TS](typescript/smart-fetch-scraper) | [PY](python/smart-fetch-scraper) | - | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | +| website-link-tester | [TS](typescript/website-link-tester) | [PY](python/website-link-tester) | - | Crawl a website's homepage, collect all links, and verify each link loads successfully | ## Model Gateway diff --git a/go/extend-browserbase/README.md b/go/extend-browserbase/README.md new file mode 100644 index 00000000..4106d083 --- /dev/null +++ b/go/extend-browserbase/README.md @@ -0,0 +1,79 @@ +# Stagehand + Browserbase + Extend: Download Expense Receipts and Parse with Extend AI + +Stagehand is the SDK for browser agents. + +## AT A GLANCE + +- **Goal**: Automate downloading receipts from an expense portal and extract structured receipt data using AI-powered document parsing. +- **Pattern Template**: Demonstrates the integration pattern of Browserbase (browser automation + download capture) + Extend AI (schema-based document extraction) in Go. +- **Workflow**: Stagehand navigates the expense portal and clicks each receipt's download link; Browserbase captures downloads. The program polls the Browserbase Downloads API for the session's files, saves them locally, then optionally sends them to Extend for structured extraction (vendor, date, totals, line items, etc.). +- **Download Handling**: Implements retry/polling around Browserbase's Downloads API until the expected files have synced. +- **Structured Extraction**: Extend AI extraction with a typed inline config built from the Extend Go SDK; results written to `output/results/receipts.json` and `receipts.csv`. +- Docs → [Browserbase Downloads](https://docs.browserbase.com/features/downloads) | [Extend AI](https://docs.extend.ai) + +## GLOSSARY + +- **Act**: perform UI actions from natural language prompts or observed actions (click, scroll, navigate) + Docs → https://docs.stagehand.dev/v4/basics/act +- **Observe**: find and return interactive elements on the page matching a description, without performing actions. Used here to locate all individual download buttons before clicking them. + Docs → https://docs.stagehand.dev/v4/basics/observe +- **Browserbase Downloads**: When files are downloaded during a browser session, Browserbase captures and stores them. Files are listed and retrieved individually via the Downloads REST API. + Docs → https://docs.browserbase.com/features/downloads +- **Extend AI extraction**: A configurable document extraction pipeline that parses files against a JSON schema and returns structured data. Config can be passed inline or via a saved extractor resource. + Docs → https://docs.extend.ai/extraction/overview +- **Download polling**: Browserbase syncs downloads in real time; the program retries every 2 seconds until the files are available or a timeout is reached. + +## QUICKSTART + +1. Install Go 1.26 or newer (`go version`). +2. `cd go/extend-browserbase` +3. Set the required API keys in your environment: + - `export BROWSERBASE_API_KEY=your_browserbase_api_key` + - `export EXTEND_API_KEY=your_extend_api_key` (optional — enables receipt parsing) +4. `go mod download` +5. `go run .` + +## EXPECTED OUTPUT + +- Launches a Browserbase browser and attaches Stagehand V4; Live View remains available in the Sessions dashboard +- Navigates to the expense portal and finds all per-receipt download links via Observe +- Clicks each download link; Browserbase captures files +- After closing the session, polls the Downloads API and saves each file to `output/documents/` +- If `EXTEND_API_KEY` is set: uploads each file to Extend and runs extraction with the inline config, writes `output/results/receipts.json` and `receipts.csv` +- Closes Stagehand and the browser cleanly + +## COMMON PITFALLS + +- Missing Go installation: ensure Go 1.26+ is installed +- Module not found: run `go mod download` if dependencies are not resolved +- Missing credentials: verify `BROWSERBASE_API_KEY` is set in your environment +- Download timeout: increase `downloadTimeout` if downloads take longer than 60 seconds +- No downloads found: ensure the download clicks actually triggered (inspect the session in the Browserbase dashboard) +- Rate limiting on Extend: the Extend SDK retries 429 and 5xx responses with exponential backoff (`option.WithMaxAttempts`), but very large batches may need `extractBatchSize` reduced from 9 +- Find more information on your Browserbase dashboard → https://www.browserbase.com/sign-in + +## USE CASES + +• Expense automation: Download receipts from expense portals and extract vendor, date, totals, and line items for accounting systems. +• Document batch processing: Collect files from web portals and run structured extraction across all of them with a single program. +• Receipt digitization: Convert paper/PDF receipts into structured JSON and CSV for import into ERP, bookkeeping, or reimbursement tools. + +## NEXT STEPS + +• Parameterize the portal URL: Accept the expense portal URL from an env var or flag to support different receipt sources. +• Custom schemas: Modify `receiptExtractionConfig` to extract different document types (invoices, W-2s, contracts) by changing the JSON schema. +• Add validation: Compare extracted totals against line item sums to flag discrepancies or incomplete extractions. +• Production extraction: Swap the synchronous `client.Extract` call for `client.ExtractRuns.Create` with polling or webhooks for long-running documents. +• Scheduled runs: Deploy on cron/Lambda to periodically check for new receipts and process them automatically. + +## HELPFUL RESOURCES + +📚 Stagehand Docs: https://docs.stagehand.dev/v4/first-steps/introduction +📚 Browserbase Downloads: https://docs.browserbase.com/features/downloads +📚 Extend AI: https://docs.extend.ai +📚 Extend Go SDK: https://github.com/extend-hq/extend-go-sdk +🎮 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/go/extend-browserbase/go.mod b/go/extend-browserbase/go.mod new file mode 100644 index 00000000..58d84e13 --- /dev/null +++ b/go/extend-browserbase/go.mod @@ -0,0 +1,21 @@ +module extend-browserbase + +go 1.26.0 + +require ( + github.com/browserbase/stagehand/packages/sdk-go/v4 v4.0.2 + github.com/extend-hq/extend-go-sdk v0.6.0 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/coder/websocket v1.8.13 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/invopop/jsonschema v0.14.0 // indirect + github.com/pb33f/ordered-map/v2 v2.3.1 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect + golang.org/x/mod v0.38.0 // indirect +) diff --git a/go/extend-browserbase/go.sum b/go/extend-browserbase/go.sum new file mode 100644 index 00000000..e5213a95 --- /dev/null +++ b/go/extend-browserbase/go.sum @@ -0,0 +1,42 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/browserbase/stagehand/packages/sdk-go/v4 v4.0.2 h1:axCIlzMz6Kao32Wli7C6uZVHbEc5MzmsUDAbf2y9TXw= +github.com/browserbase/stagehand/packages/sdk-go/v4 v4.0.2/go.mod h1:Z4qkSeN4whiyoYk+jqKbwYbUPu16JgmvEZWtiiaXyqY= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/extend-hq/extend-go-sdk v0.6.0 h1:ZP0TLd77YtFyD8bMxYdpWIsymuJpYdpiFcH+QTtN6vQ= +github.com/extend-hq/extend-go-sdk v0.6.0/go.mod h1:OGwNG071bADLZ/BicWu/TLYn+QGELr/r051eECkfcn0= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= +github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= +github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= +github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s= +go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/extend-browserbase/main.go b/go/extend-browserbase/main.go new file mode 100644 index 00000000..e5b7a4bd --- /dev/null +++ b/go/extend-browserbase/main.go @@ -0,0 +1,599 @@ +// Stagehand + Browserbase + Extend: Download Expense Receipts and Parse with Extend AI +// See README.md for full documentation + +package main + +import ( + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "sync" + "time" + + stagehand "github.com/browserbase/stagehand/packages/sdk-go/v4" + extend "github.com/extend-hq/extend-go-sdk" + extendclient "github.com/extend-hq/extend-go-sdk/client" + "github.com/extend-hq/extend-go-sdk/option" +) + +const ( + expensePortalURL = "https://v0-reimburse-me-expense-portal.vercel.app/" + browserbaseAPI = "https://api.browserbase.com/v1" + documentsDir = "output/documents" + resultsDir = "output/results" + downloadTimeout = 60 * time.Second + extractBatchSize = 9 +) + +func main() { + if err := run(context.Background()); err != nil { + log.Println("Application error:", err) + log.Println("Common issues:") + log.Println(" - Check BROWSERBASE_API_KEY is set in your environment") + log.Println(" - Set EXTEND_API_KEY to enable receipt parsing with Extend AI") + log.Println(" - Verify internet connection and expense portal accessibility") + log.Println("Docs: https://docs.stagehand.dev/v4/first-steps/introduction") + os.Exit(1) + } +} + +func run(ctx context.Context) error { + fmt.Println("Starting Expense Receipt Downloader...") + + apiKey := os.Getenv("BROWSERBASE_API_KEY") + if apiKey == "" { + return errors.New("BROWSERBASE_API_KEY is required") + } + + ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + + sessionID, clicks, err := downloadReceipts(ctx, apiKey) + if err != nil { + return err + } + + // Give Browserbase a moment to finalize downloads after the session closes. + time.Sleep(2 * time.Second) + + fmt.Println("\nRetrieving downloads from Browserbase...") + files, err := saveDownloadsWithRetry(ctx, apiKey, sessionID, clicks, downloadTimeout) + if err != nil { + return fmt.Errorf("download retrieval failed: %w", err) + } + if len(files) == 0 { + fmt.Println("No downloads were captured") + return nil + } + + fmt.Println("\n=== Download Summary ===") + fmt.Printf("Total files downloaded: %d\n", len(files)) + fmt.Printf("Files saved to: ./%s/\n", documentsDir) + + if err := parseReceiptsWithExtend(ctx, files); err != nil { + return err + } + + fmt.Println("\nExpense receipt download complete!") + return nil +} + +// downloadReceipts drives a Browserbase session with Stagehand: it opens the +// expense portal, observes every per-receipt download link, and clicks each one. +// The session is closed before returning so downloads can be retrieved. +// It returns the session ID and the number of successful download clicks. +func downloadReceipts(ctx context.Context, apiKey string) (sessionID string, clicks int, err error) { + sessionTimeout := 300.0 + browser, err := stagehand.LaunchBrowserbase(ctx, stagehand.BrowserbaseLaunchOptions{ + APIKey: apiKey, + Timeout: &sessionTimeout, + }) + if err != nil { + return "", 0, fmt.Errorf("launch Browserbase: %w", err) + } + defer func() { + cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cleanupCancel() + err = errors.Join(err, browser.Close(cleanupCtx)) + }() + + sessionID = browser.SessionID() + if sessionID == "" { + return "", 0, errors.New("Browserbase launch did not return a session ID") + } + + client, err := stagehand.Create(ctx, stagehand.CreateOptions{Browser: browser}) + if err != nil { + return "", 0, fmt.Errorf("create Stagehand: %w", err) + } + defer func() { + cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cleanupCancel() + err = errors.Join(err, client.Close(cleanupCtx)) + }() + fmt.Println("Stagehand initialized successfully!") + fmt.Println("Live View is available in the Browserbase Sessions dashboard") + + browserContext, err := browser.Context() + if err != nil { + return "", 0, fmt.Errorf("get browser context: %w", err) + } + pages, err := browserContext.Pages(ctx) + if err != nil { + return "", 0, fmt.Errorf("list pages: %w", err) + } + if len(pages) == 0 { + return "", 0, errors.New("no active page after Stagehand initialization") + } + page := pages[0] + + fmt.Println("\nNavigating to expense portal...") + waitUntil := stagehand.LoadStateDOMContentLoaded + if _, err := page.Goto(ctx, expensePortalURL, &stagehand.PageNavigationOptions{WaitUntil: &waitUntil}); err != nil { + return "", 0, fmt.Errorf("navigate to expense portal: %w", err) + } + + // Observe finds every individual download link (not the "Download All" button) + // so each can be passed straight to act for precise element targeting. + fmt.Println("\nFinding all individual download buttons...") + instruction := "Find all the small Download links on individual receipt cards." + observed, err := client.Observe(ctx, &instruction, &stagehand.StagehandClientObserveOptions{Page: page}) + if err != nil { + return "", 0, fmt.Errorf("observe download links: %w", err) + } + if len(observed.Data) == 0 { + return "", 0, errors.New("no receipt download links were found") + } + + actOptions := &stagehand.StagehandClientActOptions{Page: page} + scroll := func() { + _, _ = client.Act(ctx, stagehand.ActInstruction("Scroll down slightly"), actOptions) + } + for i, action := range observed.Data { + fmt.Printf("Downloading receipt %d/%d...\n", i+1, len(observed.Data)) + + if _, err := client.Act(ctx, stagehand.ObservedAction(action), actOptions); err == nil { + clicks++ + } else { + // If the click fails, scroll the element into view and retry once. + fmt.Printf(" Could not click download button %d, trying to scroll and retry...\n", i+1) + scroll() + if _, err := client.Act(ctx, stagehand.ObservedAction(action), actOptions); err == nil { + clicks++ + } else { + fmt.Printf(" Skipping receipt %d\n", i+1) + } + } + + // Scroll periodically so later links stay in view. + if (i+1)%4 == 0 && i+1 < len(observed.Data) { + scroll() + } + } + + fmt.Printf("\nDownload clicks completed! (%d/%d successful)\n", clicks, len(observed.Data)) + return sessionID, clicks, nil +} + +type browserbaseDownload struct { + ID string `json:"id"` + Filename string `json:"filename"` + MimeType string `json:"mimeType"` + Size int64 `json:"size"` +} + +type browserbaseDownloadList struct { + Downloads []browserbaseDownload `json:"downloads"` + Total int `json:"total"` +} + +// saveDownloadsWithRetry polls the Browserbase Downloads API every 2 seconds +// until every expected file has synced (or the timeout passes), then saves each +// file into output/documents and returns the local paths. +func saveDownloadsWithRetry(ctx context.Context, apiKey, sessionID string, expected int, timeout time.Duration) ([]string, error) { + fmt.Printf("Waiting up to %s for downloads to complete...\n", timeout) + httpClient := &http.Client{Timeout: 30 * time.Second} + deadline := time.Now().Add(timeout) + + var list browserbaseDownloadList + for { + fmt.Println("Checking for downloads...") + if err := browserbaseGetJSON(ctx, httpClient, apiKey, + browserbaseAPI+"/downloads?sessionId="+url.QueryEscape(sessionID), &list); err != nil { + return nil, err + } + if list.Total >= expected && list.Total > 0 { + break + } + if time.Now().After(deadline) { + if list.Total == 0 { + return nil, errors.New("download timeout exceeded") + } + fmt.Printf("Timed out waiting for all downloads; continuing with %d of %d\n", list.Total, expected) + break + } + fmt.Printf("Downloads not ready yet (%d/%d), retrying...\n", list.Total, expected) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(2 * time.Second): + } + } + fmt.Printf("Downloads ready! Found %d file(s)\n", list.Total) + + if err := os.MkdirAll(documentsDir, 0o755); err != nil { + return nil, err + } + if err := os.MkdirAll(resultsDir, 0o755); err != nil { + return nil, err + } + + paths := make([]string, 0, len(list.Downloads)) + for _, download := range list.Downloads { + // filepath.Base guards against path separators in server-provided names. + outputPath := filepath.Join(documentsDir, filepath.Base(download.Filename)) + if err := browserbaseGetFile(ctx, httpClient, apiKey, browserbaseAPI+"/downloads/"+download.ID, outputPath); err != nil { + return nil, fmt.Errorf("save %s: %w", download.Filename, err) + } + fmt.Printf("Saved: %s (%d bytes)\n", outputPath, download.Size) + paths = append(paths, outputPath) + } + return paths, nil +} + +func browserbaseRequest(ctx context.Context, apiKey, endpoint, accept string) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("x-bb-api-key", apiKey) + req.Header.Set("Accept", accept) + return req, nil +} + +func browserbaseGetJSON(ctx context.Context, httpClient *http.Client, apiKey, endpoint string, out any) error { + req, err := browserbaseRequest(ctx, apiKey, endpoint, "application/json") + if err != nil { + return err + } + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return fmt.Errorf("browserbase %s returned %d: %s", endpoint, resp.StatusCode, strings.TrimSpace(string(body))) + } + return json.NewDecoder(resp.Body).Decode(out) +} + +func browserbaseGetFile(ctx context.Context, httpClient *http.Client, apiKey, endpoint, outputPath string) error { + req, err := browserbaseRequest(ctx, apiKey, endpoint, "application/octet-stream") + if err != nil { + return err + } + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("browserbase %s returned %d", endpoint, resp.StatusCode) + } + file, err := os.Create(outputPath) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(file, resp.Body) + return err +} + +// receiptExtractionConfig is an inline Extend extraction config: no extractor +// resource needs to be created ahead of time. It uses the extraction_light base +// processor with the parse_performance engine for low latency on simple receipts. +func receiptExtractionConfig() *extend.ExtractConfigJSON { + currencyField := func(description string) map[string]any { + return map[string]any{ + "type": "object", + "extend:type": "currency", + "description": description, + "required": []any{"amount", "iso_4217_currency_code"}, + "properties": map[string]any{ + "amount": map[string]any{"type": []any{"number", "null"}}, + "iso_4217_currency_code": map[string]any{"type": []any{"string", "null"}}, + }, + "additionalProperties": false, + } + } + + return &extend.ExtractConfigJSON{ + BaseProcessor: extend.ExtractBaseProcessorExtractionLight.Ptr(), + BaseVersion: new("3.5.1"), + ParseConfig: &extend.ParseConfig{ + Engine: extend.ParseConfigEngineParsePerformance.Ptr(), + EngineVersion: new("2.0.0"), + Target: extend.ParseConfigTargetMarkdown.Ptr(), + BlockOptions: &extend.ParseConfigBlockOptions{ + Text: &extend.ParseConfigBlockOptionsText{ + Agentic: &extend.ParseConfigBlockOptionsTextAgentic{Enabled: new(false)}, + SignatureDetectionEnabled: new(false), + }, + Tables: &extend.ParseConfigBlockOptionsTables{ + Agentic: &extend.ParseConfigBlockOptionsTablesAgentic{Enabled: new(false)}, + TargetFormat: extend.ParseConfigBlockOptionsTablesTargetFormatMarkdown.Ptr(), + CellBlocksEnabled: new(false), + TableHeaderContinuationEnabled: new(false), + }, + Figures: &extend.ParseConfigBlockOptionsFigures{ + Enabled: new(false), + FigureImageClippingEnabled: new(false), + }, + }, + AdvancedOptions: &extend.ParseConfigAdvancedOptions{ + PageRotationEnabled: new(false), + VerticalGroupingThreshold: new(1.0), + }, + ChunkingStrategy: &extend.ParseConfigChunkingStrategy{ + Type: extend.ParseConfigChunkingStrategyTypeDocument.Ptr(), + }, + }, + Schema: &extend.JSONObject{ + "type": "object", + "required": []any{ + "vendor_name", "receipt_date", "receipt_number", "total_amount", + "subtotal_amount", "tax_amount", "line_items", "payment_method", + }, + "properties": map[string]any{ + "vendor_name": map[string]any{ + "type": []any{"string", "null"}, + "description": "The name of the merchant or vendor on the receipt.", + }, + "receipt_date": map[string]any{ + "type": []any{"string", "null"}, + "description": "The date of the transaction shown on the receipt.", + "extend:type": "date", + }, + "receipt_number": map[string]any{ + "type": []any{"string", "null"}, + "description": "The receipt or transaction number, if present.", + }, + "total_amount": currencyField("The total amount paid on the receipt."), + "subtotal_amount": currencyField("The subtotal before tax, if shown."), + "tax_amount": currencyField("The tax amount on the receipt."), + "line_items": map[string]any{ + "type": "array", + "description": "Individual items on the receipt.", + "items": map[string]any{ + "type": "object", + "required": []any{"description", "quantity", "unit_price", "amount"}, + "properties": map[string]any{ + "description": map[string]any{ + "type": []any{"string", "null"}, + "description": "Description of the item purchased.", + }, + "quantity": map[string]any{ + "type": []any{"number", "null"}, + "description": "Quantity of the item, if shown.", + }, + "unit_price": map[string]any{ + "type": []any{"number", "null"}, + "description": "Price per unit, if shown.", + }, + "amount": map[string]any{ + "type": []any{"number", "null"}, + "description": "Total amount for this line item.", + }, + }, + "additionalProperties": false, + }, + }, + "payment_method": map[string]any{ + "type": []any{"string", "null"}, + "description": "The payment method used (e.g., cash, credit card, etc.).", + }, + }, + "additionalProperties": false, + }, + AdvancedOptions: &extend.ExtractAdvancedOptions{ + AdvancedMultimodalEnabled: new(false), + CitationsEnabled: new(true), + ArrayCitationStrategy: extend.ExtractAdvancedOptionsArrayCitationStrategyItem.Ptr(), + }, + } +} + +// namedReader gives the Extend multipart upload a clean filename instead of +// the local path an *os.File would report. +type namedReader struct { + io.Reader + name string +} + +func (r namedReader) Name() string { return r.name } + +type receiptResult struct { + File string `json:"file"` + RunID string `json:"runId,omitempty"` + Data any `json:"data"` +} + +// parseReceiptsWithExtend uploads each receipt to Extend, runs extraction with +// the inline config, and writes the results as JSON and CSV. +func parseReceiptsWithExtend(ctx context.Context, filePaths []string) error { + extendAPIKey := os.Getenv("EXTEND_API_KEY") + if extendAPIKey == "" || extendAPIKey == "YOUR_EXTEND_API_KEY_HERE" { + fmt.Println("\nWARNING: EXTEND_API_KEY not configured. Skipping receipt parsing.") + fmt.Println(" Set EXTEND_API_KEY to enable automatic receipt parsing.") + return nil + } + + fmt.Println("\n=== Parsing Receipts with Extend AI ===") + + // The SDK retries 429s and 5xx errors with exponential backoff up to MaxAttempts. + client := extendclient.NewClient( + option.WithToken(extendAPIKey), + option.WithMaxAttempts(5), + option.WithHTTPClient(&http.Client{Timeout: 5 * time.Minute}), + ) + config := receiptExtractionConfig() + + fmt.Printf("Processing %d receipts with inline config...\n\n", len(filePaths)) + + results := make([]receiptResult, len(filePaths)) + processOne := func(i int, filePath string) { + fileName := filepath.Base(filePath) + run, err := extractReceipt(ctx, client, config, filePath) + if err != nil { + fmt.Printf(" Failed to parse %s: %v\n", fileName, err) + results[i] = receiptResult{File: fileName, Data: map[string]string{"error": err.Error()}} + return + } + fmt.Printf(" Parsed %s (run: %s)\n", fileName, run.ID) + results[i] = receiptResult{File: fileName, RunID: run.ID, Data: run} + } + + // Process in batches to balance speed and reliability. + for start := 0; start < len(filePaths); start += extractBatchSize { + end := min(start+extractBatchSize, len(filePaths)) + var wg sync.WaitGroup + for i := start; i < end; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + processOne(i, filePaths[i]) + }(i) + } + wg.Wait() + } + + if err := writeResultsJSON(results); err != nil { + return err + } + return writeResultsCSV(results) +} + +func extractReceipt(ctx context.Context, client *extendclient.Client, config *extend.ExtractConfigJSON, filePath string) (*extend.ExtractRun, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + uploaded, err := client.Files.Upload(ctx, namedReader{Reader: file, name: filepath.Base(filePath)}, &extend.FilesUploadRequest{}) + if err != nil { + return nil, fmt.Errorf("upload: %w", err) + } + + // Extract runs synchronously against the inline config using the uploaded file ID. + run, err := client.Extract(ctx, &extend.ExtractRequest{ + Config: config, + File: &extend.ExtractRequestFile{FileFromID: &extend.FileFromID{ID: uploaded.ID}}, + }) + if err != nil { + return nil, fmt.Errorf("extract: %w", err) + } + if run.Status != extend.ProcessorRunStatusProcessed { + msg := string(run.Status) + if run.FailureMessage != nil { + msg += ": " + *run.FailureMessage + } + return nil, fmt.Errorf("extraction did not complete (%s)", msg) + } + return run, nil +} + +func writeResultsJSON(results []receiptResult) error { + jsonPath := filepath.Join(resultsDir, "receipts.json") + data, err := json.MarshalIndent(results, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(jsonPath, data, 0o644); err != nil { + return err + } + fmt.Printf("\nSaved JSON: %s\n", jsonPath) + return nil +} + +func writeResultsCSV(results []receiptResult) error { + csvPath := filepath.Join(resultsDir, "receipts.csv") + file, err := os.Create(csvPath) + if err != nil { + return err + } + defer file.Close() + + writer := csv.NewWriter(file) + if err := writer.Write([]string{ + "file", "vendor_name", "receipt_date", "receipt_number", "total_amount", + "currency", "subtotal", "tax", "payment_method", "line_items_count", + }); err != nil { + return err + } + + for _, result := range results { + output := extractedValue(result.Data) + lineItems, _ := output["line_items"].([]any) + row := []string{ + result.File, + stringValue(output["vendor_name"]), + stringValue(output["receipt_date"]), + stringValue(output["receipt_number"]), + stringValue(nested(output, "total_amount", "amount")), + stringValue(nested(output, "total_amount", "iso_4217_currency_code")), + stringValue(nested(output, "subtotal_amount", "amount")), + stringValue(nested(output, "tax_amount", "amount")), + stringValue(output["payment_method"]), + fmt.Sprint(len(lineItems)), + } + if err := writer.Write(row); err != nil { + return err + } + } + writer.Flush() + if err := writer.Error(); err != nil { + return err + } + fmt.Printf("Saved CSV: %s\n", csvPath) + return nil +} + +// extractedValue returns the extracted field map from a successful run, or an +// empty map for failed results so CSV rows are still emitted. +func extractedValue(data any) map[string]any { + run, ok := data.(*extend.ExtractRun) + if !ok || run.Output == nil || run.Output.ExtractOutputJSON == nil { + return map[string]any{} + } + return run.Output.ExtractOutputJSON.Value +} + +func nested(m map[string]any, keys ...string) any { + var current any = m + for _, key := range keys { + obj, ok := current.(map[string]any) + if !ok { + return nil + } + current = obj[key] + } + return current +} + +func stringValue(v any) string { + if v == nil { + return "" + } + return fmt.Sprint(v) +} diff --git a/python/extend-browserbase/README.md b/python/extend-browserbase/README.md index f64adb5f..ef4082c9 100644 --- a/python/extend-browserbase/README.md +++ b/python/extend-browserbase/README.md @@ -9,7 +9,7 @@ Stagehand is the SDK for browser agents. - **Workflow**: Stagehand navigates the expense portal and clicks each receipt's download link; Browserbase captures downloads. The script polls for the session's download ZIP, extracts files, then optionally sends them to Extend for structured extraction (vendor, date, totals, line items, etc.). - **Download Handling**: Implements retry/polling around Browserbase's Session Downloads API until the ZIP is available. - **Structured Extraction**: Extend AI extraction with inline receipt JSON schema config; results written to `output/results/receipts.json` and `receipts.csv`. -- Docs → [Browserbase Downloads](https://docs.browserbase.com/features/downloads) | [Extend AI](https://docs.extend.app) +- Docs → [Browserbase Downloads](https://docs.browserbase.com/features/downloads) | [Extend AI](https://docs.extend.ai) ## GLOSSARY @@ -20,7 +20,7 @@ Stagehand is the SDK for browser agents. - **Browserbase Downloads**: When files are downloaded during a browser session, Browserbase captures and stores them. Files are retrieved via the Session Downloads API as a ZIP archive. Docs → https://docs.browserbase.com/features/downloads - **Extend AI extraction**: A configurable document extraction pipeline that parses files against a JSON schema and returns structured data. Config can be passed inline or via a saved extractor resource. - Docs → https://docs.extend.app + Docs → https://docs.extend.ai - **Download polling**: Browserbase syncs downloads in real-time; the script retries every 2 seconds until the ZIP is available or a timeout is reached. ## QUICKSTART @@ -72,7 +72,7 @@ Stagehand is the SDK for browser agents. 📚 Stagehand Docs: https://docs.stagehand.dev/v4/first-steps/introduction 📚 Python SDK: https://docs.stagehand.dev/v4/sdk/python 📚 Browserbase Downloads: https://docs.browserbase.com/features/downloads -📚 Extend AI: https://docs.extend.app +📚 Extend AI: https://docs.extend.ai 🎮 Browserbase: https://www.browserbase.com 💡 Try it out: https://www.browserbase.com/playground 🔧 Templates: https://www.browserbase.com/templates diff --git a/typescript/extend-browserbase/README.md b/typescript/extend-browserbase/README.md index c92db039..bb49cb7c 100644 --- a/typescript/extend-browserbase/README.md +++ b/typescript/extend-browserbase/README.md @@ -7,7 +7,7 @@ - **Workflow**: Stagehand navigates the expense portal and clicks each receipt's download link; Browserbase captures downloads. The script polls for the session's download ZIP, extracts files, then optionally sends them to Extend for structured extraction (vendor, date, totals, line items, etc.). - **Download Handling**: Implements retry/polling around Browserbase's Session Downloads API until the ZIP is available. - **Structured Extraction**: Extend AI extraction with inline receipt JSON schema config; results written to `output/results/receipts.json` and `receipts.csv`. -- Docs → [Browserbase Downloads](https://docs.browserbase.com/features/downloads) | [Extend AI](https://docs.extend.app) +- Docs → [Browserbase Downloads](https://docs.browserbase.com/features/downloads) | [Extend AI](https://docs.extend.ai) ## GLOSSARY @@ -18,7 +18,7 @@ - **Browserbase Downloads**: When files are downloaded during a browser session, Browserbase captures and stores them. Files are retrieved via the Session Downloads API as a ZIP archive. Docs → https://docs.browserbase.com/features/downloads - **Extend AI extraction**: A configurable document extraction pipeline that parses files against a JSON schema and returns structured data. Config can be passed inline or via a saved extractor resource. - Docs → https://docs.extend.app + Docs → https://docs.extend.ai - **Download polling**: Browserbase syncs downloads in real-time; the script retries every 2 seconds until the ZIP is available or a timeout is reached. ## QUICKSTART @@ -66,7 +66,7 @@ 📚 Stagehand Docs: https://docs.stagehand.dev/v4/first-steps/introduction 📚 Browserbase Downloads: https://docs.browserbase.com/features/downloads -📚 Extend AI: https://docs.extend.app +📚 Extend AI: https://docs.extend.ai 🎮 Browserbase: https://www.browserbase.com 💡 Try it out: https://www.browserbase.com/playground 🔧 Templates: https://www.browserbase.com/templates