Skip to content
Merged
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
15 changes: 11 additions & 4 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,31 @@ Note all the tools are in python. So in the case you need to do batch processing

You always have an LLM at your side to help you with the task. For simple tasks, you could invoke the LLM by running the following command:
```
py310/bin/python ./tools/llm_api.py --prompt "What is the capital of France?" --provider "anthropic"
venv/bin/python ./tools/llm_api.py --prompt "What is the capital of France?" --provider "anthropic"
```

The LLM API supports multiple providers:
- OpenAI (default, model: gpt-4o)
- DeepSeek (model: deepseek-chat)
- Anthropic (model: claude-3-sonnet-20240229)
- Gemini (model: gemini-pro)
- Local LLM (model: Qwen/Qwen2.5-32B-Instruct-AWQ)

But usually it's a better idea to check the content of the file and use the APIs in the `tools/llm_api.py` file to invoke the LLM if needed.

## Web browser

You could use the `tools/web_scraper.py` file to scrape the web.
```
py310/bin/python ./tools/web_scraper.py --max-concurrent 3 URL1 URL2 URL3
venv/bin/python ./tools/web_scraper.py --max-concurrent 3 URL1 URL2 URL3
```
This will output the content of the web pages.

## Search engine

You could use the `tools/search_engine.py` file to search the web.
```
py310/bin/python ./tools/search_engine.py "your search keywords"
venv/bin/python ./tools/search_engine.py "your search keywords"
```
This will output the search results in the following format:
```
Expand All @@ -48,7 +55,7 @@ If needed, you can further use the `web_scraper.py` file to scrape the web page

## User Specified Lessons

- You have a python venv in ./py310.
- You have a python venv in ./venv.
- Include info useful for debugging in the program output.
- Read the file before you try to edit it.
- Use LLM to perform flexible text understanding tasks. First test on a few files. After success, make it parallel.
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would kindly suggest not including .env.example in the .gitignore file.

The .env.example file serves as a helpful reference for environment variables, allowing developers to understand the required configuration for the project. Since it doesn’t contain sensitive information, it is safe to keep it in the repository. Adding it to .gitignore may limit its usefulness as a guide for the team.

For better collaboration and clarity, it’s recommended to keep .env.example in the repository.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah my bad! I meant to add .env only but let .env.example slipped in. Sorry! I will revert it.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ yarn-error.log*

# local env files
.env*.local
.env
.env.example

# vercel
.vercel
Expand All @@ -44,4 +46,7 @@ credentials.json
**/__pycache__/

# python virtual environment
/py310/
/venv/

# pytest
.pytest_cache/
273 changes: 211 additions & 62 deletions tests/test_llm_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import unittest
from unittest.mock import patch, MagicMock
from tools.llm_api import create_llm_client, query_llm
from unittest.mock import patch, MagicMock, mock_open
from tools.llm_api import create_llm_client, query_llm, load_environment
import os
import google.generativeai as genai
import io
import sys

def is_llm_configured():
"""Check if LLM is configured by trying to connect to the server"""
Expand All @@ -16,97 +19,243 @@ def is_llm_configured():
skip_llm_tests = not is_llm_configured()
skip_message = "Skipping LLM tests as LLM is not configured. This is normal if you haven't set up a local LLM server."

class TestEnvironmentLoading(unittest.TestCase):
def setUp(self):
# Save original environment
self.original_env = dict(os.environ)
# Clear environment variables we're testing
for key in ['TEST_VAR']:
if key in os.environ:
del os.environ[key]

def tearDown(self):
# Restore original environment
os.environ.clear()
os.environ.update(self.original_env)

@patch('pathlib.Path.exists')
@patch('tools.llm_api.load_dotenv')
@patch('builtins.open')
def test_environment_loading_precedence(self, mock_open, mock_load_dotenv, mock_exists):
# Mock all env files exist
mock_exists.return_value = True

# Mock file contents
mock_file = MagicMock()
mock_file.__enter__.return_value = io.StringIO('TEST_VAR=value\n')
mock_open.return_value = mock_file

# Mock different values for TEST_VAR in different files
def load_dotenv_side_effect(dotenv_path, **kwargs):
if '.env.local' in str(dotenv_path):
os.environ['TEST_VAR'] = 'local'
elif '.env' in str(dotenv_path):
if 'TEST_VAR' not in os.environ: # Only set if not already set
os.environ['TEST_VAR'] = 'default'
elif '.env.example' in str(dotenv_path):
if 'TEST_VAR' not in os.environ: # Only set if not already set
os.environ['TEST_VAR'] = 'example'
mock_load_dotenv.side_effect = load_dotenv_side_effect

# Load environment
load_environment()

# Verify precedence (.env.local should win)
self.assertEqual(os.environ.get('TEST_VAR'), 'local')

# Verify order of loading
calls = mock_load_dotenv.call_args_list
self.assertEqual(len(calls), 3)
self.assertTrue(str(calls[0][1]['dotenv_path']).endswith('.env.local'))
self.assertTrue(str(calls[1][1]['dotenv_path']).endswith('.env'))
self.assertTrue(str(calls[2][1]['dotenv_path']).endswith('.env.example'))

@patch('pathlib.Path.exists')
@patch('tools.llm_api.load_dotenv')
def test_environment_loading_no_files(self, mock_load_dotenv, mock_exists):
# Mock no env files exist
mock_exists.return_value = False

# Load environment
load_environment()

# Verify load_dotenv was not called
mock_load_dotenv.assert_not_called()

class TestLLMAPI(unittest.TestCase):
def setUp(self):
# Create a mock OpenAI client
self.mock_client = MagicMock()
self.mock_response = MagicMock()
self.mock_choice = MagicMock()
self.mock_message = MagicMock()
# Create mock clients for different providers
self.mock_openai_client = MagicMock()
self.mock_anthropic_client = MagicMock()
self.mock_gemini_client = MagicMock()

# Set up the mock response structure
self.mock_message.content = "Test response"
self.mock_choice.message = self.mock_message
self.mock_response.choices = [self.mock_choice]
# Set up OpenAI-style response
self.mock_openai_response = MagicMock()
self.mock_openai_choice = MagicMock()
self.mock_openai_message = MagicMock()
self.mock_openai_message.content = "Test OpenAI response"
self.mock_openai_choice.message = self.mock_openai_message
self.mock_openai_response.choices = [self.mock_openai_choice]
self.mock_openai_client.chat.completions.create.return_value = self.mock_openai_response

# Set up the mock client's chat.completions.create method
self.mock_client.chat.completions.create.return_value = self.mock_response
# Set up Anthropic-style response
self.mock_anthropic_response = MagicMock()
self.mock_anthropic_content = MagicMock()
self.mock_anthropic_content.text = "Test Anthropic response"
self.mock_anthropic_response.content = [self.mock_anthropic_content]
self.mock_anthropic_client.messages.create.return_value = self.mock_anthropic_response

# Set up Gemini-style response
self.mock_gemini_model = MagicMock()
self.mock_gemini_response = MagicMock()
self.mock_gemini_response.text = "Test Gemini response"
self.mock_gemini_model.generate_content.return_value = self.mock_gemini_response
self.mock_gemini_client.GenerativeModel.return_value = self.mock_gemini_model

# Mock environment variables
self.env_patcher = patch.dict('os.environ', {
'OPENAI_API_KEY': 'test-openai-key',
'DEEPSEEK_API_KEY': 'test-deepseek-key',
'ANTHROPIC_API_KEY': 'test-anthropic-key',
'GOOGLE_API_KEY': 'test-google-key'
})
self.env_patcher.start()

def tearDown(self):
self.env_patcher.stop()

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.OpenAI')
def test_create_llm_client(self, mock_openai):
# Test client creation with default provider (openai)
mock_openai.return_value = self.mock_client
client = create_llm_client() # 使用預設 provider

# Verify OpenAI was called with correct parameters
def test_create_openai_client(self, mock_openai):
mock_openai.return_value = self.mock_openai_client
client = create_llm_client("openai")
mock_openai.assert_called_once_with(api_key='test-openai-key')
self.assertEqual(client, self.mock_openai_client)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.OpenAI')
def test_create_deepseek_client(self, mock_openai):
mock_openai.return_value = self.mock_openai_client
client = create_llm_client("deepseek")
mock_openai.assert_called_once_with(
api_key=os.getenv('OPENAI_API_KEY') # 使用環境變數中的 API key
api_key='test-deepseek-key',
base_url="https://api.deepseek.com/v1"
)

self.assertEqual(client, self.mock_client)
self.assertEqual(client, self.mock_openai_client)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.Anthropic')
def test_create_anthropic_client(self, mock_anthropic):
mock_anthropic.return_value = self.mock_anthropic_client
client = create_llm_client("anthropic")
mock_anthropic.assert_called_once_with(api_key='test-anthropic-key')
self.assertEqual(client, self.mock_anthropic_client)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.genai')
def test_create_gemini_client(self, mock_genai):
client = create_llm_client("gemini")
mock_genai.configure.assert_called_once_with(api_key='test-google-key')
self.assertEqual(client, mock_genai)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.OpenAI')
def test_create_local_client(self, mock_openai):
mock_openai.return_value = self.mock_openai_client
client = create_llm_client("local")
mock_openai.assert_called_once_with(
base_url="http://192.168.180.137:8006/v1",
api_key="not-needed"
)
self.assertEqual(client, self.mock_openai_client)

@unittest.skipIf(skip_llm_tests, skip_message)
def test_create_invalid_provider(self):
with self.assertRaises(ValueError):
create_llm_client("invalid_provider")

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_llm_success(self, mock_create_client):
# Set up mock
mock_create_client.return_value = self.mock_client

# Test query with default provider
response = query_llm("Test prompt") # 使用預設 provider

# Verify response
self.assertEqual(response, "Test response")

# Verify client was called correctly
self.mock_client.chat.completions.create.assert_called_once_with(
model="gpt-3.5-turbo", # 使用 OpenAI 的預設模型
def test_query_openai(self, mock_create_client):
mock_create_client.return_value = self.mock_openai_client
response = query_llm("Test prompt", provider="openai")
self.assertEqual(response, "Test OpenAI response")
self.mock_openai_client.chat.completions.create.assert_called_once_with(
model="gpt-4o",
messages=[{"role": "user", "content": "Test prompt"}],
temperature=0.7
)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_llm_with_custom_model(self, mock_create_client):
# Set up mock
mock_create_client.return_value = self.mock_client

# Test query with custom model
def test_query_deepseek(self, mock_create_client):
mock_create_client.return_value = self.mock_openai_client
response = query_llm("Test prompt", provider="deepseek")
self.assertEqual(response, "Test OpenAI response")
self.mock_openai_client.chat.completions.create.assert_called_once_with(
model="deepseek-chat",
messages=[{"role": "user", "content": "Test prompt"}],
temperature=0.7
)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_anthropic(self, mock_create_client):
mock_create_client.return_value = self.mock_anthropic_client
response = query_llm("Test prompt", provider="anthropic")
self.assertEqual(response, "Test Anthropic response")
self.mock_anthropic_client.messages.create.assert_called_once_with(
model="claude-3-sonnet-20240229",
max_tokens=1000,
messages=[{"role": "user", "content": "Test prompt"}]
)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_gemini(self, mock_create_client):
mock_create_client.return_value = self.mock_gemini_client
response = query_llm("Test prompt", provider="gemini")
self.assertEqual(response, "Test Gemini response")
self.mock_gemini_client.GenerativeModel.assert_called_once_with("gemini-pro")
self.mock_gemini_model.generate_content.assert_called_once_with("Test prompt")

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_local(self, mock_create_client):
mock_create_client.return_value = self.mock_openai_client
response = query_llm("Test prompt", provider="local")
self.assertEqual(response, "Test OpenAI response")
self.mock_openai_client.chat.completions.create.assert_called_once_with(
model="Qwen/Qwen2.5-32B-Instruct-AWQ",
messages=[{"role": "user", "content": "Test prompt"}],
temperature=0.7
)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_with_custom_model(self, mock_create_client):
mock_create_client.return_value = self.mock_openai_client
response = query_llm("Test prompt", model="custom-model")

# Verify response
self.assertEqual(response, "Test response")

# Verify client was called with custom model
self.mock_client.chat.completions.create.assert_called_once_with(
self.assertEqual(response, "Test OpenAI response")
self.mock_openai_client.chat.completions.create.assert_called_once_with(
model="custom-model",
messages=[{"role": "user", "content": "Test prompt"}],
temperature=0.7
)

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_llm_with_existing_client(self, mock_create_client):
# Test query with provided client
response = query_llm("Test prompt", client=self.mock_client)

# Verify response
self.assertEqual(response, "Test response")

# Verify create_client was not called
def test_query_with_existing_client(self, mock_create_client):
response = query_llm("Test prompt", client=self.mock_openai_client)
self.assertEqual(response, "Test OpenAI response")
mock_create_client.assert_not_called()

@unittest.skipIf(skip_llm_tests, skip_message)
@patch('tools.llm_api.create_llm_client')
def test_query_llm_error(self, mock_create_client):
# Set up mock to raise an exception
self.mock_client.chat.completions.create.side_effect = Exception("Test error")
mock_create_client.return_value = self.mock_client

# Test query with error
def test_query_error(self, mock_create_client):
self.mock_openai_client.chat.completions.create.side_effect = Exception("Test error")
mock_create_client.return_value = self.mock_openai_client
response = query_llm("Test prompt")

# Verify error handling
self.assertIsNone(response)

if __name__ == '__main__':
Expand Down
Loading