Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create docker environment and Codespace for easy onborading #35

Merged
merged 47 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9b4de74
docker
erlichsefi Aug 21, 2023
28eee24
Update devcontainer.json
erlichsefi Aug 21, 2023
fa55d6f
remove requirements
erlichsefi Aug 21, 2023
1a9a7ec
docker
erlichsefi Aug 21, 2023
707f068
first docker
erlichsefi Aug 21, 2023
94e6514
docker testing
erlichsefi Aug 21, 2023
e970547
.
erlichsefi Aug 21, 2023
3102182
.
erlichsefi Aug 21, 2023
7cf302f
seleuim docker
erlichsefi Aug 21, 2023
c977c35
.
erlichsefi Aug 21, 2023
eca0fc3
.
erlichsefi Aug 21, 2023
e6f3a8b
.
erlichsefi Aug 21, 2023
f3ba82d
black
erlichsefi Aug 21, 2023
5d36bd2
.
erlichsefi Aug 21, 2023
aee0c91
.
erlichsefi Aug 21, 2023
61caf14
Update docker-compose.yaml
erlichsefi Aug 22, 2023
5c17198
Update devcontainer.json
erlichsefi Aug 22, 2023
7f964a2
Update devcontainer.json
erlichsefi Aug 22, 2023
3feb484
Update devcontainer.json
erlichsefi Aug 22, 2023
04026f5
Update selenium.py
erlichsefi Aug 22, 2023
3c6ebf6
Update devcontainer.json
erlichsefi Aug 22, 2023
064f554
Update devcontainer.json
erlichsefi Aug 23, 2023
fbb4cfd
Update lint.yml
erlichsefi Aug 23, 2023
545f5a6
Update tests.yml
erlichsefi Aug 23, 2023
0bae0fa
Update selenium.py
erlichsefi Aug 23, 2023
6294b54
Update test_selenium.py
erlichsefi Aug 23, 2023
9a5eef1
Update lint.yml
erlichsefi Aug 23, 2023
2faf57c
Update devcontainer.json
erlichsefi Aug 24, 2023
ba0a0f0
Update docker-compose.yaml
erlichsefi Aug 24, 2023
391e929
Create dev.docker-compose.yaml
erlichsefi Aug 25, 2023
bd08d33
Rename docker-compose.yaml to test.docker-compose.yaml
erlichsefi Aug 25, 2023
ef25b89
Update devcontainer.json
erlichsefi Aug 25, 2023
f0cb593
add depends
erlichsefi Aug 29, 2023
167ddc4
Update devcontainer.json
erlichsefi Aug 29, 2023
ed30831
Update and rename test.docker-compose.yaml to docker-compose.yaml
erlichsefi Aug 29, 2023
008f35b
Update Dockerfile
erlichsefi Aug 29, 2023
1e9a612
.
erlichsefi Aug 29, 2023
c05575a
clear code
erlichsefi Aug 30, 2023
894223d
.
erlichsefi Aug 30, 2023
d32fc2e
.
erlichsefi Aug 30, 2023
89a6099
click to dev
erlichsefi Aug 30, 2023
23d003f
.
erlichsefi Aug 30, 2023
1b04661
formating
erlichsefi Aug 30, 2023
210866d
foramting
erlichsefi Aug 30, 2023
cc9eb58
add example .env
erlichsefi Aug 30, 2023
05d4810
remove and .env updates
erlichsefi Aug 30, 2023
b360eca
.
erlichsefi Aug 30, 2023
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
16 changes: 16 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"dockerComposeFile": "../docker-compose.yaml",
"service": "chromegpt",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-toolsai.jupyter"
]
}
},
"forwardPorts": [3000,4444,7900,5900],
"shutdownAction": "stopCompose"
}
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
OPENAI_API_KEY=sk-
REQUEST="Find me a bar that can host a 20 person event near Chelsea, Manhattan evening of Apr 30th. Fill out contact us form if they have one with info: Name Richard, email [email protected]."
TARGET=base
31 changes: 13 additions & 18 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
name: lint

env:
TARGET: 'test'
MAKE: 'lint'
on:
push:
branches: [main]
pull_request:

env:
POETRY_VERSION: "1.4.2"

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -16,18 +15,14 @@ jobs:
python-version:
- "3.10"
steps:
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry==$POETRY_VERSION
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: poetry
- name: Install dependencies
run: |
poetry install
- name: Analysing the code with our lint
- name: Checkout
uses: actions/checkout@v3
- name: Free disk space
run: |
make lint
df --human-readable
sudo apt clean
docker 2>/dev/null 1>&2 rmi $(docker image ls --all --quiet) || true
rm --recursive --force "$AGENT_TOOLSDIRECTORY"
df --human-readable
- name: Test with pytest
run: docker-compose up --abort-on-container-exit
31 changes: 13 additions & 18 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
name: tests

