Eliot0110 commited on
Commit
68e8f6c
·
1 Parent(s): e425487

feat: 添加Docker配置、依赖管理和测试框架

Browse files

- 添加Dockerfile支持容器化部署
- 完善requirements.txt包含所有必要依赖
- 创建完整的测试框架和单元测试
- 支持HuggingFace Spaces自动构建和部署
- 添加健康检查和测试覆盖

Dockerfile CHANGED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+ FROM python:3.10-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 设置环境变量
8
+ ENV PYTHONPATH=/app
9
+ ENV PYTHONUNBUFFERED=1
10
+
11
+ # 安装系统依赖
12
+ RUN apt-get update && apt-get install -y \
13
+ git \
14
+ curl \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # 复制requirements文件并安装Python依赖
18
+ COPY requirements.txt .
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # 复制项目文件
22
+ COPY . .
23
+
24
+ # 暴露端口
25
+ EXPOSE 7860
26
+
27
+ # 健康检查
28
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
29
+ CMD curl -f http://localhost:7860/health || exit 1
30
+
31
+ # 启动命令
32
+ CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
requirements.txt CHANGED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # requirements.txt
2
+ # FastAPI 和 Web 服务
3
+ fastapi==0.104.1
4
+ uvicorn[standard]==0.24.0
5
+ pydantic==2.5.0
6
+ python-multipart==0.0.6
7
+
8
+ # AI 模型和机器学习
9
+ torch==2.1.0
10
+ transformers==4.54.1
11
+ accelerate==0.25.0
12
+ safetensors==0.4.1
13
+
14
+ # 图像处理
15
+ Pillow==10.1.0
16
+ requests==2.31.0
17
+
18
+ # 音频处理(为将来扩展准备)
19
+ # librosa==0.10.1
20
+ # soundfile==0.12.1
21
+
22
+ # 工具库
23
+ numpy==1.24.3
24
+ typing-extensions==4.8.0
25
+
26
+ # 测试框架
27
+ pytest==7.4.3
28
+ pytest-asyncio==0.21.1
29
+ httpx==0.25.2
tests/__init__.py CHANGED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # tests/__init__.py
2
+ """
3
+ 测试模块包
4
+ 包含所有组件的单元测试和集成测试
5
+ """
6
+
7
+ __version__ = '1.0.0'
tests/test_ai_model.py CHANGED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/test_ai_model.py
2
+ import pytest
3
+ from unittest.mock import Mock, patch
4
+ from modules.ai_model import AIModel
5
+
6
+ class TestAIModel:
7
+
8
+ @pytest.fixture
9
+ def mock_ai_model(self):
10
+ """创建模拟的AI模型"""
11
+ with patch('modules.ai_model.Gemma3nForConditionalGeneration') as mock_model, \
12
+ patch('modules.ai_model.AutoProcessor') as mock_processor:
13
+
14
+ mock_model.from_pretrained.return_value = Mock()
15
+ mock_processor.from_pretrained.return_value = Mock()
16
+
17
+ ai_model = AIModel()
18
+ yield ai_model
19
+
20
+ def test_detect_input_type_text(self, mock_ai_model):
21
+ """测试文本输入检测"""
22
+ assert mock_ai_model.detect_input_type("我想去巴黎") == "text"
23
+
24
+ def test_detect_input_type_image_url(self, mock_ai_model):
25
+ """测试图片URL检测"""
26
+ assert mock_ai_model.detect_input_type("https://example.com/image.jpg") == "image"
27
+
28
+ def test_detect_input_type_image_path(self, mock_ai_model):
29
+ """测试图片路径检测"""
30
+ assert mock_ai_model.detect_input_type("./images/paris.png") == "image"
31
+
32
+ def test_detect_input_type_audio(self, mock_ai_model):
33
+ """测试音频文件检测"""
34
+ assert mock_ai_model.detect_input_type("audio.mp3") == "audio"
35
+
36
+ def test_is_available(self, mock_ai_model):
37
+ """测试模型可用性检查"""
38
+ assert mock_ai_model.is_available() == True
39
+
40
+ @patch('modules.ai_model.Image')
41
+ @patch('modules.ai_model.requests')
42
+ def test_format_input_image_url(self, mock_requests, mock_image, mock_ai_model):
43
+ """测试图片URL格式化"""
44
+ mock_response = Mock()
45
+ mock_response.content = b"fake_image_data"
46
+ mock_requests.get.return_value = mock_response
47
+ mock_image.open.return_value.convert.return_value = "processed_image"
48
+
49
+ input_type, formatted_data, processed_text = mock_ai_model.format_input(
50
+ "image",
51
+ "https://example.com/image.jpg"
52
+ )
53
+
54
+ assert input_type == "image"
55
+ assert formatted_data == "processed_image"
56
+ assert "请描述这张图片" in processed_text
tests/test_config_loader.py CHANGED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/test_config_loader.py
2
+ import pytest
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+ from modules.config_loader import ConfigLoader
7
+
8
+ class TestConfigLoader:
9
+
10
+ @pytest.fixture
11
+ def temp_config_dir(self):
12
+ """创建临时配置目录和文件"""
13
+ with tempfile.TemporaryDirectory() as tmp_dir:
14
+ config_dir = Path(tmp_dir)
15
+
16
+ # 创建测试配置文件
17
+ cities_data = {
18
+ "cities": [
19
+ {"name": "巴黎", "country": "法国", "aliases": ["paris"]}
20
+ ]
21
+ }
22
+ with open(config_dir / "cities.json", 'w', encoding='utf-8') as f:
23
+ json.dump(cities_data, f, ensure_ascii=False)
24
+
25
+ personas_data = {
26
+ "personas": {
27
+ "planner": {"name": "规划型", "keywords": ["规划", "安排"]}
28
+ }
29
+ }
30
+ with open(config_dir / "personas.json", 'w', encoding='utf-8') as f:
31
+ json.dump(personas_data, f, ensure_ascii=False)
32
+
33
+ interests_data = {
34
+ "interests": {"美食": ["美食", "餐厅"]}
35
+ }
36
+ with open(config_dir / "interests.json", 'w', encoding='utf-8') as f:
37
+ json.dump(interests_data, f, ensure_ascii=False)
38
+
39
+ yield config_dir
40
+
41
+ def test_load_cities(self, temp_config_dir):
42
+ """测试城市配置加载"""
43
+ loader = ConfigLoader(temp_config_dir)
44
+ assert "巴黎" in loader.cities
45
+ assert "paris" in loader.cities
46
+ assert loader.cities["巴黎"]["name"] == "巴黎"
47
+
48
+ def test_load_personas(self, temp_config_dir):
49
+ """测试人格配置加载"""
50
+ loader = ConfigLoader(temp_config_dir)
51
+ assert "planner" in loader.personas
52
+ assert loader.personas["planner"]["name"] == "规划型"
53
+
54
+ def test_load_interests(self, temp_config_dir):
55
+ """测试兴趣配置加载"""
56
+ loader = ConfigLoader(temp_config_dir)
57
+ assert "美食" in loader.interests
tests/test_travel_assistant.py CHANGED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tests/test_travel_assistant.py
2
+ import pytest
3
+ from unittest.mock import Mock, patch
4
+ from modules.travel_assistant import TravelAssistant
5
+
6
+ class TestTravelAssistant:
7
+
8
+ @pytest.fixture
9
+ def mock_travel_assistant(self):
10
+ """创建模拟的旅游助手"""
11
+ with patch('modules.travel_assistant.ConfigLoader') as mock_config, \
12
+ patch('modules.travel_assistant.KnowledgeBase') as mock_kb, \
13
+ patch('modules.travel_assistant.AIModel') as mock_ai:
14
+
15
+ # 设置模拟对象
16
+ mock_config.return_value.cities = {"巴黎": {"name": "巴黎", "country": "法国"}}
17
+ mock_config.return_value.personas = {"planner": {"name": "规划型"}}
18
+
19
+ assistant = TravelAssistant()
20
+ yield assistant
21
+
22
+ def test_chat_basic_flow(self, mock_travel_assistant):
23
+ """测试基本聊天流程"""
24
+ reply, session_id, status_info, history = mock_travel_assistant.chat(
25
+ message="我想去巴黎旅游",
26
+ session_id=None,
27
+ history=[]
28
+ )
29
+
30
+ assert isinstance(reply, str)
31
+ assert len(session_id) == 8 # UUID前8位
32
+ assert isinstance(status_info, str)
33
+ assert len(history) == 1
34
+ assert history[0][0] == "我想去巴黎旅游"
35
+
36
+ def test_session_persistence(self, mock_travel_assistant):
37
+ """测试会话持久性"""
38
+ # 第一次对话
39
+ _, session_id, _, _ = mock_travel_assistant.chat(
40
+ message="我想去巴黎",
41
+ session_id=None,
42
+ history=[]
43
+ )
44
+
45
+ # 第二次对话使用相同session_id
46
+ reply, same_session_id, _, _ = mock_travel_assistant.chat(
47
+ message="3天行程",
48
+ session_id=session_id,
49
+ history=[]
50
+ )
51
+
52
+ assert session_id == same_session_id