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

Interface cleanup: wrapper mode default #49

Merged
merged 15 commits into from
Jan 10, 2024
6 changes: 3 additions & 3 deletions .github/workflows/regression-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
python3 -m pip install .

- name: Check prompts
run: python3 tests/regression.py --platform ubuntu --check
run: python3 -m tests.regression --platform ubuntu --check

check-macos:
runs-on: macos-latest
Expand All @@ -39,7 +39,7 @@ jobs:
python3 -m pip install .

- name: Check prompts
run: python3 tests/regression.py --platform macos --check
run: python3 -m tests.regression --platform macos --check

check-windows:
runs-on: windows-latest
Expand All @@ -53,4 +53,4 @@ jobs:
python3 -m pip install .

- name: Check prompts
run: python3 tests/regression.py --platform windows --check
run: python3 -m tests.regression --platform windows --check
6 changes: 3 additions & 3 deletions .github/workflows/regression-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
python3 -m pip install .

- name: Generate current prompts
run: python3 tests/regression.py --platform ubuntu --generate
run: python3 -m tests.regression --platform ubuntu --generate

- name: Commit
run: |
Expand All @@ -58,7 +58,7 @@ jobs:
python3 -m pip install .

- name: Generate current prompts
run: python3 tests/regression.py --platform macos --generate
run: python3 -m tests.regression --platform macos --generate

- name: Commit
run: |
Expand All @@ -85,7 +85,7 @@ jobs:
python3 -m pip install .

- name: Generate current prompts
run: python3 tests/regression.py --platform windows --generate
run: python3 -m tests.regression --platform windows --generate

- name: Commit
run: |
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/sanity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,18 @@ jobs:
- name: Update pip
run: python3 -m pip install --upgrade pip

- name: Install
run: python3 -m pip install .
- name: Install dependencies
run: python3 -m pip install mypy

- name: Install in editable mode
run: python3 -m pip install -e .

- name: Install in development mode
run: python3 -m pip install .[dev]

