diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 94fae70786..b54ce24c84 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: Tox pytest all python versions +name: Tox + Pytest (All Python Versions) on: push: @@ -16,34 +16,56 @@ concurrency: jobs: test: runs-on: ubuntu-latest + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + strategy: matrix: python-version: ['3.10', '3.11', '3.12'] steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version == '3.12' && '3.12.3' || matrix.python-version }} # Using 3.12.3 to resolve Pydantic ForwardRef issue - cache: 'pip' # Note that pip is for the tox level. Poetry is still used for installing the specific environments (tox.ini) - - - name: Check Python Version - run: python --version - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox==4.15.0 poetry - - - name: Run tox - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: tox - - # Temporarily disabling codecov until we resolve codecov rate limiting issue - # - name: Report coverage - # run: | - # bash <(curl -s https://codecov.io/bash) + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Check Python version + run: python --version + + - name: Upgrade pip and install build tools + run: | + python -m pip install --upgrade pip + pip install poetry tox==4.15.0 + + - name: Configure Poetry + run: | + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + + - name: Update lock file + run: poetry lock + + - name: Install dependencies + run: poetry install --no-interaction --no-root + + - name: Run tests with Poetry (pytest) + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + poetry run pytest -q --maxfail=1 --disable-warnings -v + + - name: Run tests via Tox (multi-environment) + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: | + tox -vv + + # Optional, enable later for codecov + # - name: Upload coverage + # uses: codecov/codecov-action@v5 + # with: + # fail_ci_if_error: false diff --git a/projects/example-improve/controller.py b/projects/example-improve/controller.py index 6459aed070..60444b23ba 100644 --- a/projects/example-improve/controller.py +++ b/projects/example-improve/controller.py @@ -1,5 +1,8 @@ +# --- controller.py --- import keyboard +from model import Point # Import Point to define directions + class Controller: def __init__(self, game, view): @@ -7,15 +10,28 @@ def __init__(self, game, view): self.view = view def handle_input(self): - if keyboard.is_pressed("up") and not hasattr(self, "last_key_pressed"): - self.game.move("down") - self.last_key_pressed = "up" - elif hasattr(self, "last_key_pressed") and self.last_key_pressed == "up": - self.game.move("right") - del self.last_key_pressed + # We need a reference to the snake to change its direction + snake = self.game.snake + + # Check for key presses and set the snake's direction using Point objects. + # Note: We must call set_direction on the snake object. + + # UP: Change Y by -1 + if keyboard.is_pressed("up"): + snake.set_direction(Point(0, -1)) + + # DOWN: Change Y by +1 elif keyboard.is_pressed("down"): - self.game.move("up") + snake.set_direction(Point(0, 1)) + + # LEFT: Change X by -1 elif keyboard.is_pressed("left"): - self.game.move("right") + snake.set_direction(Point(-1, 0)) + + # RIGHT: Change X by +1 elif keyboard.is_pressed("right"): - self.game.move("left") + snake.set_direction(Point(1, 0)) + + # Optional: Add an exit key + elif keyboard.is_pressed("esc"): + self.game.is_running = False diff --git a/projects/example-improve/main.py b/projects/example-improve/main.py index 3de503f84e..54c4d5f73b 100644 --- a/projects/example-improve/main.py +++ b/projects/example-improve/main.py @@ -1,9 +1,111 @@ +import os +import platform +import subprocess +import sys + +from importlib.metadata import distributions + from controller import Controller from model import Game +from rich.console import Console +from rich.table import Table from view import View +# Sensitive environment variables to mask +SENSITIVE_KEYS = ["OPENAI_API_KEY", "API_KEY", "SECRET", "TOKEN"] + + +def get_command_version(command): + """Run a command and return its version, or 'Not found'.""" + try: + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip().split("\n")[0] + return "Not found" + except Exception: + return "Not found" + + +def get_env_variables(): + """Return filtered environment variables with sensitive ones masked.""" + filtered = {} + for key, value in os.environ.items(): + if key in SENSITIVE_KEYS: + filtered[key] = "*****" + elif key in ["PATH", "PYTHONPATH", "VIRTUAL_ENV"]: + filtered[key] = value + return filtered + + +def get_installed_packages(): + """Return a dict of installed Python packages and versions.""" + packages = {} + for dist in distributions(): + name = dist.metadata["Name"] + version = dist.version + if name: + packages[name] = version + return packages + + +def sysinfo(): + info = { + "gpt_engineer_version": "0.3.1", # Change if your package version differs + "os": platform.system(), + "os_version": platform.version(), + "architecture": platform.machine(), + "python_version": sys.version.split()[0], + "pip_version": get_command_version("pip --version"), + "git_version": get_command_version("git --version"), + "node_version": get_command_version("node --version"), + "env_variables": get_env_variables(), + "installed_packages": get_installed_packages(), + } + + # Pretty print using Rich + console = Console() + console.print("\n[bold cyan]=== System Information ===[/bold cyan]\n") + + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Key", style="cyan", no_wrap=True) + table.add_column("Value", style="green") + + # Top-level info + for key in [ + "gpt_engineer_version", + "os", + "os_version", + "architecture", + "python_version", + "pip_version", + "git_version", + "node_version", + ]: + table.add_row(key, str(info[key])) + + console.print(table) + + # Environment variables + console.print("\n[bold cyan]Environment Variables:[/bold cyan]") + for k, v in info["env_variables"].items(): + console.print(f"{k}: {v}") + + # Installed packages (optional: print first 30 only) + console.print("\n[bold cyan]Installed Packages (first 30 shown):[/bold cyan]") + for i, (pkg, ver) in enumerate(info["installed_packages"].items()): + if i >= 30: + console.print(f"... ({len(info['installed_packages'])-30} more packages)") + break + console.print(f"{pkg}: {ver}") + def main(): + # Check if --sysinfo argument is provided + if "--sysinfo" in sys.argv: + sysinfo() + return + + # Regular game loop game = Game() view = View(game) controller = Controller(game, view) diff --git a/projects/example-improve/model.py b/projects/example-improve/model.py index e5d0b72433..c0678f62fd 100644 --- a/projects/example-improve/model.py +++ b/projects/example-improve/model.py @@ -1,32 +1,88 @@ +# --- model.py --- import random from dataclasses import dataclass +# ------------------------------ +# Data Structures +# ------------------------------ @dataclass class Point: x: int y: int +# ------------------------------ +# Snake Class +# ------------------------------ +class Snake: + def __init__(self, initial_position: Point): + self.body: list[Point] = [initial_position] # Snake starts with one segment + self.direction = Point(1, 0) # Moving right initially + self.grow_next_move = False # Flag to handle growth + + @property + def head(self) -> Point: + """Return the current head of the snake.""" + return self.body[0] + + def set_direction(self, new_direction: Point): + """Change direction if not opposite to current.""" + if ( + self.direction.x + new_direction.x != 0 + or self.direction.y + new_direction.y != 0 + ): + self.direction = new_direction + + def move(self): + """Move the snake in the current direction.""" + new_head = Point(self.head.x + self.direction.x, self.head.y + self.direction.y) + self.body.insert(0, new_head) # Add new head + if self.grow_next_move: + self.grow_next_move = False # Growth applied, reset flag + else: + self.body.pop() # Remove tail if not growing + + def grow(self): + """Trigger growth on the next move.""" + self.grow_next_move = True + + +# ------------------------------ +# Game Class +# ------------------------------ class Game: - def __init__(self): - self.snake = [Point(5, 5)] + def __init__(self, size: int = 10): + self.size = size # Board size (size x size) + self.snake = Snake(initial_position=Point(size // 2, size // 2)) self.food = self.generate_food() self.is_running = True def generate_food(self): - return Point(random.randint(0, 10), random.randint(0, 10)) + """Generate food not overlapping with the snake.""" + while True: + food_point = Point( + random.randint(0, self.size - 1), random.randint(0, self.size - 1) + ) + if food_point not in self.snake.body: + return food_point def update(self): - # Move the snake + """Update the game state: move snake, handle collisions.""" self.snake.move() - # Check for collision with food + # Check for food collision if self.snake.head == self.food: self.snake.grow() self.food = self.generate_food() - # Check for collision with boundaries - if not (0 <= self.snake.head.x < 10 and 0 <= self.snake.head.y < 10): + # Check for wall collisions + if not ( + 0 <= self.snake.head.x < self.size and 0 <= self.snake.head.y < self.size + ): + self.is_running = False + + # Check for self-collision + if self.snake.head in self.snake.body[1:]: self.is_running = False diff --git a/projects/example-improve/view.py b/projects/example-improve/view.py index 78421e5124..8a3fd2eee1 100644 --- a/projects/example-improve/view.py +++ b/projects/example-improve/view.py @@ -6,14 +6,17 @@ def __init__(self, game): self.game = game def render(self): - # Print the game state - for y in range(10): - for x in range(10): - if Point(x, y) in self.game.snake: - print("S", end="") - elif Point(x, y) == self.game.food: - print("F", end="") + # Print the game state (10x10 board) + for y in range(self.game.size): + for x in range(self.game.size): + current = Point(x, y) + if current == self.game.snake.head: + print("H", end="") # Head of the snake + elif current in self.game.snake.body[1:]: + print("S", end="") # Body of the snake + elif current == self.game.food: + print("F", end="") # Food else: - print(".", end="") + print(".", end="") # Empty space print() print() diff --git a/pyproject.toml b/pyproject.toml index bb229c8c09..462466c612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ ] [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=1.5.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] @@ -28,7 +28,7 @@ tiktoken = ">=0.0.4" tabulate = "0.9.0" python-dotenv = ">=0.21.0" langchain = ">=0.1.2" -langchain_openai = "*" +langchain_openai = "^0.1.0" toml = ">=0.10.2" tomlkit = "^0.12.4" pyperclip = "^1.8.2" @@ -36,8 +36,10 @@ langchain-anthropic = "^0.1.1" regex = "^2023.12.25" pillow = "^10.2.0" datasets = "^2.17.1" -black = "23.3.0" +black = "^23.3.0" langchain-community = "^0.2.0" +packaging = ">=23.1.0" +pydantic = "<2.0.0" [tool.poetry.group.dev.dependencies] pytest = ">=7.3.1" @@ -59,16 +61,15 @@ sphinx-typlog-theme = ">=0.8.0" toml = ">=0.10.2" myst-nb = ">=0.17.1" linkchecker = ">=10.2.1" -sphinx-copybutton = ">=0.5.1" +sphinx-copybutton = ">=0.5.2" markdown-include = ">=0.6.0" -sphinx_copybutton = ">=0.5.2" [tool.poetry.scripts] -gpt-engineer = 'gpt_engineer.applications.cli.main:app' -ge = 'gpt_engineer.applications.cli.main:app' -gpte = 'gpt_engineer.applications.cli.main:app' -bench = 'gpt_engineer.benchmark.__main__:app' -gpte_test_application = 'tests.caching_main:app' +gpt-engineer = "gpt_engineer.applications.cli.main:app" +ge = "gpt_engineer.applications.cli.main:app" +gpte = "gpt_engineer.applications.cli.main:app" +bench = "gpt_engineer.benchmark.__main__:app" +gpte_test_application = "tests.caching_main:app" [tool.poetry.extras] test = ["pytest", "pytest-cov"] @@ -85,7 +86,6 @@ doc = [ "linkchecker", "sphinx-copybutton", "markdown-include", - "sphinx_copybutton", ] [tool.ruff] diff --git a/tests/test_install.py b/tests/test_install.py index 5ee951ceee..6c2fa41b96 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2,6 +2,7 @@ Tests for successful installation of the package. """ +import os import shutil import subprocess import sys @@ -47,6 +48,12 @@ def venv_setup_teardown(): finally: # Clean up by removing the virtual environment after tests. shutil.rmtree(VENV_DIR) + try: + subprocess.run(["gpte", "--version"], check=True) + print("gpte is installed") + except (subprocess.CalledProcessError, FileNotFoundError): + print("gpte is NOT installed") + def test_installation(): @@ -93,12 +100,15 @@ def test_installed_main_execution(tmp_path, monkeypatch): p = tmp_path / "projects/example" p.mkdir(parents=True) (p / "prompt").write_text("make a program that prints the outcome of 4+4") + env = os.environ.copy() + assert "OPENAI_API_KEY" in env, "OPENAI_API_KEY environment variable is not set" proc = subprocess.Popen( ["gpte", str(p)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, cwd=tmp_path, + env=env, ) inputs = "Y\nn" diff --git a/tox.ini b/tox.ini index 088b114a25..8da15f851f 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,8 @@ basepython = py312: python3.12 deps = poetry + pytest commands = poetry install --no-root + poetry run pytest -q poetry run pytest --cov=gpt_engineer --cov-report=xml -k 'not installed_main_execution'