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

fix: Implement rate limit handling with 60s wait #668

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/Installation/INSTALLATION.md
Original file line number Diff line number Diff line change
@@ -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`
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
numpy>=1.24.0
flask
flask-cors
toml
Expand Down
13 changes: 13 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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",
)
14 changes: 13 additions & 1 deletion src/init.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -8,7 +20,7 @@ def init_devika():

logger.info("Initializing Devika...")
logger.info("checking configurations...")

config = Config()

sqlite_db = config.get_sqlite_db()
Expand Down
34 changes: 22 additions & 12 deletions src/llm/groq_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from groq import Groq as _Groq
import requests
from requests.exceptions import HTTPError

from src.config import Config

Expand All @@ -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
19 changes: 14 additions & 5 deletions src/services/utils.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -25,7 +34,7 @@ def wrapper(*args, **kwargs):
return False
return wrapper


class InvalidResponseError(Exception):
pass

Expand Down Expand Up @@ -87,4 +96,4 @@ def wrapper(*args, **kwargs):
# raise InvalidResponseError("Failed to parse response as JSON")
return False

return wrapper
return wrapper
39 changes: 39 additions & 0 deletions tests/test_groq_client.py
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 25 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -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)
42 changes: 42 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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)