- name: Run mypy
run: python3 -m mypy src/**/*.py tests/*.py

- name: Check version through Python
run: python3 -m cwhy --version

Expand Down
32 changes: 9 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,22 @@ python3 -m pip install cwhy

## Usage

### Compiler wrapper mode

This new mode is recommended as CWhy will then operate in the same context as the compiler, and will do a better job
finding the right source files.
The wrapper mode is now default and mandatory, with a slightly modified interface.
CWhy can either be used standalone by passing the full command after the triple dashes `---`, or as part of a build tool
by creating a short executable script wrapping the compiler command.

```bash
# Invoking the compiler directly.
% `cwhy --wrapper` mycode.cpp

# Using fix mode.
% `cwhy --wrapper fix` mycode.cpp
% cwhy --- g++ mycode.cpp

# Using cwhy with Java.
% `cwhy --wrapper --wrapper-compiler=javac` mycode.java
# Using CWhy with Java and an increased timeout.
% cwhy --timeout 180 --- javac MyCode.java

# Invoking with GNU make, using GPT-4.
% CXX=`cwhy --llm=gpt-4 --wrapper` make
# Invoking with GNU Make, using GPT-3.5.
% CXX=`cwhy --llm=gpt-3.5-turbo --wrapper --- c++` make

# Invoking with CMake, using GPT-4 and clang++.
% cmake -DCMAKE_CXX_COMPILER=`cwhy --llm=gpt-4 --wrapper --wrapper-compiler=clang++` ...
% cmake -DCMAKE_CXX_COMPILER=`cwhy --llm=gpt-4 --wrapper --- clang++` ...
```

When running a configuration tool such as CMake or Autoconf, this may greatly increase configuration time, as these
Expand All @@ -61,14 +57,6 @@ configuration time.
% CWHY_DISABLE=1 cmake -DCMAKE_CXX_COMPILER=`cwhy --wrapper` ...
```

### Original mode

Just pipe your compiler's output to `cwhy`.

```bash
% clang++ -g mycode.cpp |& cwhy
```

### Options

These options can be displayed with `cwhy --help`.
Expand All @@ -77,8 +65,6 @@ These options can be displayed with `cwhy --help`.
- `--timeout`: pick a different timeout than the default for API calls.
- `--show-prompt` (debug): print prompts before calling the API.

The wrapper mode specifically also has a `--wrapper-compiler` option to select the underlying compiler to use.

## Examples

### C++
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ authors = [
{ name="Nicolas van Kempen", email="[email protected]" },
{ name="Bryce Adelstein Lelbach", email="[email protected]" }
]
dependencies = ["llm_utils==0.2.2", "openai>=1.3.6", "PyYAML>=6.0.1"]
dependencies = ["llm_utils==0.2.3", "openai>=1.3.6", "PyYAML>=6.0.1", "rich>=13.7.0"]
description = "Explains and proposes fixes for compile-time errors for many programming languages."
readme = "README.md"
requires-python = ">=3.7"
Expand All @@ -26,3 +26,6 @@ cwhy = "cwhy.__main__:main"
[project.urls]
"Homepage" = "https://github.com/plasma-umass/cwhy"
"Bug Tracker" = "https://github.com/plasma-umass/cwhy/issues"

[project.optional-dependencies]
dev = ["types-PyYAML>=6.0.1"]
141 changes: 93 additions & 48 deletions src/cwhy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,119 @@
import sys
import tempfile
import textwrap
from typing import Any, Optional

import openai
from rich.console import Console

from . import cwhy


def wrapper(args):
return textwrap.dedent(
f"""
#! {sys.executable}
from cwhy import cwhy
cwhy.wrapper({args})
return (
textwrap.dedent(
f"""
#! {sys.executable}
from cwhy import cwhy
cwhy.wrapper({vars(args)})
"""
).lstrip()
).strip()
+ "\n"
)


class RichArgParser(argparse.ArgumentParser):
def __init__(self, *args: Any, **kwargs: Any):
self.console = Console(highlight=False)
super().__init__(*args, **kwargs)

def _print_message(self, message: Optional[str], file: Any = None) -> None:
if message:
self.console.print(message)


class CWhyArgumentFormatter(argparse.HelpFormatter):
# RawDescriptionHelpFormatter.
def _fill_text(self, text, width, indent):
return "".join(indent + line for line in text.splitlines(keepends=True))

# RawTextHelpFormatter.
def _split_lines(self, text, width):
return text.splitlines()

# ArgumentDefaultsHelpFormatter.
# Ignore if help message is multiline.
def _get_help_string(self, action):
help = action.help
if "\n" not in help and "%(default)" not in action.help:
if action.default is not argparse.SUPPRESS:
defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
help += " (default: %(default)s)"
return help


def main():
parser = argparse.ArgumentParser(
description = textwrap.dedent(
rf"""
[b]CWhy[/b]: Explains and proposes fixes for compile-time errors for many programming languages.
[blue][link=https://github.com/plasma-umass/cwhy]https://github.com/plasma-umass/cwhy[/link][/blue]

usage:
[b]cwhy [SUBCOMMAND] \[OPTIONS...] --- COMMAND...[/b]
usage (GNU Make):
[b]CXX=`cwhy --wrapper \[OPTIONS...] --- c++` make[/b]
usage (CMake):
[b]cmake -DCMAKE_CXX_COMPILER=`cwhy --wrapper \[OPTIONS...] --- c++`[/b]
"""
).strip()

parser = RichArgParser(
prog="cwhy",
description="CWhy explains and fixes compiler diagnostic errors.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
usage=argparse.SUPPRESS,
description=description,
formatter_class=CWhyArgumentFormatter,
)

parser.add_argument(
"--help",
"-h",
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit.",
)
parser.add_argument(
"--version",
action="version",
version=f"%(prog)s v{importlib.metadata.metadata('cwhy')['Version']}",
default=argparse.SUPPRESS,
help="Print the version of CWhy and exit.",
help="print the version of CWhy and exit",
)

parser.add_argument(
"subcommand",
nargs="?",
default="explain",
choices=["explain", "diff", "converse"],
metavar="subcommand",
help=textwrap.dedent(
"""
explain: explain the diagnostic (default)
diff: \[experimental] generate a diff to fix the diagnostic
converse: \[experimental] interactively converse with CWhy
"""
).strip(),
)

parser.add_argument(
"--llm",
type=str,
default="default",
help="The language model to use, e.g., 'gpt-3.5-turbo' or 'gpt-4'. The default mode tries gpt-4 and falls back to gpt-3.5-turbo.",
help=textwrap.dedent(
"""
the language model to use, e.g., 'gpt-3.5-turbo' or 'gpt-4'
the default mode tries gpt-4 and falls back to gpt-3.5-turbo
"""
).strip(),
)
parser.add_argument(
"--timeout",
type=int,
default=60,
help="The timeout for API calls in seconds.",
help="the timeout for API calls in seconds",
)
# The default maximum context length for `gpt-3.5-turbo` is 4096 tokens.
# We keep 256 tokens for other parts of the prompt, and split the remainder in two
Expand All @@ -64,44 +127,33 @@ def main():
"--max-error-tokens",
type=int,
default=1920,
help="The maximum number of tokens from the error message to send in the prompt.",
help="the maximum number of tokens from the error message to send in the prompt",
)
parser.add_argument(
"--max-code-tokens",
type=int,
default=1920,
help="The maximum number of code locations tokens to send in the prompt.",
help="the maximum number of code locations tokens to send in the prompt",
)

parser.add_argument(
"--show-prompt",
action="store_true",
help="When enabled, only print prompt and exit (for debugging purposes).",
help="when enabled, only print prompt and exit (for debugging purposes)",
)
parser.add_argument(
"--wrapper",
action="store_true",
help="Enable compiler wrapper behavior.",
help="generate a temporary executable used to wrap to compiler command",
)
parser.add_argument(
"--wrapper-compiler",
metavar="COMPILER",
type=str,
default="c++",
help="The underlying compiler. Only enabled with --wrapper.",
"---",
dest="command",
required=True,
help=argparse.SUPPRESS,
nargs=argparse.REMAINDER,
)

subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")

subparsers.add_parser("explain", help="Explain the diagnostic. (default)")
subparsers.add_parser("fix", help="Propose a fix for the diagnostic.")
subparsers.add_parser("diff", help="[experimental] Propose a fix in diff format.")
subparsers.add_parser(
"converse", help="[experimental] A back-and-forth mode with ChatGPT."
)

parser.set_defaults(subcommand="explain")

args = parser.parse_args()

if args.wrapper:
Expand All @@ -112,14 +164,7 @@ def main():
os.chmod(f.name, 0o755)
print(f.name)
else:
stdin = sys.stdin.read()
if not stdin:
return
try:
print(cwhy.main(args, stdin))
except (openai.NotFoundError, openai.RateLimitError, openai.APITimeoutError):
# This type of exceptions should have been handled down the stack.
pass
cwhy.main(args)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion src/cwhy/conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def converse(client, args, diagnostic):
f"""
You are an assistant debugger. The user is having an issue with their code, and you are trying to help them.
A few functions exist to help with this process, namely: {", ".join(available_functions_names)}.
Once you are confident in your answer, explain the diagnostic and possibly provide a way to fix the issue.
Once you are confident in your answer, explain the diagnostic and provide a way to fix the issue if you can.
"""
).strip()
user_message = f"Here is my error message:\n\n```\n{fns.get_truncated_error_message()}\n```\n\nWhat's the problem?"
Expand Down
Loading