From 83f27b82ce2b70075bc590f2945b944b46df19ad Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Wed, 18 Sep 2024 08:57:32 -0700 Subject: [PATCH 1/8] Add GitHub Actions workflow for running tests --- .github/workflows/python-tests.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/python-tests.yml diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..adbefdf --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,29 @@ +name: Python Tests + +on: + push: + branches: [ main, add_tests ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.9, '3.10'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Run tests + run: | + pytest \ No newline at end of file From b3118e448a09b404a38b9931ff8a2b427a857544 Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Wed, 18 Sep 2024 09:28:36 -0700 Subject: [PATCH 2/8] Added tests --- codeaide/utils/api_utils.py | 46 +++++------------ codeaide/utils/file_handler.py | 11 +++- requirements.txt | 3 +- tests/__init__.py | 0 tests/utils/__init__.py | 0 tests/utils/test_api_utils.py | 89 ++++++++++++++++++++++++++++++++ tests/utils/test_file_handler.py | 79 ++++++++++++++++++++++++++++ 7 files changed, 192 insertions(+), 36 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/utils/__init__.py create mode 100644 tests/utils/test_api_utils.py create mode 100644 tests/utils/test_file_handler.py diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index 3ab244b..ceca5a0 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -41,40 +41,20 @@ def parse_response(response): if not response or not response.content: return None, None, None, None, None, None - content = response.content[0].text - - def extract_json_field(field_name, content, is_code=False): - pattern = rf'"{field_name}"\s*:\s*"((?:\\.|[^"\\])*)"' - match = re.search(pattern, content, re.DOTALL) - if match: - field_content = match.group(1) - if is_code: - # For code, replace escaped newlines with actual newlines, but only within strings - field_content = re.sub(r'(? Date: Wed, 18 Sep 2024 09:31:48 -0700 Subject: [PATCH 3/8] Wrapped client creation in try/except --- codeaide/utils/api_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index ceca5a0..1b231d4 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -5,9 +5,14 @@ from decouple import config from codeaide.utils.constants import MAX_TOKENS, AI_MODEL, SYSTEM_PROMPT -ANTHROPIC_API_KEY = config('ANTHROPIC_API_KEY') +try: + ANTHROPIC_API_KEY = config('ANTHROPIC_API_KEY') -client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) + client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) +except Exception as e: + print(f"Error initializing API client: {str(e)}") + print("API functionality will be disabled") + client = None def send_api_request(conversation_history, max_tokens=MAX_TOKENS): system_prompt = SYSTEM_PROMPT From b15e491f9c808f73ef3330517a6a53c9fdfd666e Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Wed, 18 Sep 2024 09:53:14 -0700 Subject: [PATCH 4/8] Updates to tests --- .github/workflows/python-tests.yml | 6 +- codeaide.py | 2 +- codeaide/utils/api_utils.py | 25 ++- pytest.ini | 5 + tests/conftest.py | 11 ++ tests/utils/test_api_utils.py | 255 +++++++++++++++++++---------- 6 files changed, 210 insertions(+), 94 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/conftest.py diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index adbefdf..8e674e3 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -22,8 +22,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest + pip install pytest pytest-mock if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Run tests + env: + ANTHROPIC_API_KEY: dummy_key_for_testing run: | - pytest \ No newline at end of file + pytest -v \ No newline at end of file diff --git a/codeaide.py b/codeaide.py index 22f5849..d1b8aa2 100644 --- a/codeaide.py +++ b/codeaide.py @@ -7,7 +7,7 @@ def main(): chat_handler = ChatHandler() if len(sys.argv) > 1 and sys.argv[1] == 'test': - success, message = chat_handler.test_api_connection() + success, message = chat_handler.check_api_connection() if success: print("Connection successful!") print("Claude says:", message) diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index 1b231d4..e5428f1 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -5,14 +5,17 @@ from decouple import config from codeaide.utils.constants import MAX_TOKENS, AI_MODEL, SYSTEM_PROMPT -try: - ANTHROPIC_API_KEY = config('ANTHROPIC_API_KEY') +def get_anthropic_client(): + try: + api_key = config('ANTHROPIC_API_KEY', default=None) + if api_key is None: + raise ValueError("ANTHROPIC_API_KEY not found in environment variables") + return anthropic.Anthropic(api_key=api_key) + except Exception as e: + print(f"Error initializing Anthropic API client: {str(e)}") + return None - client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY) -except Exception as e: - print(f"Error initializing API client: {str(e)}") - print("API functionality will be disabled") - client = None +client = get_anthropic_client() def send_api_request(conversation_history, max_tokens=MAX_TOKENS): system_prompt = SYSTEM_PROMPT @@ -61,7 +64,7 @@ def parse_response(response): print("Error: Received malformed JSON from the API") return None, None, None, None, None, None -def test_api_connection(): +def check_api_connection(): try: response = client.messages.create( model=AI_MODEL, @@ -72,4 +75,8 @@ def test_api_connection(): ) return True, response.content[0].text.strip() except Exception as e: - return False, str(e) \ No newline at end of file + return False, str(e) + +if __name__ == "__main__": + success, message = check_api_connection() + print(f"Connection {'successful' if success else 'failed'}: {message}") \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..10cc899 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +markers = + send_api_request: marks tests related to sending API requests + parse_response: marks tests related to parsing API responses + api_connection: marks tests related to API connection \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..def25da --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import pytest +from unittest.mock import Mock +import anthropic + +@pytest.fixture +def mock_anthropic_client(monkeypatch): + mock_client = Mock(spec=anthropic.Anthropic) + mock_messages = Mock() + mock_client.messages = mock_messages + monkeypatch.setattr('codeaide.utils.api_utils.client', mock_client) + return mock_client \ No newline at end of file diff --git a/tests/utils/test_api_utils.py b/tests/utils/test_api_utils.py index a11b13a..21fea94 100644 --- a/tests/utils/test_api_utils.py +++ b/tests/utils/test_api_utils.py @@ -1,89 +1,180 @@ import json -from codeaide.utils.api_utils import parse_response +import pytest +from unittest.mock import Mock from collections import namedtuple +from codeaide.utils.api_utils import parse_response, send_api_request, check_api_connection +from codeaide.utils.constants import MAX_TOKENS, AI_MODEL, SYSTEM_PROMPT +from anthropic import APIError # Mock Response object Response = namedtuple('Response', ['content']) TextBlock = namedtuple('TextBlock', ['text']) -def test_parse_response_empty(): - result = parse_response(None) - assert result == (None, None, None, None, None, None) - -def test_parse_response_no_content(): - response = Response(content=[]) - result = parse_response(response) - assert result == (None, None, None, None, None, None) - -def test_parse_response_valid(): - content = { - "text": "Sample text", - "code": "print('Hello, World!')", - "code_version": "1.0", - "version_description": "Initial version", - "requirements": ["pytest"], - "questions": ["What does this code do?"] - } - response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - - assert text == "Sample text" - assert questions == ["What does this code do?"] - assert code == "print('Hello, World!')" - assert code_version == "1.0" - assert version_description == "Initial version" - assert requirements == ["pytest"] - -def test_parse_response_missing_fields(): - content = { - "text": "Sample text", - "code": "print('Hello, World!')" - } - response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - - assert text == "Sample text" - assert questions == [] - assert code == "print('Hello, World!')" - assert code_version is None - assert version_description is None - assert requirements == [] - -def test_parse_response_complex_code(): - content = { - "text": "Complex code example", - "code": 'def hello():\n print("Hello, World!")', - "code_version": "1.1", - "version_description": "Added function", - "requirements": [], - "questions": [] - } - response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - - assert text == "Complex code example" - assert code == 'def hello():\n print("Hello, World!")' - assert code_version == "1.1" - assert version_description == "Added function" - -def test_parse_response_escaped_quotes(): - content = { - "text": 'Text with "quotes"', - "code": 'print("Hello, \\"World!\\"")\nprint(\'Single quotes\')', - "code_version": "1.2", - "version_description": "Added escaped quotes", - "requirements": [], - "questions": [] - } - response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - - assert text == 'Text with "quotes"' - assert code == 'print("Hello, \\"World!\\"")\nprint(\'Single quotes\')' - assert code_version == "1.2" - assert version_description == "Added escaped quotes" - -def test_parse_response_malformed_json(): - response = Response(content=[TextBlock(text="This is not JSON")]) - result = parse_response(response) - assert result == (None, None, None, None, None, None) \ No newline at end of file +pytestmark = [ + pytest.mark.send_api_request, + pytest.mark.parse_response, + pytest.mark.api_connection +] + +class TestSendAPIRequest: + def test_send_api_request_success(self, mock_anthropic_client): + conversation_history = [ + {"role": "user", "content": "Hello, Claude!"} + ] + mock_response = Mock() + mock_response.content = [Mock(text="Hello! How can I assist you today?")] + mock_anthropic_client.messages.create.return_value = mock_response + + result = send_api_request(conversation_history) + + mock_anthropic_client.messages.create.assert_called_once_with( + model=AI_MODEL, + max_tokens=MAX_TOKENS, + messages=conversation_history, + system=SYSTEM_PROMPT + ) + assert result == mock_response + + def test_send_api_request_empty_response(self, mock_anthropic_client): + conversation_history = [ + {"role": "user", "content": "Hello, Claude!"} + ] + mock_response = Mock() + mock_response.content = [] + mock_anthropic_client.messages.create.return_value = mock_response + + result = send_api_request(conversation_history) + + assert result == (None, True) + + def test_send_api_request_api_error(self, mock_anthropic_client): + conversation_history = [ + {"role": "user", "content": "Hello, Claude!"} + ] + mock_request = Mock() + mock_anthropic_client.messages.create.side_effect = APIError( + request=mock_request, + message="API Error", + body={"error": {"message": "API Error"}} + ) + + result = send_api_request(conversation_history) + + assert result == (None, True) + + def test_send_api_request_custom_max_tokens(self, mock_anthropic_client): + conversation_history = [ + {"role": "user", "content": "Hello, Claude!"} + ] + custom_max_tokens = 500 + mock_response = Mock() + mock_response.content = [Mock(text="Hello! How can I assist you today?")] + mock_anthropic_client.messages.create.return_value = mock_response + + result = send_api_request(conversation_history, max_tokens=custom_max_tokens) + + mock_anthropic_client.messages.create.assert_called_once_with( + model=AI_MODEL, + max_tokens=custom_max_tokens, + messages=conversation_history, + system=SYSTEM_PROMPT + ) + assert result == mock_response + +class TestParseResponse: + def test_parse_response_empty(self): + result = parse_response(None) + assert result == (None, None, None, None, None, None) + + def test_parse_response_no_content(self): + response = Response(content=[]) + result = parse_response(response) + assert result == (None, None, None, None, None, None) + + def test_parse_response_valid(self): + content = { + "text": "Sample text", + "code": "print('Hello, World!')", + "code_version": "1.0", + "version_description": "Initial version", + "requirements": ["pytest"], + "questions": ["What does this code do?"] + } + response = Response(content=[TextBlock(text=json.dumps(content))]) + text, questions, code, code_version, version_description, requirements = parse_response(response) + + assert text == "Sample text" + assert questions == ["What does this code do?"] + assert code == "print('Hello, World!')" + assert code_version == "1.0" + assert version_description == "Initial version" + assert requirements == ["pytest"] + + def test_parse_response_missing_fields(self): + content = { + "text": "Sample text", + "code": "print('Hello, World!')" + } + response = Response(content=[TextBlock(text=json.dumps(content))]) + text, questions, code, code_version, version_description, requirements = parse_response(response) + + assert text == "Sample text" + assert questions == [] + assert code == "print('Hello, World!')" + assert code_version is None + assert version_description is None + assert requirements == [] + + def test_parse_response_complex_code(self): + content = { + "text": "Complex code example", + "code": 'def hello():\n print("Hello, World!")', + "code_version": "1.1", + "version_description": "Added function", + "requirements": [], + "questions": [] + } + response = Response(content=[TextBlock(text=json.dumps(content))]) + text, questions, code, code_version, version_description, requirements = parse_response(response) + + assert text == "Complex code example" + assert code == 'def hello():\n print("Hello, World!")' + assert code_version == "1.1" + assert version_description == "Added function" + + def test_parse_response_escaped_quotes(self): + content = { + "text": 'Text with "quotes"', + "code": 'print("Hello, \\"World!\\"")\nprint(\'Single quotes\')', + "code_version": "1.2", + "version_description": "Added escaped quotes", + "requirements": [], + "questions": [] + } + response = Response(content=[TextBlock(text=json.dumps(content))]) + text, questions, code, code_version, version_description, requirements = parse_response(response) + + assert text == 'Text with "quotes"' + assert code == 'print("Hello, \\"World!\\"")\nprint(\'Single quotes\')' + assert code_version == "1.2" + assert version_description == "Added escaped quotes" + + def test_parse_response_malformed_json(self): + response = Response(content=[TextBlock(text="This is not JSON")]) + result = parse_response(response) + assert result == (None, None, None, None, None, None) + +class TestAPIConnection: + def check_api_connection_success(self, mock_anthropic_client): + mock_response = Mock() + mock_response.content = [Mock(text="Yes, we are communicating.")] + mock_anthropic_client.messages.create.return_value = mock_response + result = check_api_connection() + assert result[0] == True + assert result[1] == "Yes, we are communicating." + + def check_api_connection_failure(self, mock_anthropic_client): + mock_anthropic_client.messages.create.side_effect = Exception("Connection failed") + result = check_api_connection() + assert result[0] == False + assert "Connection failed" in result[1] \ No newline at end of file From 60e1a98dcd57ec2bef2f1e21c8c9e2ea3d8b43c9 Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Thu, 19 Sep 2024 07:50:27 -0700 Subject: [PATCH 5/8] updated requirements --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 218d5e4..3fa2f78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,7 @@ anthropic==0.34.2 python-decouple==3.8 virtualenv==20.16.2 pyyaml -pytest \ No newline at end of file +pytest +black==22.6.0 +isort==5.10.1 +pre-commit==2.20.0 \ No newline at end of file From 1e83f6d6c258443714bebe4e262298a87ea374c2 Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Thu, 19 Sep 2024 07:51:09 -0700 Subject: [PATCH 6/8] updated requirements --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3fa2f78..21631b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ python-decouple==3.8 virtualenv==20.16.2 pyyaml pytest -black==22.6.0 -isort==5.10.1 -pre-commit==2.20.0 \ No newline at end of file +black +isort +pre-commit \ No newline at end of file From 05d2d5a48d981a3a8935dcbe8994cff377622310 Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Thu, 19 Sep 2024 07:51:44 -0700 Subject: [PATCH 7/8] added lint check and precommit config --- .github/workflows/black.yml | 25 +++++++++++++++++++++++++ .pre-commit-config.yaml | 11 +++++++++++ 2 files changed, 36 insertions(+) create mode 100644 .github/workflows/black.yml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..6f179cd --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,25 @@ +name: Black Code Formatter + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: psf/black@stable + with: + options: "--check --verbose" + src: "./codeaide" + - name: If needed, commit black changes to the pull request + if: failure() + run: | + black ./codeaide + git config --global user.name github-actions + git config --global user.email github-actions@github.com + git commit -am "Auto-format code with black" + git push \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..dfc3387 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3 +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) \ No newline at end of file From d511815bbc04edff52a48d4da36c94d9d60b29cc Mon Sep 17 00:00:00 2001 From: dougollerenshaw Date: Thu, 19 Sep 2024 07:57:38 -0700 Subject: [PATCH 8/8] Reformatted entire repo with black --- .pre-commit-config.yaml | 5 -- codeaide.py | 8 +- codeaide/logic/chat_handler.py | 110 ++++++++++++++++-------- codeaide/ui/chat_window.py | 106 ++++++++++++++++------- codeaide/ui/code_popup.py | 82 ++++++++++++------ codeaide/ui/example_selection_dialog.py | 30 ++++--- codeaide/utils/api_utils.py | 50 ++++++----- codeaide/utils/constants.py | 24 +++--- codeaide/utils/cost_tracker.py | 25 +++--- codeaide/utils/environment_manager.py | 35 +++++--- codeaide/utils/file_handler.py | 16 ++-- codeaide/utils/general_utils.py | 38 ++++---- codeaide/utils/terminal_manager.py | 27 +++--- tests/conftest.py | 8 +- tests/utils/test_api_utils.py | 106 ++++++++++++++--------- tests/utils/test_file_handler.py | 46 ++++++---- 16 files changed, 456 insertions(+), 260 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dfc3387..9120c3c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,8 +4,3 @@ repos: hooks: - id: black language_version: python3 -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: isort (python) \ No newline at end of file diff --git a/codeaide.py b/codeaide.py index f830e4b..c113c42 100644 --- a/codeaide.py +++ b/codeaide.py @@ -1,13 +1,16 @@ import sys + from PyQt5.QtWidgets import QApplication + from codeaide.logic.chat_handler import ChatHandler from codeaide.ui.chat_window import ChatWindow from codeaide.utils import api_utils + def main(): chat_handler = ChatHandler() - if len(sys.argv) > 1 and sys.argv[1] == 'test': + if len(sys.argv) > 1 and sys.argv[1] == "test": success, message = chat_handler.check_api_connection() success, message = api_utils.test_api_connection() if success: @@ -22,5 +25,6 @@ def main(): chat_window.show() sys.exit(app.exec_()) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/codeaide/logic/chat_handler.py b/codeaide/logic/chat_handler.py index aa4b681..5d57138 100644 --- a/codeaide/logic/chat_handler.py +++ b/codeaide/logic/chat_handler.py @@ -1,11 +1,13 @@ -from codeaide.utils.api_utils import send_api_request, parse_response +import json +import os + +from codeaide.utils.api_utils import parse_response, send_api_request +from codeaide.utils.constants import MAX_RETRIES, MAX_TOKENS +from codeaide.utils.cost_tracker import CostTracker from codeaide.utils.environment_manager import EnvironmentManager -from codeaide.utils.terminal_manager import TerminalManager from codeaide.utils.file_handler import FileHandler -from codeaide.utils.cost_tracker import CostTracker -from codeaide.utils.constants import MAX_TOKENS, MAX_RETRIES -import os -import json +from codeaide.utils.terminal_manager import TerminalManager + class ChatHandler: def __init__(self): @@ -20,66 +22,106 @@ def __init__(self): def process_input(self, user_input): try: self.conversation_history.append({"role": "user", "content": user_input}) - + for attempt in range(MAX_RETRIES): version_info = f"\n\nThe latest code version was {self.latest_version}. If you're making minor changes to the previous code, increment the minor version (e.g., 1.0 to 1.1). If you're creating entirely new code, increment the major version (e.g., 1.1 to 2.0). Ensure the new version is higher than {self.latest_version}." self.conversation_history[-1]["content"] += version_info response = send_api_request(self.conversation_history, MAX_TOKENS) print(f"Response (Attempt {attempt + 1}): {response}") - + if response is None: if attempt == MAX_RETRIES - 1: - return {'type': 'error', 'message': "Failed to get a response from the AI. Please try again."} + return { + "type": "error", + "message": "Failed to get a response from the AI. Please try again.", + } continue - + self.cost_tracker.log_request(response) - + try: - text, questions, code, code_version, version_description, requirements = parse_response(response) - - if code and self.compare_versions(code_version, self.latest_version) <= 0: - raise ValueError(f"New version {code_version} is not higher than the latest version {self.latest_version}") - + ( + text, + questions, + code, + code_version, + version_description, + requirements, + ) = parse_response(response) + + if ( + code + and self.compare_versions(code_version, self.latest_version) + <= 0 + ): + raise ValueError( + f"New version {code_version} is not higher than the latest version {self.latest_version}" + ) + if code: self.latest_version = code_version - self.conversation_history.append({"role": "assistant", "content": response.content[0].text}) - + self.conversation_history.append( + {"role": "assistant", "content": response.content[0].text} + ) + if questions: - return {'type': 'questions', 'message': text, 'questions': questions} + return { + "type": "questions", + "message": text, + "questions": questions, + } elif code: - self.file_handler.save_code(code, code_version, version_description, requirements) - return {'type': 'code', 'message': f"{text}\n\nOpening in the code window as v{code_version}...", 'code': code, 'requirements': requirements} + self.file_handler.save_code( + code, code_version, version_description, requirements + ) + return { + "type": "code", + "message": f"{text}\n\nOpening in the code window as v{code_version}...", + "code": code, + "requirements": requirements, + } else: - return {'type': 'message', 'message': text} - + return {"type": "message", "message": text} + except (json.JSONDecodeError, ValueError) as e: print(f"Error processing response (Attempt {attempt + 1}): {e}") if attempt < MAX_RETRIES - 1: error_prompt = f"\n\nThere was an error in your response: {e}. Please ensure you're using proper JSON formatting and incrementing the version number correctly. The latest version was {self.latest_version}, so the new version must be higher than this." self.conversation_history[-1]["content"] += error_prompt else: - return {'type': 'error', 'message': f"There was an error processing the AI's response after {MAX_RETRIES} attempts. Please try again."} - - return {'type': 'error', 'message': f"Failed to get a valid response from the AI after {MAX_RETRIES} attempts. Please try again."} - + return { + "type": "error", + "message": f"There was an error processing the AI's response after {MAX_RETRIES} attempts. Please try again.", + } + + return { + "type": "error", + "message": f"Failed to get a valid response from the AI after {MAX_RETRIES} attempts. Please try again.", + } + except Exception as e: print("Unexpected error in process_input") - return {'type': 'error', 'message': f"An unexpected error occurred: {str(e)}"} + return { + "type": "error", + "message": f"An unexpected error occurred: {str(e)}", + } @staticmethod def compare_versions(v1, v2): - v1_parts = list(map(int, v1.split('.'))) - v2_parts = list(map(int, v2.split('.'))) + v1_parts = list(map(int, v1.split("."))) + v2_parts = list(map(int, v2.split("."))) return (v1_parts > v2_parts) - (v1_parts < v2_parts) def run_generated_code(self, filename, requirements): project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) script_path = os.path.join(project_root, self.file_handler.output_dir, filename) - req_path = os.path.join(project_root, self.file_handler.output_dir, requirements) + req_path = os.path.join( + project_root, self.file_handler.output_dir, requirements + ) activation_command = self.env_manager.get_activation_command() - + new_packages = self.env_manager.install_requirements(req_path) script_content = f""" @@ -93,7 +135,7 @@ def run_generated_code(self, filename, requirements): script_content += 'echo "New dependencies installed:"\n' for package in new_packages: script_content += f'echo " - {package}"\n' - + script_content += f""" echo "Running {filename}..." python "{script_path}" @@ -104,4 +146,4 @@ def run_generated_code(self, filename, requirements): self.terminal_manager.run_in_terminal(script_content) def is_task_in_progress(self): - return bool(self.conversation_history) \ No newline at end of file + return bool(self.conversation_history) diff --git a/codeaide/ui/chat_window.py b/codeaide/ui/chat_window.py index e674572..0f45930 100644 --- a/codeaide/ui/chat_window.py +++ b/codeaide/ui/chat_window.py @@ -1,16 +1,38 @@ -import sys import signal -from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QMessageBox, QSpacerItem, QSizePolicy, QApplication +import sys + from PyQt5.QtCore import Qt, QTimer, pyqtSignal from PyQt5.QtGui import QColor, QFont +from PyQt5.QtWidgets import ( + QApplication, + QHBoxLayout, + QMainWindow, + QMessageBox, + QPushButton, + QSizePolicy, + QSpacerItem, + QTextEdit, + QVBoxLayout, + QWidget, +) + from codeaide.ui.code_popup import CodePopup from codeaide.ui.example_selection_dialog import show_example_dialog from codeaide.utils import general_utils from codeaide.utils.constants import ( - CHAT_WINDOW_WIDTH, CHAT_WINDOW_HEIGHT, CHAT_WINDOW_BG, CHAT_WINDOW_FG, - USER_MESSAGE_COLOR, AI_MESSAGE_COLOR, USER_FONT, AI_FONT, AI_EMOJI, INITIAL_MESSAGE + AI_EMOJI, + AI_FONT, + AI_MESSAGE_COLOR, + CHAT_WINDOW_BG, + CHAT_WINDOW_FG, + CHAT_WINDOW_HEIGHT, + CHAT_WINDOW_WIDTH, + INITIAL_MESSAGE, + USER_FONT, + USER_MESSAGE_COLOR, ) + class ChatWindow(QMainWindow): def __init__(self, chat_handler): super().__init__() @@ -21,10 +43,10 @@ def __init__(self, chat_handler): self.code_popup = None self.setup_ui() self.add_to_chat("AI", INITIAL_MESSAGE) - + # Set up SIGINT handler signal.signal(signal.SIGINT, self.sigint_handler) - + # Allow CTRL+C to interrupt the Qt event loop self.timer = QTimer() self.timer.start(500) # Timeout in ms @@ -40,22 +62,26 @@ def setup_ui(self): # Chat display self.chat_display = QTextEdit(self) self.chat_display.setReadOnly(True) - self.chat_display.setStyleSheet(f""" + self.chat_display.setStyleSheet( + f""" background-color: {CHAT_WINDOW_BG}; color: {CHAT_WINDOW_FG}; border: 1px solid #ccc; padding: 5px; - """) + """ + ) main_layout.addWidget(self.chat_display, stretch=3) # Input area self.input_text = QTextEdit(self) - self.input_text.setStyleSheet(f""" + self.input_text.setStyleSheet( + f""" background-color: {CHAT_WINDOW_BG}; color: {USER_MESSAGE_COLOR}; border: 1px solid #ccc; padding: 5px; - """) + """ + ) self.input_text.setAcceptRichText(True) self.input_text.setFont(general_utils.set_font(USER_FONT)) self.input_text.setFixedHeight(100) @@ -83,7 +109,10 @@ def setup_ui(self): def eventFilter(self, obj, event): if obj == self.input_text and event.type() == event.KeyPress: - if event.key() == Qt.Key_Return and not event.modifiers() & Qt.ShiftModifier: + if ( + event.key() == Qt.Key_Return + and not event.modifiers() & Qt.ShiftModifier + ): self.on_submit() return True elif event.key() == Qt.Key_Return and event.modifiers() & Qt.ShiftModifier: @@ -110,7 +139,7 @@ def add_to_chat(self, sender, message): color = USER_MESSAGE_COLOR if sender == "User" else AI_MESSAGE_COLOR font = USER_FONT if sender == "User" else AI_FONT sender = AI_EMOJI if sender == "AI" else sender - + html_message = general_utils.format_chat_message(sender, message, font, color) self.chat_display.append(html_message + "
") self.chat_display.ensureCursorVisible() @@ -126,22 +155,26 @@ def handle_response(self, response): self.remove_thinking_messages() self.enable_ui_elements() - if response['type'] == 'message': - self.add_to_chat("AI", response['message']) - elif response['type'] == 'questions': - message = response['message'] - questions = response['questions'] - combined_message = f"{message}\n" + "\n".join(f" * {question}" for question in questions) + if response["type"] == "message": + self.add_to_chat("AI", response["message"]) + elif response["type"] == "questions": + message = response["message"] + questions = response["questions"] + combined_message = f"{message}\n" + "\n".join( + f" * {question}" for question in questions + ) self.add_to_chat("AI", combined_message) if self.chat_handler.is_task_in_progress(): - self.add_to_chat("AI", "Please provide answers to these questions to continue.") - elif response['type'] == 'code': - self.add_to_chat("AI", response['message']) + self.add_to_chat( + "AI", "Please provide answers to these questions to continue." + ) + elif response["type"] == "code": + self.add_to_chat("AI", response["message"]) print("About to update or create code popup") self.update_or_create_code_popup(response) print("Code popup updated or created") - elif response['type'] == 'error': - self.add_to_chat("AI", response['message']) + elif response["type"] == "error": + self.add_to_chat("AI", response["message"]) def remove_thinking_messages(self): cursor = self.chat_display.textCursor() @@ -150,7 +183,7 @@ def remove_thinking_messages(self): cursor.select(cursor.BlockUnderCursor) if "Thinking... 🤔" in cursor.selectedText(): cursor.removeSelectedText() - cursor.deleteChar() + cursor.deleteChar() else: cursor.movePosition(cursor.NextBlock) @@ -166,10 +199,18 @@ def enable_ui_elements(self): def update_or_create_code_popup(self, response): if self.code_popup and not self.code_popup.isHidden(): - self.code_popup.update_with_new_version(response['code'], response.get('requirements', [])) + self.code_popup.update_with_new_version( + response["code"], response.get("requirements", []) + ) else: - self.code_popup = CodePopup(None, self.chat_handler.file_handler, response['code'], response.get('requirements', []), self.chat_handler.run_generated_code) - self.code_popup.show() + self.code_popup = CodePopup( + None, + self.chat_handler.file_handler, + response["code"], + response.get("requirements", []), + self.chat_handler.run_generated_code, + ) + self.code_popup.show() def load_example(self): example = show_example_dialog(self) @@ -179,10 +220,14 @@ def load_example(self): else: QMessageBox.information(self, "No Selection", "No example was selected.") - def on_exit(self): - reply = QMessageBox.question(self, 'Quit', 'Do you want to quit?', - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + reply = QMessageBox.question( + self, + "Quit", + "Do you want to quit?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No, + ) if reply == QMessageBox.Yes: self.close() @@ -195,4 +240,3 @@ def closeEvent(self, event): def sigint_handler(self, *args): """Handler for the SIGINT signal.""" QApplication.quit() - diff --git a/codeaide/ui/code_popup.py b/codeaide/ui/code_popup.py index 4576f80..f21b254 100644 --- a/codeaide/ui/code_popup.py +++ b/codeaide/ui/code_popup.py @@ -1,10 +1,28 @@ -from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, - QComboBox, QPushButton, QLabel, QFileDialog, QApplication) -from PyQt5.QtCore import Qt, QRect +import os + +from PyQt5.QtCore import QRect, Qt from PyQt5.QtGui import QFont -from codeaide.utils.constants import CODE_WINDOW_WIDTH, CODE_WINDOW_HEIGHT, CODE_WINDOW_BG, CODE_WINDOW_FG, CODE_FONT +from PyQt5.QtWidgets import ( + QApplication, + QComboBox, + QFileDialog, + QHBoxLayout, + QLabel, + QPushButton, + QTextEdit, + QVBoxLayout, + QWidget, +) + from codeaide.utils import general_utils -import os +from codeaide.utils.constants import ( + CODE_FONT, + CODE_WINDOW_BG, + CODE_WINDOW_FG, + CODE_WINDOW_HEIGHT, + CODE_WINDOW_WIDTH, +) + class CodePopup(QWidget): def __init__(self, parent, file_handler, code, requirements, run_callback): @@ -17,7 +35,9 @@ def __init__(self, parent, file_handler, code, requirements, run_callback): self.load_versions() self.show_code(code, requirements) self.position_window() - self.loading_versions = False # Prevents multiple calls to on_version_change method + self.loading_versions = ( + False # Prevents multiple calls to on_version_change method + ) self.show() def setup_ui(self): @@ -27,17 +47,19 @@ def setup_ui(self): self.text_area = QTextEdit(self) self.text_area.setReadOnly(True) - self.text_area.setStyleSheet(f""" + self.text_area.setStyleSheet( + f""" background-color: {CODE_WINDOW_BG}; color: {CODE_WINDOW_FG}; border: 1px solid #ccc; padding: 5px; - """) + """ + ) self.text_area.setFont(general_utils.set_font(CODE_FONT)) layout.addWidget(self.text_area) controls_layout = QVBoxLayout() - + version_label = QLabel("Choose a version to display/run:") controls_layout.addWidget(version_label) @@ -51,7 +73,7 @@ def setup_ui(self): ("Copy Code", self.on_copy_code), ("Save Code", self.on_save_code), ("Copy Requirements", self.on_copy_requirements), - ("Close", self.close) + ("Close", self.close), ] for text, callback in buttons: @@ -71,15 +93,21 @@ def update_with_new_version(self, code, requirements): self.show_code(code, requirements) def load_versions(self): - self.loading_versions = True # Set flag before loading to prevent on_version_change from running + self.loading_versions = ( + True # Set flag before loading to prevent on_version_change from running + ) self.versions_dict = self.file_handler.get_versions_dict() - version_values = [f"v{version}: {data['version_description']}" - for version, data in self.versions_dict.items()] + version_values = [ + f"v{version}: {data['version_description']}" + for version, data in self.versions_dict.items() + ] self.version_dropdown.clear() self.version_dropdown.addItems(version_values) if version_values: self.version_dropdown.setCurrentIndex(len(version_values) - 1) - self.loading_versions = False # Reset flag after loading to allow on_version_change to run + self.loading_versions = ( + False # Reset flag after loading to allow on_version_change to run + ) def show_code(self, code, requirements): self.text_area.setPlainText(code) @@ -89,27 +117,27 @@ def show_code(self, code, requirements): def on_version_change(self): if self.loading_versions: return # Exit early if we're still loading versions - + selected = self.version_dropdown.currentText() - version = selected.split(':')[0].strip('v') + version = selected.split(":")[0].strip("v") version_data = self.versions_dict[version] - code_path = version_data['code_path'] - with open(code_path, 'r') as file: + code_path = version_data["code_path"] + with open(code_path, "r") as file: code = file.read() - requirements = version_data['requirements'] + requirements = version_data["requirements"] self.show_code(code, requirements) def on_run(self): selected = self.version_dropdown.currentText() - version = selected.split(':')[0].strip('v') + version = selected.split(":")[0].strip("v") version_data = self.versions_dict[version] - code_path = version_data['code_path'] - requirements = version_data['requirements'] - + code_path = version_data["code_path"] + requirements = version_data["requirements"] + req_file_name = f"requirements_{version}.txt" req_path = os.path.join(os.path.dirname(code_path), req_file_name) - with open(req_path, 'w') as f: - f.write('\n'.join(requirements)) + with open(req_path, "w") as f: + f.write("\n".join(requirements)) self.run_callback(code_path, req_path) @@ -117,7 +145,9 @@ def on_copy_code(self): QApplication.clipboard().setText(self.text_area.toPlainText()) def on_save_code(self): - file_path, _ = QFileDialog.getSaveFileName(self, "Save Code", "", "Python Files (*.py)") + file_path, _ = QFileDialog.getSaveFileName( + self, "Save Code", "", "Python Files (*.py)" + ) if file_path: with open(file_path, "w") as file: file.write(self.text_area.toPlainText()) diff --git a/codeaide/ui/example_selection_dialog.py b/codeaide/ui/example_selection_dialog.py index f8c0af6..7966b5b 100644 --- a/codeaide/ui/example_selection_dialog.py +++ b/codeaide/ui/example_selection_dialog.py @@ -1,8 +1,17 @@ -from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QPushButton, QTextBrowser, QSplitter from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import ( + QDialog, + QListWidget, + QPushButton, + QSplitter, + QTextBrowser, + QVBoxLayout, +) + from codeaide.utils import general_utils from codeaide.utils.constants import CHAT_WINDOW_BG, CHAT_WINDOW_FG + class ExampleSelectionDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) @@ -12,17 +21,17 @@ def __init__(self, parent=None): self.layout = QVBoxLayout() self.layout.setSpacing(5) - self.layout.setContentsMargins(8, 8, 8, 8) + self.layout.setContentsMargins(8, 8, 8, 8) # Create a splitter to divide the list and preview self.splitter = QSplitter(Qt.Vertical) self.example_list = QListWidget() self.preview_text = QTextBrowser() - + self.splitter.addWidget(self.example_list) self.splitter.addWidget(self.preview_text) - + # Set the ratio to 1:3 (20% list, 80% preview) self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 4) @@ -43,13 +52,13 @@ def __init__(self, parent=None): def load_examples(self): for example in self.examples: - self.example_list.addItem(example['description']) + self.example_list.addItem(example["description"]) def update_preview(self, current, previous): if current: for example in self.examples: - if example['description'] == current.text(): - self.preview_text.setPlainText(example['prompt']) + if example["description"] == current.text(): + self.preview_text.setPlainText(example["prompt"]) break def get_selected_example(self): @@ -57,12 +66,13 @@ def get_selected_example(self): if selected_items: selected_description = selected_items[0].text() for example in self.examples: - if example['description'] == selected_description: - return example['prompt'] + if example["description"] == selected_description: + return example["prompt"] return None + def show_example_dialog(parent): dialog = ExampleSelectionDialog(parent) if dialog.exec_(): return dialog.get_selected_example() - return None \ No newline at end of file + return None diff --git a/codeaide/utils/api_utils.py b/codeaide/utils/api_utils.py index e5428f1..31bdd17 100644 --- a/codeaide/utils/api_utils.py +++ b/codeaide/utils/api_utils.py @@ -1,13 +1,16 @@ import json import re + import anthropic from anthropic import APIError from decouple import config -from codeaide.utils.constants import MAX_TOKENS, AI_MODEL, SYSTEM_PROMPT + +from codeaide.utils.constants import AI_MODEL, MAX_TOKENS, SYSTEM_PROMPT + def get_anthropic_client(): try: - api_key = config('ANTHROPIC_API_KEY', default=None) + api_key = config("ANTHROPIC_API_KEY", default=None) if api_key is None: raise ValueError("ANTHROPIC_API_KEY not found in environment variables") return anthropic.Anthropic(api_key=api_key) @@ -15,68 +18,73 @@ def get_anthropic_client(): print(f"Error initializing Anthropic API client: {str(e)}") return None + client = get_anthropic_client() + def send_api_request(conversation_history, max_tokens=MAX_TOKENS): system_prompt = SYSTEM_PROMPT try: print(f"\n\n{'='*50}\n") - print(f"Sending API request. The max tokens is {max_tokens}. Here's the conversation history:") + print( + f"Sending API request. The max tokens is {max_tokens}. Here's the conversation history:" + ) for message in conversation_history: print(f"{message['role']}: {message['content']}") print("\n") - + response = client.messages.create( model=AI_MODEL, max_tokens=max_tokens, messages=conversation_history, - system=system_prompt + system=system_prompt, ) - + content = response.content[0].text if response.content else "" - + if not content: print("Warning: Received empty response from API") return None, True - + return response - + except Exception as e: print(f"Error in API request: {str(e)}") return None, True + def parse_response(response): if not response or not response.content: return None, None, None, None, None, None try: content = json.loads(response.content[0].text) - - text = content.get('text') - code = content.get('code') - code_version = content.get('code_version') - version_description = content.get('version_description') - requirements = content.get('requirements', []) - questions = content.get('questions', []) + + text = content.get("text") + code = content.get("code") + code_version = content.get("code_version") + version_description = content.get("version_description") + requirements = content.get("requirements", []) + questions = content.get("questions", []) return text, questions, code, code_version, version_description, requirements except json.JSONDecodeError: print("Error: Received malformed JSON from the API") return None, None, None, None, None, None + def check_api_connection(): try: response = client.messages.create( model=AI_MODEL, max_tokens=100, - messages=[ - {"role": "user", "content": "Hi Claude, are we communicating?"} - ] + messages=[{"role": "user", "content": "Hi Claude, are we communicating?"}], ) return True, response.content[0].text.strip() except Exception as e: return False, str(e) - + + if __name__ == "__main__": success, message = check_api_connection() - print(f"Connection {'successful' if success else 'failed'}: {message}") \ No newline at end of file + print(f"Connection {'successful' if success else 'failed'}: {message}") diff --git a/codeaide/utils/constants.py b/codeaide/utils/constants.py index 9570889..adc1ad2 100644 --- a/codeaide/utils/constants.py +++ b/codeaide/utils/constants.py @@ -1,7 +1,7 @@ # API Configuration -MAX_TOKENS = 8192 # This is the maximum token limit for the API +MAX_TOKENS = 8192 # This is the maximum token limit for the API AI_MODEL = "claude-3-5-sonnet-20240620" -MAX_RETRIES = 3 # Maximum number of retries for API requests (in case of errors or responses that can't be parsed) +MAX_RETRIES = 3 # Maximum number of retries for API requests (in case of errors or responses that can't be parsed) # UI Configuration CHAT_WINDOW_WIDTH = 800 @@ -11,20 +11,20 @@ # Chat window styling CHAT_WINDOW_WIDTH = 800 CHAT_WINDOW_HEIGHT = 800 -CHAT_WINDOW_BG = 'black' -CHAT_WINDOW_FG = 'white' -USER_MESSAGE_COLOR = 'white' -AI_MESSAGE_COLOR = '#ADD8E6' # Light Blue -USER_FONT = ("Arial", 16, 'normal') -AI_FONT = ("Menlo", 14, 'normal') -AI_EMOJI = '🤖' # Robot emoji +CHAT_WINDOW_BG = "black" +CHAT_WINDOW_FG = "white" +USER_MESSAGE_COLOR = "white" +AI_MESSAGE_COLOR = "#ADD8E6" # Light Blue +USER_FONT = ("Arial", 16, "normal") +AI_FONT = ("Menlo", 14, "normal") +AI_EMOJI = "🤖" # Robot emoji # Code popup styling CODE_WINDOW_WIDTH = 800 CODE_WINDOW_HEIGHT = 800 -CODE_WINDOW_BG = 'black' -CODE_WINDOW_FG = 'white' -CODE_FONT = ("Courier", 14, 'normal') +CODE_WINDOW_BG = "black" +CODE_WINDOW_FG = "white" +CODE_FONT = ("Courier", 14, "normal") # System prompt for API requests SYSTEM_PROMPT = """ diff --git a/codeaide/utils/cost_tracker.py b/codeaide/utils/cost_tracker.py index a795bd5..9a1ead3 100644 --- a/codeaide/utils/cost_tracker.py +++ b/codeaide/utils/cost_tracker.py @@ -1,5 +1,6 @@ from datetime import datetime + class CostTracker: def __init__(self): self.cost_log = [] @@ -10,24 +11,26 @@ def log_request(self, response): # We'll estimate based on the response tokens completion_tokens = response.usage.output_tokens # Estimate prompt tokens (this is not accurate, but it's a rough estimate) - estimated_prompt_tokens = completion_tokens // 2 + estimated_prompt_tokens = completion_tokens // 2 total_tokens = estimated_prompt_tokens + completion_tokens estimated_cost = (total_tokens / 1000) * self.cost_per_1k_tokens - self.cost_log.append({ - 'timestamp': datetime.now(), - 'estimated_prompt_tokens': estimated_prompt_tokens, - 'completion_tokens': completion_tokens, - 'total_tokens': total_tokens, - 'estimated_cost': estimated_cost - }) + self.cost_log.append( + { + "timestamp": datetime.now(), + "estimated_prompt_tokens": estimated_prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens, + "estimated_cost": estimated_cost, + } + ) def get_total_cost(self): - return sum(entry['estimated_cost'] for entry in self.cost_log) + return sum(entry["estimated_cost"] for entry in self.cost_log) def print_summary(self): total_cost = self.get_total_cost() - total_tokens = sum(entry['total_tokens'] for entry in self.cost_log) + total_tokens = sum(entry["total_tokens"] for entry in self.cost_log) print(f"\nTotal estimated cost: ${total_cost:.4f}") print(f"Total tokens used: {total_tokens}") - print(f"Number of API calls: {len(self.cost_log)}") \ No newline at end of file + print(f"Number of API calls: {len(self.cost_log)}") diff --git a/codeaide/utils/environment_manager.py b/codeaide/utils/environment_manager.py index ad89702..e8ebf29 100644 --- a/codeaide/utils/environment_manager.py +++ b/codeaide/utils/environment_manager.py @@ -1,12 +1,15 @@ -import subprocess import os +import subprocess import sys import venv + class EnvironmentManager: def __init__(self, env_name="codeaide_env"): self.env_name = env_name - self.env_path = os.path.join(os.path.expanduser("~"), ".codeaide_envs", self.env_name) + self.env_path = os.path.join( + os.path.expanduser("~"), ".codeaide_envs", self.env_name + ) self.installed_packages = set() self._setup_environment() self._get_installed_packages() @@ -16,28 +19,34 @@ def _setup_environment(self): venv.create(self.env_path, with_pip=True) def _get_installed_packages(self): - pip_path = os.path.join(self.env_path, "bin", "pip") if os.name != 'nt' else os.path.join(self.env_path, "Scripts", "pip.exe") + pip_path = ( + os.path.join(self.env_path, "bin", "pip") + if os.name != "nt" + else os.path.join(self.env_path, "Scripts", "pip.exe") + ) result = subprocess.run( - f"{pip_path} freeze", - shell=True, check=True, capture_output=True, text=True + f"{pip_path} freeze", shell=True, check=True, capture_output=True, text=True ) self.installed_packages = { - pkg.split('==')[0].lower() for pkg in result.stdout.split('\n') if pkg + pkg.split("==")[0].lower() for pkg in result.stdout.split("\n") if pkg } def install_requirements(self, requirements_file): - pip_path = os.path.join(self.env_path, "bin", "pip") if os.name != 'nt' else os.path.join(self.env_path, "Scripts", "pip.exe") - with open(requirements_file, 'r') as f: + pip_path = ( + os.path.join(self.env_path, "bin", "pip") + if os.name != "nt" + else os.path.join(self.env_path, "Scripts", "pip.exe") + ) + with open(requirements_file, "r") as f: required_packages = {line.strip().lower() for line in f if line.strip()} packages_to_install = required_packages - self.installed_packages if packages_to_install: - packages_str = ' '.join(packages_to_install) + packages_str = " ".join(packages_to_install) try: subprocess.run( - f"{pip_path} install {packages_str}", - shell=True, check=True + f"{pip_path} install {packages_str}", shell=True, check=True ) self.installed_packages.update(packages_to_install) return list(packages_to_install) @@ -47,7 +56,7 @@ def install_requirements(self, requirements_file): return [] def get_activation_command(self): - if os.name == 'nt': # Windows + if os.name == "nt": # Windows return f"call {os.path.join(self.env_path, 'Scripts', 'activate.bat')}" else: # Unix-like - return f"source {os.path.join(self.env_path, 'bin', 'activate')}" \ No newline at end of file + return f"source {os.path.join(self.env_path, 'bin', 'activate')}" diff --git a/codeaide/utils/file_handler.py b/codeaide/utils/file_handler.py index 498db31..9f7768d 100644 --- a/codeaide/utils/file_handler.py +++ b/codeaide/utils/file_handler.py @@ -1,10 +1,13 @@ import os import shutil + class FileHandler: def __init__(self, base_dir=None): if base_dir is None: - self.base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + self.base_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) else: self.base_dir = base_dir self.output_dir = os.path.join(self.base_dir, "generated_code") @@ -21,7 +24,6 @@ def clear_output_dir(self): os.makedirs(self.output_dir) def save_code(self, code, version, version_description, requirements=[]): - code_path = os.path.join(self.output_dir, f"generated_script_{version}.py") requirements_path = os.path.join(self.output_dir, f"requirements_{version}.txt") abs_code_path = os.path.abspath(code_path) @@ -37,10 +39,10 @@ def save_code(self, code, version, version_description, requirements=[]): print(f"Error saving file: {str(e)}") print(f"Adding version {version} to versions_dict") self.versions_dict[version] = { - 'version_description': version_description, - 'requirements': requirements, - 'code_path': abs_code_path, - 'requirements_path': abs_req_path + "version_description": version_description, + "requirements": requirements, + "code_path": abs_code_path, + "requirements_path": abs_req_path, } print(f"Current versions dict: {self.versions_dict}") return code_path @@ -63,4 +65,4 @@ def get_code(self, version): def get_requirements(self, version): file_path = os.path.join(self.output_dir, f"requirements_{version}.txt") with open(file_path, "r") as file: - return file.read().splitlines() \ No newline at end of file + return file.read().splitlines() diff --git a/codeaide/utils/general_utils.py b/codeaide/utils/general_utils.py index 74ad75d..8f56835 100644 --- a/codeaide/utils/general_utils.py +++ b/codeaide/utils/general_utils.py @@ -1,18 +1,22 @@ import os + import yaml from PyQt5.QtGui import QFont - + # Store the path of the general_utils.py file UTILS_DIR = os.path.dirname(os.path.abspath(__file__)) + def get_project_root(): """Get the project root directory.""" # Always use UTILS_DIR as the starting point - return os.path.abspath(os.path.join(UTILS_DIR, '..', '..')) + return os.path.abspath(os.path.join(UTILS_DIR, "..", "..")) + def get_examples_file_path(): """Get the path to the examples.yaml file.""" - return os.path.join(get_project_root(), 'codeaide', 'examples.yaml') + return os.path.join(get_project_root(), "codeaide", "examples.yaml") + def load_examples(): """Load and return all examples from the YAML file.""" @@ -20,11 +24,11 @@ def load_examples(): if not os.path.exists(examples_file): print(f"Examples file not found: {examples_file}") return [] - + try: - with open(examples_file, 'r', encoding='utf-8') as file: + with open(examples_file, "r", encoding="utf-8") as file: data = yaml.safe_load(file) - return data.get('examples', []) + return data.get("examples", []) except yaml.YAMLError as e: print(f"YAML Error: {str(e)}") return [] @@ -32,6 +36,7 @@ def load_examples(): print(f"Unexpected error: {str(e)}") return [] + def set_font(font_tuple): if len(font_tuple) == 2: font_family, font_size = font_tuple @@ -40,29 +45,30 @@ def set_font(font_tuple): font_family, font_size, font_style = font_tuple else: raise ValueError("Font tuple must be of length 2 or 3") - + qfont = QFont(font_family, font_size) - + if font_style == "italic": qfont.setStyle(QFont.StyleItalic) elif font_style == "bold": qfont.setWeight(QFont.Bold) # "normal" is the default, so we don't need to do anything for it - + return qfont + def format_chat_message(sender, message, font, color): qfont = set_font(font) font_family = qfont.family() font_size = qfont.pointSize() font_style = "italic" if qfont.style() == QFont.StyleItalic else "normal" - - formatted_message = message.replace('\n', '
') - - html_message = f''' + + formatted_message = message.replace("\n", "
") + + html_message = f""" {sender}: {formatted_message} - ''' - - return html_message \ No newline at end of file + """ + + return html_message diff --git a/codeaide/utils/terminal_manager.py b/codeaide/utils/terminal_manager.py index 59fbc90..fc442a0 100644 --- a/codeaide/utils/terminal_manager.py +++ b/codeaide/utils/terminal_manager.py @@ -1,8 +1,9 @@ -import subprocess -import tempfile +import atexit import os +import subprocess import sys -import atexit +import tempfile + class TerminalManager: def __init__(self): @@ -10,21 +11,23 @@ def __init__(self): atexit.register(self.cleanup) def run_in_terminal(self, script_content): - if sys.platform == 'darwin': # macOS + if sys.platform == "darwin": # macOS return self._run_in_macos_terminal(script_content) - elif sys.platform.startswith('linux'): + elif sys.platform.startswith("linux"): return self._run_in_linux_terminal(script_content) - elif sys.platform.startswith('win'): + elif sys.platform.startswith("win"): return self._run_in_windows_terminal(script_content) else: raise OSError("Unsupported operating system") def _run_in_macos_terminal(self, script_content): - with tempfile.NamedTemporaryFile(mode='w', suffix='.command', delete=False) as f: - f.write('#!/bin/bash\n') + with tempfile.NamedTemporaryFile( + mode="w", suffix=".command", delete=False + ) as f: + f.write("#!/bin/bash\n") f.write(script_content) f.write('\necho "Script execution completed. You can close this window."\n') - f.write('exec bash\n') # Keep the terminal open + f.write("exec bash\n") # Keep the terminal open os.chmod(f.name, 0o755) process = subprocess.Popen(["open", "-a", "Terminal", f.name]) self.terminals.append((process, f.name)) @@ -36,7 +39,7 @@ def _run_in_linux_terminal(self, script_content): echo "Script execution completed. You can close this window." exec bash """ - with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f: f.write(script_with_shell) os.chmod(f.name, 0o755) process = subprocess.Popen(["x-terminal-emulator", "-e", f.name]) @@ -50,7 +53,7 @@ def _run_in_windows_terminal(self, script_content): echo Script execution completed. You can close this window. cmd /k """ - with tempfile.NamedTemporaryFile(mode='w', suffix='.bat', delete=False) as f: + with tempfile.NamedTemporaryFile(mode="w", suffix=".bat", delete=False) as f: f.write(bat_script) process = subprocess.Popen(["start", "cmd", "/c", f.name], shell=True) self.terminals.append((process, f.name)) @@ -61,4 +64,4 @@ def cleanup(self): try: os.remove(file_name) except: - pass # Ignore errors during cleanup \ No newline at end of file + pass # Ignore errors during cleanup diff --git a/tests/conftest.py b/tests/conftest.py index def25da..c4a59d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,13 @@ -import pytest from unittest.mock import Mock + import anthropic +import pytest + @pytest.fixture def mock_anthropic_client(monkeypatch): mock_client = Mock(spec=anthropic.Anthropic) mock_messages = Mock() mock_client.messages = mock_messages - monkeypatch.setattr('codeaide.utils.api_utils.client', mock_client) - return mock_client \ No newline at end of file + monkeypatch.setattr("codeaide.utils.api_utils.client", mock_client) + return mock_client diff --git a/tests/utils/test_api_utils.py b/tests/utils/test_api_utils.py index 21fea94..65421ed 100644 --- a/tests/utils/test_api_utils.py +++ b/tests/utils/test_api_utils.py @@ -1,26 +1,31 @@ import json -import pytest -from unittest.mock import Mock from collections import namedtuple -from codeaide.utils.api_utils import parse_response, send_api_request, check_api_connection -from codeaide.utils.constants import MAX_TOKENS, AI_MODEL, SYSTEM_PROMPT +from unittest.mock import Mock + +import pytest from anthropic import APIError +from codeaide.utils.api_utils import ( + check_api_connection, + parse_response, + send_api_request, +) +from codeaide.utils.constants import AI_MODEL, MAX_TOKENS, SYSTEM_PROMPT + # Mock Response object -Response = namedtuple('Response', ['content']) -TextBlock = namedtuple('TextBlock', ['text']) +Response = namedtuple("Response", ["content"]) +TextBlock = namedtuple("TextBlock", ["text"]) pytestmark = [ pytest.mark.send_api_request, pytest.mark.parse_response, - pytest.mark.api_connection + pytest.mark.api_connection, ] + class TestSendAPIRequest: def test_send_api_request_success(self, mock_anthropic_client): - conversation_history = [ - {"role": "user", "content": "Hello, Claude!"} - ] + conversation_history = [{"role": "user", "content": "Hello, Claude!"}] mock_response = Mock() mock_response.content = [Mock(text="Hello! How can I assist you today?")] mock_anthropic_client.messages.create.return_value = mock_response @@ -31,14 +36,12 @@ def test_send_api_request_success(self, mock_anthropic_client): model=AI_MODEL, max_tokens=MAX_TOKENS, messages=conversation_history, - system=SYSTEM_PROMPT + system=SYSTEM_PROMPT, ) assert result == mock_response def test_send_api_request_empty_response(self, mock_anthropic_client): - conversation_history = [ - {"role": "user", "content": "Hello, Claude!"} - ] + conversation_history = [{"role": "user", "content": "Hello, Claude!"}] mock_response = Mock() mock_response.content = [] mock_anthropic_client.messages.create.return_value = mock_response @@ -48,14 +51,12 @@ def test_send_api_request_empty_response(self, mock_anthropic_client): assert result == (None, True) def test_send_api_request_api_error(self, mock_anthropic_client): - conversation_history = [ - {"role": "user", "content": "Hello, Claude!"} - ] + conversation_history = [{"role": "user", "content": "Hello, Claude!"}] mock_request = Mock() mock_anthropic_client.messages.create.side_effect = APIError( request=mock_request, message="API Error", - body={"error": {"message": "API Error"}} + body={"error": {"message": "API Error"}}, ) result = send_api_request(conversation_history) @@ -63,9 +64,7 @@ def test_send_api_request_api_error(self, mock_anthropic_client): assert result == (None, True) def test_send_api_request_custom_max_tokens(self, mock_anthropic_client): - conversation_history = [ - {"role": "user", "content": "Hello, Claude!"} - ] + conversation_history = [{"role": "user", "content": "Hello, Claude!"}] custom_max_tokens = 500 mock_response = Mock() mock_response.content = [Mock(text="Hello! How can I assist you today?")] @@ -77,10 +76,11 @@ def test_send_api_request_custom_max_tokens(self, mock_anthropic_client): model=AI_MODEL, max_tokens=custom_max_tokens, messages=conversation_history, - system=SYSTEM_PROMPT + system=SYSTEM_PROMPT, ) assert result == mock_response + class TestParseResponse: def test_parse_response_empty(self): result = parse_response(None) @@ -98,11 +98,18 @@ def test_parse_response_valid(self): "code_version": "1.0", "version_description": "Initial version", "requirements": ["pytest"], - "questions": ["What does this code do?"] + "questions": ["What does this code do?"], } response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - + ( + text, + questions, + code, + code_version, + version_description, + requirements, + ) = parse_response(response) + assert text == "Sample text" assert questions == ["What does this code do?"] assert code == "print('Hello, World!')" @@ -111,13 +118,17 @@ def test_parse_response_valid(self): assert requirements == ["pytest"] def test_parse_response_missing_fields(self): - content = { - "text": "Sample text", - "code": "print('Hello, World!')" - } + content = {"text": "Sample text", "code": "print('Hello, World!')"} response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - + ( + text, + questions, + code, + code_version, + version_description, + requirements, + ) = parse_response(response) + assert text == "Sample text" assert questions == [] assert code == "print('Hello, World!')" @@ -132,11 +143,18 @@ def test_parse_response_complex_code(self): "code_version": "1.1", "version_description": "Added function", "requirements": [], - "questions": [] + "questions": [], } response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - + ( + text, + questions, + code, + code_version, + version_description, + requirements, + ) = parse_response(response) + assert text == "Complex code example" assert code == 'def hello():\n print("Hello, World!")' assert code_version == "1.1" @@ -149,11 +167,18 @@ def test_parse_response_escaped_quotes(self): "code_version": "1.2", "version_description": "Added escaped quotes", "requirements": [], - "questions": [] + "questions": [], } response = Response(content=[TextBlock(text=json.dumps(content))]) - text, questions, code, code_version, version_description, requirements = parse_response(response) - + ( + text, + questions, + code, + code_version, + version_description, + requirements, + ) = parse_response(response) + assert text == 'Text with "quotes"' assert code == 'print("Hello, \\"World!\\"")\nprint(\'Single quotes\')' assert code_version == "1.2" @@ -164,6 +189,7 @@ def test_parse_response_malformed_json(self): result = parse_response(response) assert result == (None, None, None, None, None, None) + class TestAPIConnection: def check_api_connection_success(self, mock_anthropic_client): mock_response = Mock() @@ -174,7 +200,9 @@ def check_api_connection_success(self, mock_anthropic_client): assert result[1] == "Yes, we are communicating." def check_api_connection_failure(self, mock_anthropic_client): - mock_anthropic_client.messages.create.side_effect = Exception("Connection failed") + mock_anthropic_client.messages.create.side_effect = Exception( + "Connection failed" + ) result = check_api_connection() assert result[0] == False - assert "Connection failed" in result[1] \ No newline at end of file + assert "Connection failed" in result[1] diff --git a/tests/utils/test_file_handler.py b/tests/utils/test_file_handler.py index 564031d..8f47a70 100644 --- a/tests/utils/test_file_handler.py +++ b/tests/utils/test_file_handler.py @@ -1,79 +1,89 @@ -import pytest import os import tempfile + +import pytest + from codeaide.utils.file_handler import FileHandler + @pytest.fixture def file_handler(): with tempfile.TemporaryDirectory() as temp_dir: handler = FileHandler(base_dir=temp_dir) yield handler + def test_clear_output_dir(file_handler): # Create a file in the output directory test_file = os.path.join(file_handler.output_dir, "test.txt") with open(test_file, "w") as f: f.write("test") - + file_handler.clear_output_dir() - + assert os.path.exists(file_handler.output_dir) assert len(os.listdir(file_handler.output_dir)) == 0 + def test_save_code(file_handler): code = "print('Hello, World!')" version = "1.0" description = "Initial version" requirements = ["pytest"] - + code_path = file_handler.save_code(code, version, description, requirements) - + assert os.path.exists(code_path) with open(code_path, "r") as f: assert f.read() == code - + assert version in file_handler.versions_dict - assert file_handler.versions_dict[version]['version_description'] == description - assert file_handler.versions_dict[version]['requirements'] == requirements + assert file_handler.versions_dict[version]["version_description"] == description + assert file_handler.versions_dict[version]["requirements"] == requirements + def test_save_requirements(file_handler): requirements = ["pytest", "requests"] version = "1.0" - + req_path = file_handler.save_requirements(requirements, version) - + assert os.path.exists(req_path) with open(req_path, "r") as f: assert f.read().splitlines() == requirements + def test_get_versions_dict(file_handler): file_handler.save_code("code1", "1.0", "Version 1") file_handler.save_code("code2", "2.0", "Version 2") - + versions_dict = file_handler.get_versions_dict() - + assert "1.0" in versions_dict assert "2.0" in versions_dict + def test_get_code(file_handler): original_code = "print('Test')" file_handler.save_code(original_code, "1.0", "Test version") - + retrieved_code = file_handler.get_code("1.0") - + assert retrieved_code == original_code + def test_get_requirements(file_handler): original_requirements = ["pytest", "requests"] file_handler.save_code("code", "1.0", "Test", original_requirements) - + retrieved_requirements = file_handler.get_requirements("1.0") - + assert retrieved_requirements == original_requirements + def test_nonexistent_version(file_handler): with pytest.raises(FileNotFoundError): file_handler.get_code("nonexistent") - + with pytest.raises(FileNotFoundError): - file_handler.get_requirements("nonexistent") \ No newline at end of file + file_handler.get_requirements("nonexistent")