Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[bumpversion]
current_version = 0.3.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea 0.3 makes sense

commit = True
tag = True

[bumpversion:file:setup.py]
search = version="{current_version}"
replace = version="{new_version}"

[bumpversion:file:stagehand/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
81 changes: 81 additions & 0 deletions .github/README_PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Publishing stagehand-python to PyPI

This repository is configured with a GitHub Actions workflow to automate the process of publishing new versions to PyPI.

## Prerequisites

Before using the publishing workflow, ensure you have:

1. Set up the following secrets in your GitHub repository settings:
- `PYPI_USERNAME`: Your PyPI username
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don;'t think I have the access rights to set these, and would likely not want my pypi account in the secrets for this repo anyway - you want me to create a new one? or idk

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should def set up a browserbase account for this

- `PYPI_API_TOKEN`: Your PyPI API token (not your password)

## How to Publish a New Version

### Manual Trigger

1. Go to the "Actions" tab in your GitHub repository
2. Select the "Publish to PyPI" workflow from the list
3. Click "Run workflow" on the right side
4. Configure the workflow:
- Choose the release type:
- `patch` (e.g., 0.3.0 → 0.3.1) for bug fixes
- `minor` (e.g., 0.3.0 → 0.4.0) for backward-compatible features
- `major` (e.g., 0.3.0 → 1.0.0) for breaking changes
- Toggle "Create GitHub Release" if you want to create a GitHub release
5. Click "Run workflow" to start the process

### What Happens During Publishing

The workflow will:

1. Checkout the repository
2. Set up Python environment
3. Install dependencies
4. **Run Ruff linting checks**:
- Checks for code style and quality issues
- Verifies formatting according to project standards
- Fails the workflow if issues are found
5. Run tests to ensure everything works
6. Update the version number using bumpversion
7. Build the package
8. Upload to PyPI
9. Push the version bump commit and tag
10. Create a GitHub release (if selected)

## Code Quality Standards

This project uses Ruff for linting and formatting. The workflow enforces these standards before publishing:

- Style checks following configured rules in `pyproject.toml`
- Format verification without making changes
- All linting issues must be fixed before a successful publish

To run the same checks locally:
```bash
# Install Ruff
pip install ruff

# Run linting
ruff check .

# Check formatting
ruff format --check .

# Auto-fix issues where possible
ruff check --fix .
ruff format .
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I currently get lots of issues running this locally? meaning, many type issues


# Use Black to format the code
black .
```

## Troubleshooting

If the workflow fails, check the following:

1. **Linting errors**: Fix any issues reported by Ruff
2. Ensure all secrets are properly set
3. Verify that tests pass locally
4. Check if you have proper permissions on the repository
5. Make sure you have a PyPI account with publishing permissions
82 changes: 82 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Publish to PyPI

on:
workflow_dispatch:
inputs:
release_type:
description: 'Release type (patch, minor, major)'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
create_release:
description: 'Create GitHub Release'
required: true
default: true
type: boolean

jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine wheel setuptools bumpversion ruff
pip install -r requirements.txt
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi

- name: Run Ruff linting
run: |
# Run Ruff linter
ruff check .

# Run Ruff formatter check (without modifying files)
ruff format --check .

- name: Run tests
run: |
pytest

- name: Update version
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
bumpversion ${{ github.event.inputs.release_type }}

- name: Build package
run: |
python -m build

- name: Upload to PyPI
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*

- name: Push version bump
run: |
git push
git push --tags

- name: Create GitHub Release
if: ${{ github.event.inputs.create_release == 'true' }}
uses: softprops/action-gh-release@v1
with:
tag_name: v$(python setup.py --version)
name: Release v$(python setup.py --version)
generate_release_notes: true
28 changes: 15 additions & 13 deletions evals/act/google_jobs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
import traceback
from typing import Any, Dict, Optional
from typing import Any, Optional, dict

from pydantic import BaseModel

Expand All @@ -10,23 +10,24 @@

class Qualifications(BaseModel):
degree: Optional[str] = None
yearsOfExperience: Optional[float] = None # Representing the number
years_of_experience: Optional[float] = None # Representing the number


class JobDetails(BaseModel):
applicationDeadline: Optional[str] = None
minimumQualifications: Qualifications
preferredQualifications: Qualifications
application_deadline: Optional[str] = None
minimum_qualifications: Qualifications
preferred_qualifications: Qualifications


def is_job_details_valid(details: Dict[str, Any]) -> bool:
def is_job_details_valid(details: dict[str, Any]) -> bool:
"""
Validates that each top-level field in the extracted job details is not None.
For nested dictionary values, each sub-value must be non-null and a string or a number.
For nested dictionary values, each sub-value must be non-null and a string
or a number.
"""
if not details:
return False
for key, value in details.items():
for _key, value in details.items():
if value is None:
return False
if isinstance(value, dict):
Expand All @@ -53,9 +54,9 @@ async def google_jobs(model_name: str, logger, use_text_extract: bool) -> dict:
4. Extracting job posting details using an AI-driven extraction schema.

The extraction schema requires:
- applicationDeadline: The opening date until which applications are accepted.
- minimumQualifications: An object with degree and yearsOfExperience.
- preferredQualifications: An object with degree and yearsOfExperience.
- application_deadline: The opening date until which applications are accepted.
- minimum_qualifications: An object with degree and years_of_experience.
- preferred_qualifications: An object with degree and years_of_experience.

Returns a dictionary containing:
- _success (bool): Whether valid job details were extracted.
Expand Down Expand Up @@ -90,8 +91,9 @@ async def google_jobs(model_name: str, logger, use_text_extract: bool) -> dict:
job_details = await stagehand.page.extract(
ExtractOptions(
instruction=(
"Extract the following details from the job posting: application deadline, "
"minimum qualifications (degree and years of experience), and preferred qualifications "
"Extract the following details from the job posting: "
"application deadline, minimum qualifications "
"(degree and years of experience), and preferred qualifications "
"(degree and years of experience)"
),
schemaDefinition=JobDetails.model_json_schema(),
Expand Down
8 changes: 6 additions & 2 deletions evals/extract/extract_press_releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class PressReleases(BaseModel):

async def extract_press_releases(model_name: str, logger, use_text_extract: bool):
"""
Extract press releases from the dummy press releases page using the Stagehand client.
Extract press releases from the dummy press releases page using the Stagehand
client.

Args:
model_name (str): Name of the AI model to use.
Expand Down Expand Up @@ -56,7 +57,10 @@ async def extract_press_releases(model_name: str, logger, use_text_extract: bool
# TODO - FAILING - extract is likely timing out
raw_result = await stagehand.page.extract(
ExtractOptions(
instruction="extract the title and corresponding publish date of EACH AND EVERY press releases on this page. DO NOT MISS ANY PRESS RELEASES.",
instruction=(
"extract the title and corresponding publish date of EACH AND EVERY "
"press releases on this page. DO NOT MISS ANY PRESS RELEASES."
),
schemaDefinition=PressReleases.model_json_schema(),
useTextExtract=use_text_extract,
)
Expand Down
11 changes: 7 additions & 4 deletions evals/init_stagehand.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

async def init_stagehand(model_name: str, logger, dom_settle_timeout_ms: int = 3000):
"""
Initialize a Stagehand client with the given model name, logger, and DOM settle timeout.
Initialize a Stagehand client with the given model name, logger, and DOM settle
timeout.

This function creates a configuration from environment variables, initializes the Stagehand client,
and returns a tuple of (stagehand, init_response). The init_response contains debug and session URLs.
This function creates a configuration from environment variables, initializes
the Stagehand client, and returns a tuple of (stagehand, init_response).
The init_response contains debug and session URLs.

Args:
model_name (str): The name of the AI model to use.
Expand Down Expand Up @@ -37,7 +39,8 @@ async def init_stagehand(model_name: str, logger, dom_settle_timeout_ms: int = 3
model_client_options={"apiKey": os.getenv("MODEL_API_KEY")},
)

# Create a Stagehand client with the configuration; server_url is taken from environment variables.
# Create a Stagehand client with the configuration; server_url is taken from
# environment variables.
stagehand = Stagehand(
config=config, server_url=os.getenv("STAGEHAND_SERVER_URL"), verbose=2
)
Expand Down
3 changes: 3 additions & 0 deletions examples/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ async def main():
if len(observed) > 0:
element = observed[0]
console.print("✅ [success]Found element:[/] News button")
console.print("\n▶️ [highlight] Performing action on observed element")
await page.act(element)
console.print("✅ [success]Performing Action:[/] Action completed successfully")

else:
console.print("❌ [error]No element found[/]")

Expand Down
5 changes: 3 additions & 2 deletions examples/example_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from rich.panel import Panel
from rich.theme import Theme

from stagehand.sync import Stagehand
from stagehand.config import StagehandConfig
from stagehand.sync import Stagehand

# Create a custom theme for consistent styling
custom_theme = Theme(
Expand Down Expand Up @@ -60,6 +60,7 @@ def main():
)

import time

time.sleep(2)

console.print("\n▶️ [highlight] Navigating[/] to Google")
Expand Down Expand Up @@ -112,4 +113,4 @@ def main():
padding=(1, 10),
),
)
main()
main()
21 changes: 21 additions & 0 deletions format.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash

# Define source directories (adjust as needed)
SOURCE_DIRS="evals stagehand"

# Apply Black formatting only to source directories
echo "Applying Black formatting..."
black $SOURCE_DIRS

# Fix import sorting (addresses I001 errors)
echo "Sorting imports..."
isort $SOURCE_DIRS

# Apply Ruff with autofix for remaining issues
echo "Applying Ruff autofixes..."
ruff check --fix $SOURCE_DIRS

echo "Checking for remaining issues..."
ruff check $SOURCE_DIRS

echo "Done! Code has been formatted and linted."
Loading