diff --git a/README.md b/README.md index 009f267..927ceda 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A collection of examples for building browser automations with [Intuned](https:/ | [quick-recipes](./typescript-examples/quick-recipes/) | Quick browser automation recipes | | [rpa-example](./typescript-examples/rpa-example/) | Consultation booking automation | | [rpa-auth-example](./typescript-examples/rpa-auth-example/) | Authenticated consultation booking with Auth Sessions | +| [rpa-forms-example](./typescript-examples/rpa-forms-example/) | AI-powered form automation using Stagehand to fill insurance quote forms | | [auth-with-secret-otp](./typescript-examples/auth-with-secret-otp/) | Multi-step authentication with TOTP (Time-based OTP) using secret keys | | [auth-with-email-otp](./typescript-examples/auth-with-email-otp/) | Multi-step authentication with Email-based OTP using Resend API | | [e-commerce-scrapingcourse](./typescript-examples/e-commerce-scrapingcourse/) | E-commerce product scraper with pagination | @@ -35,6 +36,7 @@ A collection of examples for building browser automations with [Intuned](https:/ | [quick-recipes](./python-examples/quick-recipes/) | Quick browser automation recipes | | [rpa-example](./python-examples/rpa-example/) | Consultation booking automation | | [rpa-auth-example](./python-examples/rpa-auth-example/) | Authenticated consultation booking with Auth Sessions | +| [rpa-forms-example](./python-examples/rpa-forms-example/) | AI-powered form automation using Stagehand to fill insurance quote forms |ß | [auth-with-secret-otp](./python-examples/auth-with-secret-otp/) | Multi-step authentication with TOTP (Time-based OTP) verification | | [auth-with-email-otp](./python-examples/auth-with-email-otp/) | Multi-step authentication with Email-based OTP using Resend API | | [e-commerce-scrapingcourse](./python-examples/e-commerce-scrapingcourse/) | E-commerce product scraper with pagination | diff --git a/python-examples/README.md b/python-examples/README.md index 10a3f0d..af0b7b3 100644 --- a/python-examples/README.md +++ b/python-examples/README.md @@ -8,6 +8,7 @@ Intuned sample projects in Python. | [quick-recipes](./quick-recipes/) | Quick browser automation recipes | | [rpa-example](./rpa-example/) | Consultation booking automation | | [rpa-auth-example](./rpa-auth-example/) | Authenticated consultation booking with Auth Sessions | +| [rpa-forms-example](./rpa-forms-example/) | AI-powered form automation using Stagehand to fill insurance quote forms | | [auth-with-email-otp](./auth-with-email-otp/) | Multi-step authentication with Email-based OTP using Resend API | | [auth-with-secret-otp](./auth-with-secret-otp/) | Multi-step authentication with TOTP (Time-based OTP) verification | | [e-commerce-scrapingcourse](./e-commerce-scrapingcourse/) | E-commerce product scraper with pagination | diff --git a/python-examples/rpa-forms-example/.env.example b/python-examples/rpa-forms-example/.env.example new file mode 100644 index 0000000..29b1e20 --- /dev/null +++ b/python-examples/rpa-forms-example/.env.example @@ -0,0 +1 @@ +INTUNED_API_KEY=your_api_key_here \ No newline at end of file diff --git a/python-examples/rpa-forms-example/.gitignore b/python-examples/rpa-forms-example/.gitignore new file mode 100644 index 0000000..ec933ac --- /dev/null +++ b/python-examples/rpa-forms-example/.gitignore @@ -0,0 +1,51 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Production builds +/build +/dist +/.next/ +/out/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime/temporary files +.cache +.parcel-cache +*.tsbuildinfo + +# Coverage +/coverage +.nyc_output + +# Python +__pycache__/ +*.py[cod] +*.pyc +.Python +build/ +*.egg-info/ +.venv/ +venv/ +.env + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ diff --git a/python-examples/rpa-forms-example/.parameters/api/insurance-form-filler/default.json b/python-examples/rpa-forms-example/.parameters/api/insurance-form-filler/default.json new file mode 100644 index 0000000..69e9441 --- /dev/null +++ b/python-examples/rpa-forms-example/.parameters/api/insurance-form-filler/default.json @@ -0,0 +1,39 @@ +{ + "metadata": { + "site": "https://www.erieinsurance.com/", + "insurance_type": "auto" + }, + "applicant": { + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "04/12/1992", + "gender": "male", + "marital_status": "single", + "accident_prevention_course": true, + "email": "john.doe@example.com", + "phone_number": "555-123-4567", + "is_cell_phone": true, + "can_text": true, + "preferred_name": "Johnny", + "home_multi_policy_discount": false, + "currently_has_auto_insurance": false, + "coverage_effective_date": "01/01/2026" + }, + "address": { + "street_line1": "350 W 42nd St", + "city": "New York", + "state": "NY", + "zip_code": "10036" + }, + "vehicle": { + "vehicle_type": "Automobile", + "year": 2021, + "make": "Toyota", + "model": "Camry Le", + "primary_use": "Pleasure", + "annual_mileage": 12000, + "days_driven_per_week": 5, + "miles_driven_one_way": 15, + "model_details": "Car04 4D,2.5,4" + } + } \ No newline at end of file diff --git a/python-examples/rpa-forms-example/.parameters/api/insurance-form-filler/honda.json b/python-examples/rpa-forms-example/.parameters/api/insurance-form-filler/honda.json new file mode 100644 index 0000000..1c4c22f --- /dev/null +++ b/python-examples/rpa-forms-example/.parameters/api/insurance-form-filler/honda.json @@ -0,0 +1,40 @@ +{ + "metadata": { + "site": "https://www.erieinsurance.com/", + "insurance_type": "auto" + }, + "applicant": { + "first_name": "Michael", + "last_name": "Smith", + "date_of_birth": "09/18/1988", + "gender": "male", + "marital_status": "married", + "accident_prevention_course": false, + "email": "michael.smith@example.com", + "phone_number": "555-987-6543", + "is_cell_phone": true, + "can_text": true, + "preferred_name": "Mike", + "home_multi_policy_discount": true, + "currently_has_auto_insurance": true, + "coverage_effective_date": "02/15/2026" + }, + "address": { + "street_line1": "245 Market St", + "city": "Philadelphia", + "state": "PA", + "zip_code": "19106" + }, + "vehicle": { + "vehicle_type": "Automobile", + "year": 2019, + "make": "Honda", + "model": "Civic EX", + "primary_use": "Farm", + "annual_mileage": 14000, + "days_driven_per_week": 5, + "miles_driven_one_way": 12, + "model_details": "Car02 2D,1.5,4" + } + } + \ No newline at end of file diff --git a/python-examples/rpa-forms-example/Intuned.jsonc b/python-examples/rpa-forms-example/Intuned.jsonc new file mode 100644 index 0000000..fce0f01 --- /dev/null +++ b/python-examples/rpa-forms-example/Intuned.jsonc @@ -0,0 +1,30 @@ +// For more information, see our Intuned settings reference +// https://docs.intunedhq.com/docs/05-references/intuned-json +{ + "projectName": "insurance-form-filler", + "apiAccess": { + "enabled": true + }, + "authSessions": { + "enabled": false + }, + "replication": { + "maxConcurrentRequests": 1, + "size": "standard" + }, + "metadata": { + "template": { + "name": "insurance-form-filler", + "description": "Insurance form filler using Stagehand in Intuned", + "tags": [ + "Stagehand", + "AI", + "RPA", + "Forms", + "intuned-runtime-sdk", + "intuned-runtime-sdk-setup-context-hook" + ] + } + } +} + diff --git a/python-examples/rpa-forms-example/README.md b/python-examples/rpa-forms-example/README.md new file mode 100644 index 0000000..881e417 --- /dev/null +++ b/python-examples/rpa-forms-example/README.md @@ -0,0 +1,143 @@ +# rpa-forms-example Intuned project + +AI-powered form automation using Stagehand to automatically fill out insurance quote forms with applicant, address, and object information. + +## Run on Intuned + +Open this project in Intuned by clicking the button below. + +[![Run on Intuned](https://cdn1.intuned.io/button.svg)](https://app.intuned.io?repo=https://github.com/Intuned/cookbook/tree/main/python-examples/rpa-forms-example) + +## Getting Started + +To get started developing browser automation projects with Intuned, check out our [concepts and terminology](https://docs.intunedhq.com/docs/getting-started/conceptual-guides/core-concepts#runs%3A-executing-your-automations). + + +## Development + +> **_NOTE:_** All commands support `--help` flag to get more information about the command and its arguments and options. + +### Install dependencies +```bash +uv sync +``` + +After installing dependencies, `intuned` command should be available in your environment. + +### Run an API +```bash +uv run intuned run api insurance-form-filler .parameters/api/insurance-form-filler/default.json +``` + +### Deploy project +```bash +uv run intuned deploy +``` + + + + +### `intuned-browser`: Intuned Browser SDK + +This project uses Intuned browser SDK. For more information, check out the [Intuned Browser SDK documentation](https://docs.intunedhq.com/automation-sdks/overview). + + + + +## Project Structure +The project structure is as follows: +``` +/ +├── api/ # Your API endpoints +│ └── insurance_form_filler.py # Main automation API for filling insurance forms +├── hooks/ # Setup hooks +│ └── setup_context.py # Browser context setup hook +├── utils/ # Utility modules +│ └── types_and_schemas.py # Pydantic models for type validation +├── Intuned.json # Intuned project configuration file +└── pyproject.toml # Python project dependencies +``` + +### How It Works + +1. **insurance_form_filler.py** - Uses Stagehand's AI-powered automation to navigate to the insurance website, select insurance type, and fill out multi-step forms including: + - Applicant information (name, date of birth, gender, marital status) + - Contact details (email, phone, text preferences) + - Address information (street, city, state, zip code) + - Vehicle details (type, year, make, model, usage) + - Additional preferences (multi-policy discount, current insurance status, coverage effective date) + + The automation uses natural language instructions to interact with form elements, making it resilient to UI changes. + + +## `Intuned.jsonc` Reference +```jsonc +{ + // Your Intuned workspace ID. + // Optional - If not provided here, it must be supplied via the `--workspace-id` flag during deployment. + "workspaceId": "your_workspace_id", + + // The name of your Intuned project. + // Optional - If not provided here, it must be supplied via the command line when deploying. + "projectName": "your_project_name", + + // Replication settings + "replication": { + // The maximum number of concurrent executions allowed via Intuned API. This does not affect jobs. + // A number of machines equal to this will be allocated to handle API requests. + // Not applicable if api access is disabled. + "maxConcurrentRequests": 1, + + // The machine size to use for this project. This is applicable for both API requests and jobs. + // "standard": Standard machine size (6 shared vCPUs, 2GB RAM) + // "large": Large machine size (8 shared vCPUs, 4GB RAM) + // "xlarge": Extra large machine size (1 performance vCPU, 8GB RAM) + "size": "standard" + }, + + // Auth session settings + "authSessions": { + // Whether auth sessions are enabled for this project. + // If enabled, "auth-sessions/check.ts" API must be implemented to validate the auth session. + "enabled": true, + + // Whether to save Playwright traces for auth session runs. + "saveTraces": false, + + // The type of auth session to use. + // "API" type requires implementing "auth-sessions/create.ts" API to create/recreate the auth session programmatically. + // "MANUAL" type uses a recorder to manually create the auth session. + "type": "API", + + + // Recorder start URL for the recorder to navigate to when creating the auth session. + // Required if "type" is "MANUAL". Not used if "type" is "API". + "startUrl": "https://example.com/login", + + // Recorder finish URL for the recorder. Once this URL is reached, the recorder stops and saves the auth session. + // Required if "type" is "MANUAL". Not used if "type" is "API". + "finishUrl": "https://example.com/dashboard", + + // Recorder browser mode + // "fullscreen": Launches the browser in fullscreen mode. + // "kiosk": Launches the browser in kiosk mode (no address bar, no navigation controls). + // Only applicable for "MANUAL" type. + "browserMode": "fullscreen" + }, + + // API access settings + "apiAccess": { + // Whether to enable consumption through Intuned API. If this is false, the project can only be consumed through jobs. + // This is required for projects that use auth sessions. + "enabled": true + }, + + // Whether to run the deployed API in a headful browser. Running in headful can help with some anti-bot detections. However, it requires more resources and may work slower or crash if the machine size is "standard". + "headful": false, + + // The region where your Intuned project is hosted. + // For a list of available regions, contact support or refer to the documentation. + // Optional - Default: "us" + "region": "us" +} +``` diff --git a/python-examples/rpa-forms-example/api/insurance-form-filler.py b/python-examples/rpa-forms-example/api/insurance-form-filler.py new file mode 100644 index 0000000..cdc11ad --- /dev/null +++ b/python-examples/rpa-forms-example/api/insurance-form-filler.py @@ -0,0 +1,207 @@ +from stagehand import Stagehand +from playwright.async_api import Page +from intuned_runtime import attempt_store, get_ai_gateway_config +from utils.types_and_schemas import ListParameters + + +class InvalidActionError(Exception): + pass + + +async def perform_action(page: Page, instruction: str) -> None: + for _ in range(3): + action = await page.observe(instruction) + if action: + await page.act(action[0]) + await page.wait_for_load_state("domcontentloaded") + await page.wait_for_timeout(2000) + return + else: + await page.wait_for_timeout(2000) + raise InvalidActionError(f"Could not find action for instruction: {instruction}") + + +async def automation(page: Page, params: ListParameters, *args: ..., **kwargs: ...): + base_url, api_key = get_ai_gateway_config() + cdp_url = attempt_store.get("cdp_url") + + # Initialize Stagehand with act/extract/observe capabilities + stagehand = Stagehand( + env="LOCAL", + local_browser_launch_options=dict( + cdp_url=cdp_url, viewport=dict(width=1280, height=800) + ), + model_api_key=api_key, + model_client_options={ + "baseURL": base_url, + }, + ) + await stagehand.init() + print("\nInitialized 🤘 Stagehand") + await page.set_viewport_size({"width": 1280, "height": 800}) + + params = ListParameters.model_validate(params) + + site = params.metadata.site + await stagehand.page.goto(site) + + # --- Object type selection --- + await perform_action( + stagehand.page, + f"Choose the {params.metadata.insurance_type} option from the insurance type dropdown", + ) + + # --- ZIP entry --- + await perform_action( + stagehand.page, + f"Fill in the zip code {params.address.zip_code} in the zip code field", + ) + + await perform_action(stagehand.page, "Click the Get a quote button") + await page.wait_for_selector("#mainContent") + # --- Name --- + await perform_action( + stagehand.page, + f"Fill in the first name {params.applicant.first_name} in the first name field", + ) + await perform_action( + stagehand.page, + f"Fill in the last name {params.applicant.last_name} in the last name field", + ) + # --- DOB --- + await perform_action( + stagehand.page, + f"Fill in the date of birth {params.applicant.date_of_birth} in the date of birth field", + ) + + # --- Address --- + await perform_action( + stagehand.page, + f"Fill in the address {params.address.street_line1} in the street address field", + ) + await perform_action( + stagehand.page, + f"Fill in the city {params.address.city} in the city field", + ) + await perform_action( + stagehand.page, + f"select the state {params.address.state} from the state dropdown", + ) + await perform_action( + stagehand.page, + f"Fill in the zip code {params.address.zip_code} in the zip code field", + ) + await perform_action(stagehand.page, "Click the Continue button") + # --- Vehicle --- + await perform_action( + stagehand.page, + f"Choose the {params.vehicle.vehicle_type} option from the dropdown", + ) + await perform_action( + stagehand.page, + f"Select the year {params.vehicle.year} from the year dropdown", + ) + await perform_action( + stagehand.page, + f"Select the make {params.vehicle.make} from the make dropdown", + ) + await perform_action( + stagehand.page, + f"Select the model {params.vehicle.model} from the model dropdown", + ) + await perform_action( + stagehand.page, + f"Fill in the model details {params.vehicle.model_details} in the model details field if not already filled", + ) + await perform_action(stagehand.page, "Click the Continue button") + + await perform_action(stagehand.page, "Click the Continue button") + + # --- Fill driver information --- + await perform_action( + stagehand.page, + f"Fill in the first name {params.applicant.first_name} in the first name field if the first name field is empty", + ) + await perform_action( + stagehand.page, + f"Fill in the last name {params.applicant.last_name} in the last name field if the last name field is empty", + ) + await perform_action( + stagehand.page, + f"click the {params.applicant.gender} radio button", + ) + await perform_action( + stagehand.page, + f"choose the {params.applicant.marital_status} option from the marital status dropdown", + ) + if params.applicant.accident_prevention_course: + await perform_action(stagehand.page, "Click the Yes radio button.") + else: + await perform_action(stagehand.page, "Click the No radio button.") + await perform_action(stagehand.page, "Click the Continue button") + await perform_action(stagehand.page, "Click the Continue button") + + # --- Final details --- + await perform_action( + stagehand.page, f"Fill in the email {params.applicant.email} in the email field" + ) + await perform_action( + stagehand.page, + f"Fill in the phone number {params.applicant.phone_number} in the phone number field", + ) + if params.applicant.is_cell_phone: + await perform_action( + stagehand.page, + "Click the Yes radio button in the Is this a cell phone? field", + ) + else: + await perform_action( + stagehand.page, + "Click the No radio button in the Is this a cell phone? field", + ) + if params.applicant.can_text: + await perform_action( + stagehand.page, + "Click the Yes radio button in the Can an ERIE Agent text you about this quote? field", + ) + else: + await perform_action( + stagehand.page, + "Click the No radio button in the Can an ERIE Agent text you about this quote? field", + ) + if params.applicant.preferred_name: + await perform_action( + stagehand.page, + f"Fill in the preferred name {params.applicant.preferred_name} in the preferred name field", + ) + if params.applicant.home_multi_policy_discount: + await perform_action( + stagehand.page, + "Click the Yes radio button in the Would you like our Home Multi-Policy Discount applied to your quote? field", + ) + else: + await perform_action( + stagehand.page, + "Click the No radio button in the Would you like our Home Multi-Policy Discount applied to your quote? field", + ) + if params.applicant.currently_has_auto_insurance: + await perform_action( + stagehand.page, + "Click the Yes radio button in the Do you currently have auto insurance? field", + ) + else: + await perform_action( + stagehand.page, + "Click the No radio button in the Do you currently have auto insurance? field", + ) + await perform_action( + stagehand.page, + f"Fill in the coverage effective date {params.applicant.coverage_effective_date} in the coverage effective date field", + ) + await perform_action(stagehand.page, "Click the Continue button") + await perform_action(stagehand.page, "Click the Submit Quote to Agent button") + result = await stagehand.page.extract("Extract the confirmation message") + if result: + return result + else: + raise InvalidActionError("Could not find confirmation message") diff --git a/python-examples/rpa-forms-example/hooks/setup_context.py b/python-examples/rpa-forms-example/hooks/setup_context.py new file mode 100644 index 0000000..de57852 --- /dev/null +++ b/python-examples/rpa-forms-example/hooks/setup_context.py @@ -0,0 +1,5 @@ +from intuned_runtime import attempt_store + + +async def setup_context(*, api_name: str, api_parameters: str, cdp_url: str): + attempt_store.set("cdp_url", cdp_url) diff --git a/python-examples/rpa-forms-example/pyproject.toml b/python-examples/rpa-forms-example/pyproject.toml new file mode 100644 index 0000000..3fdfa2b --- /dev/null +++ b/python-examples/rpa-forms-example/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "insurace-form-filler" +version = "0.0.1" +description = "" +authors = [{ name = "Intuned", email = "service@intunedhq.com" }] +requires-python = ">=3.12,<3.13" +readme = "README.md" +keywords = [ + "Python", + "AI", + "Stagehand", + "intuned-browser-sdk", + "intuned-runtime-sdk", + "intuned-runtime-sdk-setup-context-hook", +] +dependencies = [ + "playwright==1.56", + "intuned-runtime==1.3.14", + "stagehand==0.5.3", + "intuned-browser==0.1.10", +] + +[tool.uv] +package = false diff --git a/python-examples/rpa-forms-example/utils/types_and_schemas.py b/python-examples/rpa-forms-example/utils/types_and_schemas.py new file mode 100644 index 0000000..12c4c77 --- /dev/null +++ b/python-examples/rpa-forms-example/utils/types_and_schemas.py @@ -0,0 +1,128 @@ +from typing import Optional, Literal +from pydantic import BaseModel, Field + + +# ---------- Metadata ---------- + + +class Metadata(BaseModel): + site: str = Field(..., format="uri") + insurance_type: Literal[ + "auto", "homeowners", "renters", "motorcycle", "boat", "commercial_auto" + ] + + +# ---------- Applicant ---------- + + +class Applicant(BaseModel): + first_name: str = Field( + ..., min_length=1, description="The first name of the applicant" + ) + last_name: str = Field( + ..., min_length=1, description="The last name of the applicant" + ) + date_of_birth: str = Field( + ..., format="date", description="The date of birth of the applicant" + ) + gender: Literal["male", "female", "other"] = Field( + ..., description="The gender of the applicant" + ) + marital_status: Literal["single", "married", "divorced", "widowed"] = Field( + ..., description="The marital status of the applicant" + ) + accident_prevention_course: Optional[bool] = Field( + default=False, + description="Whether the applicant has completed an accident prevention course", + ) + email: str = Field( + ..., format="email", description="The email address of the applicant" + ) + phone_number: str = Field(..., description="The phone number of the applicant") + is_cell_phone: bool = Field( + ..., description="Whether the phone number is a cell phone" + ) + can_text: bool = Field( + ..., description="Can an ERIE Agent text you about this quote?" + ) + preferred_name: Optional[str] = Field( + default=None, description="The preferred name of the applicant" + ) + home_multi_policy_discount: bool = Field( + ..., + description="Would you like our Home Multi-Policy Discount applied to your quote?", + ) + currently_has_auto_insurance: bool = Field( + ..., description="Do you currently have auto insurance?" + ) + coverage_effective_date: str = Field( + ..., + pattern=r"^\d{2}/\d{2}/\d{4}$", + description="When does coverage need to be effective? (mm/dd/yyyy format)", + ) + + +# ---------- Address ---------- + + +class Address(BaseModel): + street_line1: str = Field( + ..., min_length=1, description="The street address of the applicant" + ) + street_line2: Optional[str] = None # Apt / Unit + city: str = Field(..., min_length=1, description="The city of the applicant") + state: str = Field( + ..., min_length=2, max_length=2, description="The state of the applicant" + ) + zip_code: str = Field( + ..., pattern=r"^\d{5}$", description="The zip code of the applicant" + ) + + +# ---------- Vehicle ---------- + + +class Vehicle(BaseModel): + vehicle_type: Literal[ + "Automobile", + "Travel Trailer", + "ATV", + "Utility Trailer", + "Snowmobile", + "Motor Home", + "Camper", + "Moped", + "Trail Bike", + "Dune Buggy", + "Mini Bike", + "Golf Cart", + "Recreational Trailer", + ] = Field(..., description="The type of vehicle") + year: int = Field(..., description="The year of the vehicle") + make: str = Field(..., min_length=1, description="The make of the vehicle") + model: str = Field(..., min_length=1, description="The model of the vehicle") + primary_use: Literal["Farm", "Business", "Pleasure", "Work/School"] | None = Field( + default=None, description="The primary use of the vehicle" + ) + annual_mileage: int | None = Field( + default=None, description="The annual mileage of the vehicle" + ) + days_driven_per_week: int | None = Field( + default=None, description="The number of days driven per week" + ) + miles_driven_one_way: int | None = Field( + default=None, description="The number of miles driven one way" + ) + model_details: str | None = Field( + ..., description="The details of the model of the vehicle" + ) + + +# ---------- Root Model ---------- + + +class ListParameters(BaseModel): + metadata: Metadata + applicant: Applicant + address: Address + vehicle: Vehicle diff --git a/python-examples/stagehand/hooks/setup_context.py b/python-examples/stagehand/hooks/setup_context.py index 3a3a003..23ff236 100644 --- a/python-examples/stagehand/hooks/setup_context.py +++ b/python-examples/stagehand/hooks/setup_context.py @@ -3,6 +3,4 @@ async def setup_context(*, api_name: str, api_parameters: str, cdp_url: str): attempt_store.set("cdp_url", cdp_url) - - - + \ No newline at end of file diff --git a/typescript-examples/README.md b/typescript-examples/README.md index 29d912d..9b8aad6 100644 --- a/typescript-examples/README.md +++ b/typescript-examples/README.md @@ -8,6 +8,7 @@ Intuned sample projects in TypeScript. | [quick-recipes](./quick-recipes/) | Quick browser automation recipes | | [rpa-example](./rpa-example/) | Consultation booking automation | | [rpa-auth-example](./rpa-auth-example/) | Authenticated consultation booking with Auth Sessions | +| [rpa-forms-example](./rpa-forms-example/) | AI-powered form automation using Stagehand to fill insurance quote forms | | [auth-with-secret-otp](./auth-with-secret-otp/) | Multi-step authentication with TOTP (Time-based OTP) verification | | [auth-with-email-otp](./auth-with-email-otp/) | Multi-step authentication with Email-based OTP using Resend API | | [e-commerce-scrapingcourse](./e-commerce-scrapingcourse/) | E-commerce product scraper with pagination | diff --git a/typescript-examples/rpa-forms-example/.env.example b/typescript-examples/rpa-forms-example/.env.example new file mode 100644 index 0000000..29b1e20 --- /dev/null +++ b/typescript-examples/rpa-forms-example/.env.example @@ -0,0 +1 @@ +INTUNED_API_KEY=your_api_key_here \ No newline at end of file diff --git a/typescript-examples/rpa-forms-example/.parameters/api/insurance-form-filler/default.json b/typescript-examples/rpa-forms-example/.parameters/api/insurance-form-filler/default.json new file mode 100644 index 0000000..958797a --- /dev/null +++ b/typescript-examples/rpa-forms-example/.parameters/api/insurance-form-filler/default.json @@ -0,0 +1,39 @@ +{ + "metadata": { + "site": "https://www.erieinsurance.com/", + "insurance_type": "auto" + }, + "applicant": { + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "04/12/1992", + "gender": "male", + "marital_status": "single", + "accident_prevention_course": true, + "email": "john.doe@example.com", + "phone_number": "555-123-4567", + "is_cell_phone": true, + "can_text": true, + "preferred_name": "Johnny", + "home_multi_policy_discount": false, + "currently_has_auto_insurance": false, + "coverage_effective_date": "01/01/2026" + }, + "address": { + "street_line1": "350 W 42nd St", + "city": "New York", + "state": "NY", + "zip_code": "10036" + }, + "vehicle": { + "vehicle_type": "Automobile", + "year": 2021, + "make": "Toyota", + "model": "Camry Le", + "primary_use": "Pleasure", + "annual_mileage": 12000, + "days_driven_per_week": 5, + "miles_driven_one_way": 15, + "model_details": "Car04 4D,2.5,4" + } +} \ No newline at end of file diff --git a/typescript-examples/rpa-forms-example/.parameters/api/insurance-form-filler/honda.json b/typescript-examples/rpa-forms-example/.parameters/api/insurance-form-filler/honda.json new file mode 100644 index 0000000..1c4c22f --- /dev/null +++ b/typescript-examples/rpa-forms-example/.parameters/api/insurance-form-filler/honda.json @@ -0,0 +1,40 @@ +{ + "metadata": { + "site": "https://www.erieinsurance.com/", + "insurance_type": "auto" + }, + "applicant": { + "first_name": "Michael", + "last_name": "Smith", + "date_of_birth": "09/18/1988", + "gender": "male", + "marital_status": "married", + "accident_prevention_course": false, + "email": "michael.smith@example.com", + "phone_number": "555-987-6543", + "is_cell_phone": true, + "can_text": true, + "preferred_name": "Mike", + "home_multi_policy_discount": true, + "currently_has_auto_insurance": true, + "coverage_effective_date": "02/15/2026" + }, + "address": { + "street_line1": "245 Market St", + "city": "Philadelphia", + "state": "PA", + "zip_code": "19106" + }, + "vehicle": { + "vehicle_type": "Automobile", + "year": 2019, + "make": "Honda", + "model": "Civic EX", + "primary_use": "Farm", + "annual_mileage": 14000, + "days_driven_per_week": 5, + "miles_driven_one_way": 12, + "model_details": "Car02 2D,1.5,4" + } + } + \ No newline at end of file diff --git a/typescript-examples/rpa-forms-example/Intuned.jsonc b/typescript-examples/rpa-forms-example/Intuned.jsonc new file mode 100644 index 0000000..fce0f01 --- /dev/null +++ b/typescript-examples/rpa-forms-example/Intuned.jsonc @@ -0,0 +1,30 @@ +// For more information, see our Intuned settings reference +// https://docs.intunedhq.com/docs/05-references/intuned-json +{ + "projectName": "insurance-form-filler", + "apiAccess": { + "enabled": true + }, + "authSessions": { + "enabled": false + }, + "replication": { + "maxConcurrentRequests": 1, + "size": "standard" + }, + "metadata": { + "template": { + "name": "insurance-form-filler", + "description": "Insurance form filler using Stagehand in Intuned", + "tags": [ + "Stagehand", + "AI", + "RPA", + "Forms", + "intuned-runtime-sdk", + "intuned-runtime-sdk-setup-context-hook" + ] + } + } +} + diff --git a/typescript-examples/rpa-forms-example/README.md b/typescript-examples/rpa-forms-example/README.md new file mode 100644 index 0000000..630f3b9 --- /dev/null +++ b/typescript-examples/rpa-forms-example/README.md @@ -0,0 +1,161 @@ +# rpa-forms-example Intuned project + +AI-powered form automation using Stagehand to automatically fill out insurance quote forms with applicant, address, and object information. + +## Run on Intuned + +Open this project in Intuned by clicking the button below. + +[![Run on Intuned](https://cdn1.intuned.io/button.svg)](https://app.intuned.io?repo=https://github.com/Intuned/cookbook/tree/main/typescript-examples/rpa-forms-example) + +## Getting Started + +To get started developing browser automation projects with Intuned, check out our [concepts and terminology](https://docs.intunedhq.com/docs/getting-started/conceptual-guides/core-concepts#runs%3A-executing-your-automations). + +### Prerequisites + +This project uses Stagehand for AI-powered form automation. You'll need to set the `OPENAI_API_KEY` environment variable: + +```bash +export OPENAI_API_KEY=your_openai_api_key_here +``` + +## Development + +> **_NOTE:_** All commands support `--help` flag to get more information about the command and its arguments and options. + +### Install Dependencies +```bash +# npm +npm install + +# yarn +yarn +``` + +### Run the API Locally +```bash +# npm +npm run intuned run api insurance-form-filler .parameters/api/insurance-form-filler/default.json + +# yarn +yarn intuned run api insurance-form-filler .parameters/api/insurance-form-filler/default.json +``` + +### Deploy to Intuned +```bash +# npm +npm run intuned deploy + +# yarn +yarn intuned deploy +``` + +### `intuned-browser`: Intuned Browser SDK + +This project uses Intuned browser SDK. For more information, check out the [Intuned Browser SDK documentation](https://docs.intunedhq.com/automation-sdks/overview). + +### `intuned-runtime`: Intuned Runtime SDK + +All intuned projects use the Intuned runtime SDK. It also exposes some helpers for nested scheduling and auth sessions. This project uses some of these helpers. For more information, check out the documentation coming soon. + +This project uses the `setupContext` hook from the Intuned runtime SDK. This hook is used to set up the browser context and page for the project. For more information, check out the documentation coming soon. + +## Project Structure +The project structure is as follows: +``` +/ +├── api/ # Your API endpoints +│ └── insurance-form-filler.ts # Main automation API for filling insurance forms +├── hooks/ # Setup hooks +│ └── setupContext.ts # Browser context setup hook +├── utils/ # Utility modules +│ └── typesAndSchemas.ts # Zod schemas for type validation +├── Intuned.jsonc # Intuned project configuration file +└── package.json # Node.js project dependencies +``` + +### How It Works + +1. **insurance-form-filler.ts** - Uses Stagehand's AI-powered automation to navigate to the insurance website, select insurance type, and fill out multi-step forms including: + - Applicant information (name, date of birth, gender, marital status) + - Contact details (email, phone, text preferences) + - Address information (street, city, state, zip code) + - Vehicle details (type, year, make, model, usage) + - Additional preferences (multi-policy discount, current insurance status, coverage effective date) + + The automation uses natural language instructions to interact with form elements, making it resilient to UI changes. + + +## `Intuned.jsonc` Reference +```jsonc +{ + // Your Intuned workspace ID. + // Optional - If not provided here, it must be supplied via the `--workspace-id` flag during deployment. + "workspaceId": "your_workspace_id", + + // The name of your Intuned project. + // Optional - If not provided here, it must be supplied via the command line when deploying. + "projectName": "your_project_name", + + // Replication settings + "replication": { + // The maximum number of concurrent executions allowed via Intuned API. This does not affect jobs. + // A number of machines equal to this will be allocated to handle API requests. + // Not applicable if api access is disabled. + "maxConcurrentRequests": 1, + + // The machine size to use for this project. This is applicable for both API requests and jobs. + // "standard": Standard machine size (6 shared vCPUs, 2GB RAM) + // "large": Large machine size (8 shared vCPUs, 4GB RAM) + // "xlarge": Extra large machine size (1 performance vCPU, 8GB RAM) + "size": "standard" + }, + + // Auth session settings + "authSessions": { + // Whether auth sessions are enabled for this project. + // If enabled, "auth-sessions/check.ts" API must be implemented to validate the auth session. + "enabled": true, + + // Whether to save Playwright traces for auth session runs. + "saveTraces": false, + + // The type of auth session to use. + // "API" type requires implementing "auth-sessions/create.ts" API to create/recreate the auth session programmatically. + // "MANUAL" type uses a recorder to manually create the auth session. + "type": "API", + + + // Recorder start URL for the recorder to navigate to when creating the auth session. + // Required if "type" is "MANUAL". Not used if "type" is "API". + "startUrl": "https://example.com/login", + + // Recorder finish URL for the recorder. Once this URL is reached, the recorder stops and saves the auth session. + // Required if "type" is "MANUAL". Not used if "type" is "API". + "finishUrl": "https://example.com/dashboard", + + // Recorder browser mode + // "fullscreen": Launches the browser in fullscreen mode. + // "kiosk": Launches the browser in kiosk mode (no address bar, no navigation controls). + // Only applicable for "MANUAL" type. + "browserMode": "fullscreen" + }, + + // API access settings + "apiAccess": { + // Whether to enable consumption through Intuned API. If this is false, the project can only be consumed through jobs. + // This is required for projects that use auth sessions. + "enabled": true + }, + + // Whether to run the deployed API in a headful browser. Running in headful can help with some anti-bot detections. However, it requires more resources and may work slower or crash if the machine size is "standard". + "headful": false, + + // The region where your Intuned project is hosted. + // For a list of available regions, contact support or refer to the documentation. + // Optional - Default: "us" + "region": "us" +} +``` + diff --git a/typescript-examples/rpa-forms-example/api/insurance-form-filler.ts b/typescript-examples/rpa-forms-example/api/insurance-form-filler.ts new file mode 100644 index 0000000..cac0398 --- /dev/null +++ b/typescript-examples/rpa-forms-example/api/insurance-form-filler.ts @@ -0,0 +1,289 @@ +import { Stagehand, AISdkClient } from "@browserbasehq/stagehand"; +import { createOpenAI } from "@ai-sdk/openai"; +import type { Page, BrowserContext } from "playwright"; +import { attemptStore, getAiGatewayConfig } from "@intuned/runtime"; +import { listParametersSchema, ListParameters } from "../utils/typesAndSchemas"; + +class InvalidActionError extends Error { + constructor(message: string) { + super(message); + this.name = "InvalidActionError"; + } +} + +async function getWebSocketUrl(cdpUrl: string): Promise { + const versionUrl = cdpUrl.endsWith("/") + ? `${cdpUrl}json/version` + : `${cdpUrl}/json/version`; + const response = await fetch(versionUrl); + const data = await response.json(); + return data.webSocketDebuggerUrl; +} + +async function performAction( + stagehand: Stagehand, + page: Page, + instruction: string +): Promise { + for (let i = 0; i < 3; i++) { + const action = await stagehand.observe(instruction); + if (action && action.length > 0) { + await stagehand.act(action[0]); + await page.waitForLoadState("domcontentloaded"); + await page.waitForTimeout(2000); + return; + } else { + await page.waitForTimeout(2000); + } + } + throw new InvalidActionError( + `Could not find action for instruction: ${instruction} after 3 attempts` + ); +} + +export default async function handler( + params: ListParameters, + page: Page, + _context: BrowserContext +) { + // Get AI gateway config for Stagehand + const { baseUrl, apiKey } = await getAiGatewayConfig(); + const cdpUrl = attemptStore.get("cdpUrl") as string; + const webSocketUrl = await getWebSocketUrl(cdpUrl); + + // Create AI SDK provider with Intuned's AI gateway + const openai = createOpenAI({ + apiKey, + baseURL: baseUrl, + }); + + const llmClient = new AISdkClient({ + model: openai("gpt-5-mini"), + }); + + // Initialize Stagehand with act/extract/observe capabilities + const stagehand = new Stagehand({ + env: "LOCAL", + localBrowserLaunchOptions: { + cdpUrl: webSocketUrl, + viewport: { width: 1280, height: 800 }, + }, + llmClient, + logger: console.log, + }); + await stagehand.init(); + console.log("\nInitialized 🤘 Stagehand"); + // Validate parameters + await page.setViewportSize({ width: 1280, height: 800 }); + const validatedParams = listParametersSchema.parse(params); + + const { metadata, applicant, address, vehicle } = validatedParams; + // Navigate to site + await page.goto(metadata.site); + + // --- Object type selection --- + await performAction( + stagehand, + page, + `Choose the ${metadata.insurance_type} option from the insurance type dropdown if not chosen` + ); + + // --- ZIP entry --- + await performAction( + stagehand, + page, + `Fill in the zip code ${address.zip_code} in the zip code field` + ); + + await performAction(stagehand, page, "Click the Get a quote button"); + await page.waitForSelector("#mainContent"); + + // --- Name --- + await performAction( + stagehand, + page, + `Fill in the first name ${applicant.first_name} in the first name field` + ); + await performAction( + stagehand, + page, + `Fill in the last name ${applicant.last_name} in the last name field` + ); + + // --- DOB --- + await performAction( + stagehand, + page, + `Fill in the date of birth ${applicant.date_of_birth} in the date of birth field` + ); + + // --- Address --- + await performAction( + stagehand, + page, + `Fill in the address ${address.street_line1} in the street address field` + ); + await performAction( + stagehand, + page, + `Fill in the city ${address.city} in the city field` + ); + await performAction( + stagehand, + page, + `select the state ${address.state} from the state dropdown` + ); + await performAction( + stagehand, + page, + `Fill in the zip code ${address.zip_code} in the zip code field` + ); + await performAction(stagehand, page, "Click the Continue button"); + + // --- Vehicle --- + await performAction( + stagehand, + page, + `Choose the ${vehicle.vehicle_type} option from the dropdown if not chosen` + ); + await performAction( + stagehand, + page, + `Select the year ${vehicle.year} from the year dropdown` + ); + await performAction( + stagehand, + page, + `Select the make ${vehicle.make} from the make dropdown` + ); + await performAction( + stagehand, + page, + `Select the model ${vehicle.model} from the model dropdown` + ); + await performAction( + stagehand, + page, + `Fill in the model details ${vehicle.model_details} in the model details field if not already filled` + ); + await performAction(stagehand, page, "Click the Continue button"); + await performAction(stagehand, page, "Click the Continue button"); + + // --- Fill driver information --- + await performAction( + stagehand, + page, + `Fill in the first name ${applicant.first_name} in the first name field if the first name field is empty` + ); + await performAction( + stagehand, + page, + `Fill in the last name ${applicant.last_name} in the last name field if the last name field is empty` + ); + await performAction( + stagehand, + page, + `click the ${applicant.gender} radio button` + ); + await performAction( + stagehand, + page, + `choose the ${applicant.marital_status} option from the marital status dropdown` + ); + if (applicant.accident_prevention_course) { + await performAction(stagehand, page, "Click the Yes radio button."); + } else { + await performAction(stagehand, page, "Click the No radio button."); + } + await performAction(stagehand, page, "Click the Continue button"); + await performAction(stagehand, page, "Click the Continue button"); + + // --- Final details --- + await performAction( + stagehand, + page, + `Fill in the email ${applicant.email} in the email field` + ); + await performAction( + stagehand, + page, + `Fill in the phone number ${applicant.phone_number} in the phone number field` + ); + if (applicant.is_cell_phone) { + await performAction( + stagehand, + page, + "Click the Yes radio button in the Is this a cell phone? field" + ); + } else { + await performAction( + stagehand, + page, + "Click the No radio button in the Is this a cell phone? field" + ); + } + if (applicant.can_text) { + await performAction( + stagehand, + page, + "Click the Yes radio button in the Can an ERIE Agent text you about this quote? field" + ); + } else { + await performAction( + stagehand, + page, + "Click the No radio button in the Can an ERIE Agent text you about this quote? field" + ); + } + if (applicant.preferred_name) { + await performAction( + stagehand, + page, + `Fill in the preferred name ${applicant.preferred_name} in the preferred name field` + ); + } + if (applicant.home_multi_policy_discount) { + await performAction( + stagehand, + page, + "Click the Yes radio button in the Would you like our Home Multi-Policy Discount applied to your quote? field" + ); + } else { + await performAction( + stagehand, + page, + "Click the No radio button in the Would you like our Home Multi-Policy Discount applied to your quote? field" + ); + } + if (applicant.currently_has_auto_insurance) { + await performAction( + stagehand, + page, + "Click the Yes radio button in the Do you currently have auto insurance? field" + ); + } else { + await performAction( + stagehand, + page, + "Click the No radio button in the Do you currently have auto insurance? field" + ); + } + await performAction( + stagehand, + page, + `Fill in the coverage effective date ${applicant.coverage_effective_date} in the coverage effective date field` + ); + await performAction(stagehand, page, "Click the Continue button"); + await performAction( + stagehand, + page, + "Click the Submit Quote to Agent button" + ); + + const result = await stagehand.extract("Extract the confirmation message"); + if (result) { + return result; + } else { + throw new InvalidActionError("Could not find confirmation message"); + } +} diff --git a/typescript-examples/rpa-forms-example/hooks/setupContext.ts b/typescript-examples/rpa-forms-example/hooks/setupContext.ts new file mode 100644 index 0000000..2b3bf81 --- /dev/null +++ b/typescript-examples/rpa-forms-example/hooks/setupContext.ts @@ -0,0 +1,11 @@ +import { attemptStore } from "@intuned/runtime"; + +export default async function setupContext({ + cdpUrl, +}: { + cdpUrl: string; + apiName: string; + apiParameters: any; +}) { + attemptStore.set("cdpUrl", cdpUrl); +} diff --git a/typescript-examples/rpa-forms-example/package.json b/typescript-examples/rpa-forms-example/package.json new file mode 100644 index 0000000..b364972 --- /dev/null +++ b/typescript-examples/rpa-forms-example/package.json @@ -0,0 +1,28 @@ +{ + "name": "insurance-form-filler", + "version": "1.0.0", + "description": "Insurance form filler using Stagehand in Intuned", + "tags": [ + "Stagehand", + "AI", + "RPA", + "Forms", + "intuned-runtime-sdk", + "intuned-runtime-sdk-setup-context-hook", + "intuned-browser-sdk" + ], + "main": "index.js", + "scripts": { + "intuned": "intuned" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@browserbasehq/stagehand": "^3.0.6", + "@intuned/browser": "0.1.8", + "@intuned/runtime": "^1.3.15", + "@types/node": "^20.10.3", + "playwright": "~1.56.0", + "zod": "3.25.67" + } +} diff --git a/typescript-examples/rpa-forms-example/tsconfig.json b/typescript-examples/rpa-forms-example/tsconfig.json new file mode 100644 index 0000000..f0046e6 --- /dev/null +++ b/typescript-examples/rpa-forms-example/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "ESNext", + "target": "ES2021", + "outDir": "./dist", + "sourceMap": false, + "declaration": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} + diff --git a/typescript-examples/rpa-forms-example/utils/typesAndSchemas.ts b/typescript-examples/rpa-forms-example/utils/typesAndSchemas.ts new file mode 100644 index 0000000..db33563 --- /dev/null +++ b/typescript-examples/rpa-forms-example/utils/typesAndSchemas.ts @@ -0,0 +1,93 @@ +import { z } from "zod"; + +// ---------- Metadata ---------- + +const metadataSchema = z.object({ + site: z.string().url(), + insurance_type: z.enum([ + "auto", + "homeowners", + "renters", + "motorcycle", + "boat", + "commercial_auto", + ]), +}); + +// ---------- Applicant ---------- + +const applicantSchema = z.object({ + first_name: z.string().min(1, "First name is required"), + last_name: z.string().min(1, "Last name is required"), + date_of_birth: z + .string() + .regex(/^\d{2}\/\d{2}\/\d{4}$/, "Date must be in MM/DD/YYYY format"), + gender: z.enum(["male", "female", "other"]), + marital_status: z.enum(["single", "married", "divorced", "widowed"]), + accident_prevention_course: z.boolean().default(false), + email: z.string().email("Invalid email address"), + phone_number: z.string(), + is_cell_phone: z.boolean(), + can_text: z.boolean(), + preferred_name: z.string().optional(), + home_multi_policy_discount: z.boolean(), + currently_has_auto_insurance: z.boolean(), + coverage_effective_date: z + .string() + .regex(/^\d{2}\/\d{2}\/\d{4}$/, "Date must be in MM/DD/YYYY format"), +}); + +// ---------- Address ---------- + +const addressSchema = z.object({ + street_line1: z.string().min(1, "Street address is required"), + street_line2: z.string().optional(), + city: z.string().min(1, "City is required"), + state: z.string().length(2, "State must be 2 characters"), + zip_code: z.string().regex(/^\d{5}$/, "Zip code must be 5 digits"), +}); + +// ---------- Vehicle ---------- + +const vehicleSchema = z.object({ + vehicle_type: z.enum([ + "Automobile", + "Travel Trailer", + "ATV", + "Utility Trailer", + "Snowmobile", + "Motor Home", + "Camper", + "Moped", + "Trail Bike", + "Dune Buggy", + "Mini Bike", + "Golf Cart", + "Recreational Trailer", + ]), + year: z.number().int().positive(), + make: z.string().min(1, "Make is required"), + model: z.string().min(1, "Model is required"), + primary_use: z + .enum(["Farm", "Business", "Pleasure", "Work/School"]) + .optional(), + annual_mileage: z.number().int().positive().optional(), + days_driven_per_week: z.number().int().positive().optional(), + miles_driven_one_way: z.number().int().positive().optional(), + model_details: z.string(), +}); + +// ---------- Root Schema ---------- + +export const listParametersSchema = z.object({ + metadata: metadataSchema, + applicant: applicantSchema, + address: addressSchema, + vehicle: vehicleSchema, +}); + +export type ListParameters = z.infer; +export type Metadata = z.infer; +export type Applicant = z.infer; +export type Address = z.infer; +export type Vehicle = z.infer;