From 9ec4699d7903679189ae798f0835aaab2496911a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:07:16 +0000 Subject: [PATCH 1/2] fix: Add numpy dependency and environment validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add numpy>=1.24.0 to requirements.txt - Create setup.py with core dependencies - Add environment validation in init_devika - Update installation documentation - Add tests for numpy validation Fixes #372 Co-Authored-By: Erkin Alp Güney --- docs/Installation/INSTALLATION.md | 32 +++++++++++++++++++++++++++++++ pyproject.toml | 8 ++++++++ requirements.txt | 1 + setup.py | 13 +++++++++++++ src/init.py | 14 +++++++++++++- tests/test_init.py | 25 ++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 docs/Installation/INSTALLATION.md create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 tests/test_init.py diff --git a/docs/Installation/INSTALLATION.md b/docs/Installation/INSTALLATION.md new file mode 100644 index 00000000..22139435 --- /dev/null +++ b/docs/Installation/INSTALLATION.md @@ -0,0 +1,32 @@ +# Installation Guide + +## Prerequisites +- Python 3.8 or higher +- pip (Python package installer) + +## Installation Steps + +1. Clone the repository: +```bash +git clone https://github.com/stitionai/devika.git +cd devika +``` + +2. Install dependencies: +```bash +pip install -r requirements.txt +``` + +## Common Issues + +### Missing numpy dependency +If you encounter an error about numpy not being available, install it explicitly: +```bash +pip install numpy>=1.24.0 +``` + +## Troubleshooting +If you encounter any installation issues: +1. Ensure you have Python 3.8 or higher installed +2. Try upgrading pip: `pip install --upgrade pip` +3. Install numpy explicitly if needed: `pip install numpy>=1.24.0` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..bd883f4e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["setuptools>=42.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +addopts = "-v" diff --git a/requirements.txt b/requirements.txt index 91666960..83a8cae2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +numpy>=1.24.0 flask flask-cors toml diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..513f4a7c --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup, find_packages + +setup( + name="devika", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "numpy>=1.24.0", + "requests>=2.25.1", + "pytest>=6.0.0", + ], + python_requires=">=3.8", +) diff --git a/src/init.py b/src/init.py index abb8b95b..16a3ffcc 100644 --- a/src/init.py +++ b/src/init.py @@ -1,4 +1,16 @@ import os +import importlib.util + +# Validate numpy dependency first +try: + import numpy + print(f"numpy version {numpy.__version__} found") +except ImportError: + raise ImportError( + "numpy is required but not installed. Please install it using:\n" + "pip install numpy>=1.24.0" + ) + from src.config import Config from src.logger import Logger @@ -8,7 +20,7 @@ def init_devika(): logger.info("Initializing Devika...") logger.info("checking configurations...") - + config = Config() sqlite_db = config.get_sqlite_db() diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 00000000..153955c8 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,25 @@ +import pytest +from unittest.mock import patch +import numpy + +def test_numpy_import(): + """Test that numpy can be imported and has correct version.""" + assert numpy.__version__ >= "1.24.0" + +def test_init_devika_numpy_validation(): + """Test that init_devika validates numpy dependency.""" + from src.init import init_devika + + # Should not raise any ImportError + init_devika() + +@patch("importlib.import_module") +def test_init_devika_numpy_missing(mock_import): + """Test that init_devika handles missing numpy correctly.""" + mock_import.side_effect = ImportError("No module named numpy") + + with pytest.raises(ImportError) as exc_info: + from src.init import init_devika + init_devika() + + assert "numpy is required but not installed" in str(exc_info.value) From 73c14f2af60c27415cbc7c10d4e8c27d0f4532ff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:19:14 +0000 Subject: [PATCH 2/2] fix: Implement rate limit handling with 60s wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 60-second wait on HTTP 429 responses - Properly handle Groq API rate limit errors - Add comprehensive test coverage - Improve error messaging and logging Fixes #524 Co-Authored-By: Erkin Alp Güney --- src/llm/groq_client.py | 34 ++++++++++++++++++++----------- src/services/utils.py | 19 +++++++++++++----- tests/test_groq_client.py | 39 ++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 42 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 tests/test_groq_client.py create mode 100644 tests/test_utils.py diff --git a/src/llm/groq_client.py b/src/llm/groq_client.py index 828c9cc6..04f2002c 100644 --- a/src/llm/groq_client.py +++ b/src/llm/groq_client.py @@ -1,4 +1,6 @@ from groq import Groq as _Groq +import requests +from requests.exceptions import HTTPError from src.config import Config @@ -10,15 +12,23 @@ def __init__(self): self.client = _Groq(api_key=api_key) def inference(self, model_id: str, prompt: str) -> str: - chat_completion = self.client.chat.completions.create( - messages=[ - { - "role": "user", - "content": prompt.strip(), - } - ], - model=model_id, - temperature=0 - ) - - return chat_completion.choices[0].message.content + try: + chat_completion = self.client.chat.completions.create( + messages=[ + { + "role": "user", + "content": prompt.strip(), + } + ], + model=model_id, + temperature=0 + ) + return chat_completion.choices[0].message.content + except Exception as e: + # Convert Groq API errors to HTTPError for consistent handling + if "rate limit" in str(e).lower(): + response = requests.Response() + response.status_code = 429 + response._content = str(e).encode() + raise HTTPError(response=response) + raise diff --git a/src/services/utils.py b/src/services/utils.py index 6678168e..e35e4e72 100644 --- a/src/services/utils.py +++ b/src/services/utils.py @@ -1,6 +1,7 @@ # create wrapper function that will has retry logic of 5 times import sys import time +import requests from functools import wraps import json @@ -11,9 +12,17 @@ def wrapper(*args, **kwargs): max_tries = 5 tries = 0 while tries < max_tries: - result = func(*args, **kwargs) - if result: - return result + try: + result = func(*args, **kwargs) + if result: + return result + except requests.exceptions.HTTPError as e: + if e.response.status_code == 429: + print("Rate limit reached, waiting 60 seconds...") + emit_agent("info", {"type": "warning", "message": "Rate limit reached, waiting 60 seconds..."}) + time.sleep(60) + continue + raise print("Invalid response from the model, I'm trying again...") emit_agent("info", {"type": "warning", "message": "Invalid response from the model, trying again..."}) tries += 1 @@ -25,7 +34,7 @@ def wrapper(*args, **kwargs): return False return wrapper - + class InvalidResponseError(Exception): pass @@ -87,4 +96,4 @@ def wrapper(*args, **kwargs): # raise InvalidResponseError("Failed to parse response as JSON") return False - return wrapper \ No newline at end of file + return wrapper diff --git a/tests/test_groq_client.py b/tests/test_groq_client.py new file mode 100644 index 00000000..9358a343 --- /dev/null +++ b/tests/test_groq_client.py @@ -0,0 +1,39 @@ +import pytest +from unittest.mock import Mock, patch +from requests.exceptions import HTTPError + +from src.llm.groq_client import Groq + + +def test_groq_rate_limit_handling(): + groq = Groq() + + # Mock the Groq client to simulate rate limit error + mock_client = Mock() + mock_client.chat.completions.create.side_effect = Exception( + 'Rate limit reached for model `mixtral-8x7b-32768`. Please try again in 7.164s.' + ) + groq.client = mock_client + + # Test that rate limit error is converted to HTTPError + with pytest.raises(HTTPError) as exc_info: + groq.inference("mixtral-8x7b-32768", "test prompt") + + assert exc_info.value.response.status_code == 429 + assert "rate limit" in str(exc_info.value.response.content.decode()).lower() + + +def test_groq_other_error_handling(): + groq = Groq() + + # Mock the Groq client to simulate other error + mock_client = Mock() + mock_client.chat.completions.create.side_effect = Exception("Some other error") + groq.client = mock_client + + # Test that other errors are re-raised as-is + with pytest.raises(Exception) as exc_info: + groq.inference("mixtral-8x7b-32768", "test prompt") + + assert "Some other error" in str(exc_info.value) + assert not isinstance(exc_info.value, HTTPError) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..ccbe0cd2 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,42 @@ +import pytest +import time +import requests +from unittest.mock import Mock, patch +from src.services.utils import retry_wrapper + +def test_retry_wrapper_rate_limit(): + # Mock a function that raises rate limit error + @retry_wrapper + def rate_limited_func(): + response = Mock(spec=requests.Response) + response.status_code = 429 + response.json.return_value = { + 'error': { + 'message': 'Rate limit reached', + 'type': 'tokens', + 'code': 'rate_limit_exceeded' + } + } + raise requests.exceptions.HTTPError(response=response) + + # Test that it waits 60 seconds on rate limit + with patch('time.sleep') as mock_sleep: + with pytest.raises(requests.exceptions.HTTPError): + rate_limited_func() + # Verify it attempted to sleep for 60 seconds + assert mock_sleep.call_args[0][0] == 60 + +def test_retry_wrapper_other_errors(): + # Mock a function that raises other HTTP errors + @retry_wrapper + def other_error_func(): + response = Mock(spec=requests.Response) + response.status_code = 500 + raise requests.exceptions.HTTPError(response=response) + + # Test that it retries with default backoff + with patch('time.sleep') as mock_sleep: + with pytest.raises(requests.exceptions.HTTPError): + other_error_func() + # Verify it used shorter retry delays + assert all(call[0][0] < 60 for call in mock_sleep.call_args_list)