Skip to content

Commit 3c4187e

Browse files
authored
feat: enhance LLM and search engine capabilities (#12)
* feat: enhance LLM and search engine capabilities - Add Anthropic API support with Claude model - Implement environment variables configuration via .env.local - Add retry mechanism and error handling for search engine - Improve search engine reliability with: - Random User-Agent rotation - API/HTML backend fallback - Exponential backoff retry - Add new dependencies: anthropic and python-dotenv Breaking changes: - LLM API now requires provider-specific API keys in .env.local * docs: add environment variables setup step in README - Add step 2 for environment configuration - Include instructions for copying .env.example - Renumber subsequent installation step * refactor: improve LLM API configuration and testing - Add default model settings for different providers (OpenAI, Anthropic, Local) - Replace hardcoded API configuration with environment variables - Update search engine backend from 'html' to 'api' - Enhance debug messages in search engine tests - Update test cases to reflect new default configurations Default models: - OpenAI: gpt-3.5-turbo - Anthropic: claude-3-sonnet-20240229 - Local: Qwen/Qwen2.5-32B-Instruct-AWQ * ci: support master branch in GitHub Actions
1 parent 27dee37 commit 3c4187e

File tree

10 files changed

+258
-57
lines changed

10 files changed

+258
-57
lines changed

.cursorrules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Note all the tools are in python. So in the case you need to do batch processing
1717

1818
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:
1919
```
20-
py310/bin/python ./tools/llm_api.py --prompt "What is the capital of France?"
20+
py310/bin/python ./tools/llm_api.py --prompt "What is the capital of France?" --provider "anthropic"
2121
```
2222

2323
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.

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
OPENAI_API_KEY=
2+
ANTHROPIC_API_KEY=

.github/workflows/tests.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Unit Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ master, main ]
6+
push:
7+
branches: [ master, main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v3
15+
16+
- name: Set up Python 3.10
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.10'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
python -m playwright install chromium
26+
27+
- name: Copy environment file
28+
run: |
29+
cp .env.example .env
30+
31+
- name: Run tests
32+
env:
33+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
34+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
35+
run: |
36+
PYTHONPATH=. python -m unittest discover tests/

.gitignore

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
38+
credentials.json
39+
40+
# asdf
41+
.tool-versions
42+
43+
# pycache
44+
**/__pycache__/
45+
46+
# python virtual environment
47+
/py310/

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ source py310/bin/activate
2626
.\py310\Scripts\activate
2727
```
2828

29-
2. Install dependencies:
29+
2. Configure environment variables:
30+
```bash
31+
# Copy the example environment file
32+
cp .env.example .env
33+
34+
# Edit .env with your API keys and configurations
35+
```
36+
37+
3. Install dependencies:
3038
```bash
3139
# Install required packages
3240
pip install -r requirements.txt

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ duckduckgo-search>=4.1.1
77

88
# LLM integration
99
openai>=1.12.0
10+
anthropic>=0.42.0
11+
python-dotenv>=1.0.0
1012

1113
# Testing
1214
unittest2>=1.1.0

tests/test_llm_api.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,13 @@ def setUp(self):
3535
@unittest.skipIf(skip_llm_tests, skip_message)
3636
@patch('tools.llm_api.OpenAI')
3737
def test_create_llm_client(self, mock_openai):
38-
# Test client creation
38+
# Test client creation with default provider (openai)
3939
mock_openai.return_value = self.mock_client
40-
client = create_llm_client()
40+
client = create_llm_client() # 使用預設 provider
4141

4242
# Verify OpenAI was called with correct parameters
4343
mock_openai.assert_called_once_with(
44-
base_url="http://192.168.180.137:8006/v1",
45-
api_key="not-needed"
44+
api_key=os.getenv('OPENAI_API_KEY') # 使用環境變數中的 API key
4645
)
4746

4847
self.assertEqual(client, self.mock_client)
@@ -53,15 +52,15 @@ def test_query_llm_success(self, mock_create_client):
5352
# Set up mock
5453
mock_create_client.return_value = self.mock_client
5554

56-
# Test query with default parameters
57-
response = query_llm("Test prompt")
55+
# Test query with default provider
56+
response = query_llm("Test prompt") # 使用預設 provider
5857

5958
# Verify response
6059
self.assertEqual(response, "Test response")
6160

6261
# Verify client was called correctly
6362
self.mock_client.chat.completions.create.assert_called_once_with(
64-
model="Qwen/Qwen2.5-32B-Instruct-AWQ",
63+
model="gpt-3.5-turbo", # 使用 OpenAI 的預設模型
6564
messages=[{"role": "user", "content": "Test prompt"}],
6665
temperature=0.7
6766
)

tests/test_search_engine.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def test_successful_search(self, mock_ddgs):
4444
search("test query", max_results=2)
4545

4646
# Check debug output
47-
self.assertIn("DEBUG: Searching for query: test query", self.stderr.getvalue())
47+
expected_debug = "DEBUG: Attempt 1/3 - Searching for query: test query"
48+
self.assertIn(expected_debug, self.stderr.getvalue())
4849
self.assertIn("DEBUG: Found 2 results", self.stderr.getvalue())
4950

5051
# Check search results output
@@ -62,7 +63,7 @@ def test_successful_search(self, mock_ddgs):
6263
mock_ddgs_instance.__enter__.return_value.text.assert_called_once_with(
6364
"test query",
6465
max_results=2,
65-
backend='html'
66+
backend='api'
6667
)
6768

6869
@patch('tools.search_engine.DDGS')

tools/llm_api.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,93 @@
11
#!/usr/bin/env /workspace/tmp_windsurf/py310/bin/python3
22

33
from openai import OpenAI
4+
from anthropic import Anthropic
45
import argparse
6+
import os
7+
from dotenv import load_dotenv
8+
from pathlib import Path
59

6-
def create_llm_client():
7-
client = OpenAI(
8-
base_url="http://192.168.180.137:8006/v1",
9-
api_key="not-needed" # API key might not be needed for local deployment
10-
)
11-
return client
10+
# 載入 .env.local 檔案
11+
env_path = Path('.') / '.env.local'
12+
load_dotenv(dotenv_path=env_path)
1213

13-
def query_llm(prompt, client=None, model="Qwen/Qwen2.5-32B-Instruct-AWQ"):
14+
def create_llm_client(provider="openai"):
15+
if provider == "openai":
16+
api_key = os.getenv('OPENAI_API_KEY')
17+
if not api_key:
18+
raise ValueError("OPENAI_API_KEY not found in environment variables")
19+
return OpenAI(
20+
api_key=api_key
21+
)
22+
elif provider == "anthropic":
23+
api_key = os.getenv('ANTHROPIC_API_KEY')
24+
if not api_key:
25+
raise ValueError("ANTHROPIC_API_KEY not found in environment variables")
26+
return Anthropic(
27+
api_key=api_key
28+
)
29+
elif provider == "local":
30+
return OpenAI(
31+
base_url="http://192.168.180.137:8006/v1",
32+
api_key="not-needed" # 本地部署可能不需要 API key
33+
)
34+
else:
35+
raise ValueError(f"Unsupported provider: {provider}")
36+
37+
def query_llm(prompt, client=None, model=None, provider="openai"):
1438
if client is None:
15-
client = create_llm_client()
39+
client = create_llm_client(provider)
1640

1741
try:
18-
response = client.chat.completions.create(
19-
model=model,
20-
messages=[
21-
{"role": "user", "content": prompt}
22-
],
23-
temperature=0.7,
24-
)
25-
return response.choices[0].message.content
42+
# 設定預設模型
43+
if model is None:
44+
if provider == "openai":
45+
model = "gpt-3.5-turbo"
46+
elif provider == "anthropic":
47+
model = "claude-3-sonnet-20240229"
48+
elif provider == "local":
49+
model = "Qwen/Qwen2.5-32B-Instruct-AWQ"
50+
51+
if provider == "openai" or provider == "local":
52+
response = client.chat.completions.create(
53+
model=model,
54+
messages=[
55+
{"role": "user", "content": prompt}
56+
],
57+
temperature=0.7,
58+
)
59+
return response.choices[0].message.content
60+
elif provider == "anthropic":
61+
response = client.messages.create(
62+
model=model,
63+
max_tokens=1000,
64+
messages=[
65+
{"role": "user", "content": prompt}
66+
]
67+
)
68+
return response.content[0].text
2669
except Exception as e:
2770
print(f"Error querying LLM: {e}")
28-
print("Note: If you haven't configured a local LLM server, this error is expected and can be ignored.")
29-
print("The LLM functionality is optional and won't affect other features.")
3071
return None
3172

3273
def main():
3374
parser = argparse.ArgumentParser(description='Query an LLM with a prompt')
3475
parser.add_argument('--prompt', type=str, help='The prompt to send to the LLM', required=True)
35-
parser.add_argument('--model', type=str, default="Qwen/Qwen2.5-32B-Instruct-AWQ",
36-
help='The model to use (default: Qwen/Qwen2.5-32B-Instruct-AWQ)')
76+
parser.add_argument('--provider', type=str, choices=['openai', 'anthropic'],
77+
default="openai", help='The API provider to use')
78+
parser.add_argument('--model', type=str,
79+
help='The model to use (default depends on provider)')
3780
args = parser.parse_args()
3881

39-
client = create_llm_client()
40-
response = query_llm(args.prompt, client, model=args.model)
82+
# 設定預設模型
83+
if not args.model:
84+
if args.provider == "openai":
85+
args.model = "gpt-3.5-turbo"
86+
else:
87+
args.model = "claude-3-5-sonnet-20241022"
88+
89+
client = create_llm_client(args.provider)
90+
response = query_llm(args.prompt, client, model=args.model, provider=args.provider)
4191
if response:
4292
print(response)
4393
else:

0 commit comments

Comments
 (0)