env:
TARGET: 'test'
MAKE: 'tests'
on:
push:
branches: [main]
pull_request:

env:
POETRY_VERSION: "1.4.2"

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -16,18 +15,14 @@ jobs:
python-version:
- "3.10"
steps:
- uses: actions/checkout@v3
- name: Install poetry
run: |
pipx install poetry==$POETRY_VERSION
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: poetry
- name: Install dependencies
run: |
poetry install
- name: Run all tests
- name: Checkout
uses: actions/checkout@v3
- name: Free disk space
run: |
make tests
df --human-readable
sudo apt clean
docker 2>/dev/null 1>&2 rmi $(docker image ls --all --quiet) || true
rm --recursive --force "$AGENT_TOOLSDIRECTORY"
df --human-readable
- name: Test with pytest
run: docker-compose up --abort-on-container-exit
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3.8 as base

WORKDIR /app

# setup code
COPY . .
RUN pip install poetry==1.4.2

# Install dependencies using Poetry
RUN poetry config virtualenvs.create false && \
poetry install --no-interaction --no-ansi

CMD python -m chromegpt -v -t "${REQUEST}"

# image to dev
FROM base as dev
CMD sh -c "while sleep 1000; do :; done"

# image to run tests
FROM base as test
ARG MAKE="tests"
CMD make $MAKE
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

[![lint](https://github.com/richardyc/chrome-gpt/actions/workflows/lint.yml/badge.svg)](https://github.com/richardyc/chrome-gpt/actions/workflows/lint.yml) [![test](https://github.com/richardyc/chrome-gpt/actions/workflows/tests.yml/badge.svg)](https://github.com/richardyc/chrome-gpt/actions/workflows/tests.yml) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/RealRichomie.svg?style=social&label=Follow%20%40RealRichomie)](https://twitter.com/RealRichomie)


⚠️This is an experimental AutoGPT agent that might take incorrect actions and could lead to serious consequences. Please use it at your own discretion⚠️

Chrome-GPT is an AutoGPT experiment that utilizes [Langchain](https://github.com/hwchase17/langchain) and [Selenium](https://github.com/SeleniumHQ/selenium) to enable an AutoGPT agent take control of an entire Chrome session. With the ability to interactively scroll, click, and input text on web pages, the AutoGPT agent can navigate and manipulate web content.
Expand Down Expand Up @@ -41,6 +42,11 @@ Demo made by [Richard He](https://twitter.com/RealRichomie)
3. Open a poetry shell `poetry shell`
4. Run chromegpt via `python -m chromegpt`


You can start in you own codespace here:

[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/erlichsefi/Chrome-GPT/codespaces)

<h2 align="center"> 🧠 Usage </h2>

- GPT-3.5 Usage (Default): `python -m chromegpt -v -t "{your request}"`
Expand All @@ -63,6 +69,10 @@ Options:
--help Show this message and exit.
```

Or Just update .env and

`source .env & docker-compose up`

<h2 align="center"> ⭐ Star History </h2>

[![Star History Chart](https://api.star-history.com/svg?repos=richardyc/Chrome-GPT&type=Date)](https://star-history.com/#richardyc/Chrome-GPT&Date)
Expand Down
15 changes: 15 additions & 0 deletions chromegpt/tools/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from typing import Any, Callable

from chromegpt.tools.selenium import SeleniumWrapper


def execute_with_driver(test_function: Callable[[SeleniumWrapper], None]) -> Callable:
def wrapper(*args: Any, **kwargs: Any) -> None:
try:
client = SeleniumWrapper(headless=True)
test_function(client, *args, **kwargs)
finally:
# release the driver
del client

return wrapper
16 changes: 12 additions & 4 deletions chromegpt/tools/selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,27 @@ class SeleniumWrapper:
selenium = SeleniumWrapper()
"""

def __init__(self, headless: bool = False) -> None:
def __init__(self, headless: bool = False, docker: bool = True) -> None:
"""Initialize Selenium and start interactive session."""
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless")
else:
chrome_options.add_argument("--start-maximized")
self.driver = webdriver.Chrome(options=chrome_options)
if docker:
self.driver = webdriver.Remote(
"http://selenium-chrome:4444/wd/hub",
options=chrome_options,
)
else:
self.driver = webdriver.Chrome(options=chrome_options)
self.driver.implicitly_wait(5) # Wait 5 seconds for elements to load

def __del__(self) -> None:
"""Close Selenium session."""
self.driver.close()
if hasattr(self, "driver") and self.driver is not None:
self.driver.close()
self.driver.quit()

def previous_webpage(self) -> str:
"""Go back in browser history."""
Expand Down Expand Up @@ -305,7 +313,7 @@ def fill_out_form(self, form_input: Optional[str] = None, **kwargs: Any) -> str:
" website did not change after filling out form."
)
except WebDriverException as e:
print(e)
# print(e)
return f"Error filling out form with input {form_input}, message: {e.msg}"

def scroll(self, direction: str) -> str:
Expand Down
11 changes: 7 additions & 4 deletions chromegpt/tools/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Utils for chromegpt tools."""

import re
from typing import List, Optional
from typing import List, Optional, Union

from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
from selenium.webdriver.remote.webelement import WebElement
from unidecode import unidecode

Expand All @@ -14,7 +15,7 @@ def is_complete_sentence(text: str) -> bool:
return re.search(r"[.!?]\s*$", text) is not None


def get_all_text_elements(driver: WebDriver) -> List[str]:
def get_all_text_elements(driver: Union[WebDriver, RemoteWebDriver]) -> List[str]:
xpath = (
"//*[not(self::script or self::style or"
" self::noscript)][string-length(normalize-space(text())) > 0]"
Expand All @@ -30,7 +31,7 @@ def get_all_text_elements(driver: WebDriver) -> List[str]:
return texts


def find_interactable_elements(driver: WebDriver) -> List[str]:
def find_interactable_elements(driver: Union[WebDriver, RemoteWebDriver]) -> List[str]:
"""Find all interactable elements on the page."""
# Extract interactable components (buttons and links)
buttons = driver.find_elements(By.XPATH, "//button")
Expand Down Expand Up @@ -62,7 +63,9 @@ def prettify_text(text: str, limit: Optional[int] = None) -> str:
return text


def element_completely_viewable(driver: WebDriver, elem: WebElement) -> bool:
def element_completely_viewable(
driver: Union[WebDriver, RemoteWebDriver], elem: WebElement
) -> bool:
"""Check if an element is completely viewable in the browser window."""
elem_left_bound = elem.location.get("x")
elem_top_bound = elem.location.get("y")
Expand Down
35 changes: 35 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: '3'
services:
chromegpt:
build:
context: .
dockerfile: Dockerfile
target: ${TARGET:-dev} # Default value is "dev"
args:
- MAKE=${MAKE}
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REQUEST=${REQUEST}
depends_on:
# make sure the the image will be created only after Selenium is stable
# otherwise, tests a run before Selenium is accessible
selenium-chrome:
condition: service_healthy


selenium-chrome:
image: selenium/standalone-chrome:latest
ports:
- "4444:4444"
- "7900:7900"
- "5900:5900"
environment:
- SE_NODE_MAX_SESSIONS=10
- SE_NODE_SESSION_TIMEOUT=30000
shm_size: "2g"
# define Selenium stable 'Healthy' only after it's accessible.
healthcheck:
test: ["CMD", "curl", "-f", "http://selenium-chrome:4444/wd/hub/status"]
interval: 10s # Check every 30 seconds
timeout: 5s # Timeout after 10 seconds
retries: 3 # Retry 3 times before considering the container unhealthy
40 changes: 22 additions & 18 deletions tests/test_selenium.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,51 @@
"""Integration test for Selenium API Wrapper."""

import pytest

from chromegpt.tools.selenium import SeleniumWrapper


@pytest.fixture
def client() -> SeleniumWrapper:
return SeleniumWrapper(headless=True)
from chromegpt.tools.driver import SeleniumWrapper, execute_with_driver


@execute_with_driver
def test_describe_website(client: SeleniumWrapper) -> None:
"""Test that SeleniumWrapper returns correct website"""

output = None
output = client.describe_website("https://example.com")
assert "this domain is for use in illu" in output
assert output is not None and "this domain is for use in illu" in output


@execute_with_driver
def test_click(client: SeleniumWrapper) -> None:
"""Test that SeleniumWrapper click works"""

output = None
client.describe_website("https://example.com")
output = client.click_button_by_text('link with title "More information..."')
assert "Clicked interactable element and the website changed" in output
assert (
output is not None
and "Clicked interactable element and the website changed" in output
)


@execute_with_driver
def test_google_input(client: SeleniumWrapper) -> None:
"""Test that SeleniumWrapper can find input form"""

output = None
output = client.find_form_inputs("https://google.com")
assert "q" in output
assert output is not None and "q" in output


@execute_with_driver
def test_google_fill(client: SeleniumWrapper) -> None:
"""Test that SeleniumWrapper can fill input form"""

output = None
client.find_form_inputs("https://google.com")
output = client.fill_out_form(q="hello world")
assert "website changed after filling out form" in output

assert output is not None and "website changed after filling out form" in output


@execute_with_driver
def test_google_search(client: SeleniumWrapper) -> None:
"""Test google search functionality"""
res = client.google_search("hello world")
assert "hello" in res
assert "Which url would you like to goto" in res
output = None
output = client.google_search("hello world")
assert output is not None and "hello" in output
assert output is not None and "Which url would you like to goto" in output