Spaces:
Sleeping
Sleeping
Commit
·
e207dc8
1
Parent(s):
81a2146
Add complete scripts directory with training, testing, and deployment tools
Browse files- DEPLOYMENT.md +150 -0
- scripts/check_training_ready.py +206 -0
- scripts/create_sample_dataset.py +195 -0
- scripts/download_alternative_models.py +186 -0
- scripts/download_model.py +120 -0
- scripts/download_open_models.py +163 -0
- scripts/finetune_lora.py +251 -0
- scripts/inference_textilindo_ai.py +178 -0
- scripts/local_training_setup.py +273 -0
- scripts/novita_ai_setup.py +256 -0
- scripts/novita_ai_setup_v2.py +376 -0
- scripts/run_novita_finetuning.py +117 -0
- scripts/setup_textilindo_training.py +175 -0
- scripts/test_model.py +201 -0
- scripts/test_novita_connection.py +158 -0
- scripts/test_textilindo_ai.py +235 -0
- scripts/train_textilindo_ai.py +282 -0
- scripts/train_textilindo_ai_optimized.py +296 -0
- scripts/train_with_monitoring.py +228 -0
- test_deployment.py +266 -0
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Textilindo AI Assistant - Hugging Face Spaces Deployment Guide
|
| 2 |
+
|
| 3 |
+
## 🚀 Quick Setup for Hugging Face Spaces
|
| 4 |
+
|
| 5 |
+
### 1. Environment Variables Required
|
| 6 |
+
|
| 7 |
+
Set these environment variables in your Hugging Face Space settings:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# Required: Hugging Face API Key
|
| 11 |
+
HUGGINGFACE_API_KEY=your_huggingface_api_key_here
|
| 12 |
+
|
| 13 |
+
# Optional: Default model (defaults to meta-llama/Llama-3.1-8B-Instruct)
|
| 14 |
+
DEFAULT_MODEL=meta-llama/Llama-3.1-8B-Instruct
|
| 15 |
+
|
| 16 |
+
# Optional: Alternative lightweight models
|
| 17 |
+
# DEFAULT_MODEL=meta-llama/Llama-3.2-1B-Instruct
|
| 18 |
+
# DEFAULT_MODEL=microsoft/DialoGPT-medium
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### 2. Files Structure
|
| 22 |
+
|
| 23 |
+
Your Hugging Face Space should contain:
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
├── app.py # Main FastAPI application
|
| 27 |
+
├── Dockerfile # Docker configuration
|
| 28 |
+
├── requirements.txt # Python dependencies
|
| 29 |
+
├── README.md # Space description
|
| 30 |
+
├── configs/
|
| 31 |
+
│ ├── system_prompt.md # AI system prompt
|
| 32 |
+
│ └── training_config.yaml # Training configuration
|
| 33 |
+
├── data/
|
| 34 |
+
│ └── lora_dataset_*.jsonl # Training datasets
|
| 35 |
+
└── templates/
|
| 36 |
+
└── chat.html # Chat interface (optional)
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### 3. Deployment Steps
|
| 40 |
+
|
| 41 |
+
1. **Create a new Hugging Face Space:**
|
| 42 |
+
- Go to https://huggingface.co/new-space
|
| 43 |
+
- Choose "Docker" as the SDK
|
| 44 |
+
- Name your space (e.g., "textilindo-ai-assistant")
|
| 45 |
+
|
| 46 |
+
2. **Upload files:**
|
| 47 |
+
- Clone your space repository
|
| 48 |
+
- Copy all files from this project
|
| 49 |
+
- Commit and push to your space
|
| 50 |
+
|
| 51 |
+
3. **Set environment variables:**
|
| 52 |
+
- Go to your space settings
|
| 53 |
+
- Add the required environment variables
|
| 54 |
+
- Make sure to set `HUGGINGFACE_API_KEY`
|
| 55 |
+
|
| 56 |
+
4. **Deploy:**
|
| 57 |
+
- Your space will automatically build and deploy
|
| 58 |
+
- Check the logs for any issues
|
| 59 |
+
|
| 60 |
+
### 4. API Endpoints
|
| 61 |
+
|
| 62 |
+
Once deployed, your space will have these endpoints:
|
| 63 |
+
|
| 64 |
+
- `GET /` - Main chat interface
|
| 65 |
+
- `POST /chat` - Chat API endpoint
|
| 66 |
+
- `GET /health` - Health check
|
| 67 |
+
- `GET /info` - Application information
|
| 68 |
+
|
| 69 |
+
### 5. Usage Examples
|
| 70 |
+
|
| 71 |
+
#### Chat API
|
| 72 |
+
```bash
|
| 73 |
+
curl -X POST "https://your-space-name.hf.space/chat" \
|
| 74 |
+
-H "Content-Type: application/json" \
|
| 75 |
+
-d '{"message": "dimana lokasi textilindo?"}'
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
#### Health Check
|
| 79 |
+
```bash
|
| 80 |
+
curl "https://your-space-name.hf.space/health"
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### 6. Troubleshooting
|
| 84 |
+
|
| 85 |
+
#### Common Issues:
|
| 86 |
+
|
| 87 |
+
1. **"HUGGINGFACE_API_KEY not found"**
|
| 88 |
+
- Make sure you've set the environment variable in your space settings
|
| 89 |
+
- The app will use mock responses if no API key is provided
|
| 90 |
+
|
| 91 |
+
2. **Model loading errors**
|
| 92 |
+
- Check if the model name is correct
|
| 93 |
+
- Try using a lighter model like `meta-llama/Llama-3.2-1B-Instruct`
|
| 94 |
+
|
| 95 |
+
3. **Memory issues**
|
| 96 |
+
- Hugging Face Spaces have limited memory
|
| 97 |
+
- Use smaller models or reduce batch sizes
|
| 98 |
+
|
| 99 |
+
4. **Build failures**
|
| 100 |
+
- Check the build logs in your space
|
| 101 |
+
- Ensure all dependencies are in requirements.txt
|
| 102 |
+
|
| 103 |
+
### 7. Customization
|
| 104 |
+
|
| 105 |
+
#### Change the Model:
|
| 106 |
+
Update the `DEFAULT_MODEL` environment variable:
|
| 107 |
+
```bash
|
| 108 |
+
DEFAULT_MODEL=meta-llama/Llama-3.2-1B-Instruct
|
| 109 |
+
```
|
| 110 |
+
|
| 111 |
+
#### Update System Prompt:
|
| 112 |
+
Edit `configs/system_prompt.md` and redeploy.
|
| 113 |
+
|
| 114 |
+
#### Add More Training Data:
|
| 115 |
+
Add more JSONL files to the `data/` directory.
|
| 116 |
+
|
| 117 |
+
### 8. Performance Optimization
|
| 118 |
+
|
| 119 |
+
For better performance on Hugging Face Spaces:
|
| 120 |
+
|
| 121 |
+
1. **Use smaller models:**
|
| 122 |
+
- `meta-llama/Llama-3.2-1B-Instruct` (1B parameters)
|
| 123 |
+
- `microsoft/DialoGPT-medium` (355M parameters)
|
| 124 |
+
|
| 125 |
+
2. **Optimize system prompt:**
|
| 126 |
+
- Keep it concise
|
| 127 |
+
- Remove unnecessary instructions
|
| 128 |
+
|
| 129 |
+
3. **Monitor resource usage:**
|
| 130 |
+
- Check the space logs
|
| 131 |
+
- Use the `/health` endpoint
|
| 132 |
+
|
| 133 |
+
### 9. Security Notes
|
| 134 |
+
|
| 135 |
+
- Never commit API keys to your repository
|
| 136 |
+
- Use environment variables for sensitive data
|
| 137 |
+
- The app includes CORS middleware for web access
|
| 138 |
+
- All user inputs are logged (check logs for debugging)
|
| 139 |
+
|
| 140 |
+
### 10. Support
|
| 141 |
+
|
| 142 |
+
If you encounter issues:
|
| 143 |
+
|
| 144 |
+
1. Check the space logs
|
| 145 |
+
2. Verify environment variables
|
| 146 |
+
3. Test with the `/health` endpoint
|
| 147 |
+
4. Try the mock responses (without API key)
|
| 148 |
+
|
| 149 |
+
For more help, check the Hugging Face Spaces documentation:
|
| 150 |
+
https://huggingface.co/docs/hub/spaces
|
scripts/check_training_ready.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Check if everything is ready for Textilindo AI training
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import yaml
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
def check_file_exists(file_path, description):
|
| 12 |
+
"""Check if a file exists and print status"""
|
| 13 |
+
if os.path.exists(file_path):
|
| 14 |
+
print(f"✅ {description}: {file_path}")
|
| 15 |
+
return True
|
| 16 |
+
else:
|
| 17 |
+
print(f"❌ {description}: {file_path}")
|
| 18 |
+
return False
|
| 19 |
+
|
| 20 |
+
def check_config():
|
| 21 |
+
"""Check configuration files"""
|
| 22 |
+
print("🔍 Checking configuration files...")
|
| 23 |
+
|
| 24 |
+
config_path = "configs/training_config.yaml"
|
| 25 |
+
if not os.path.exists(config_path):
|
| 26 |
+
print(f"❌ Training config not found: {config_path}")
|
| 27 |
+
return False
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
with open(config_path, 'r') as f:
|
| 31 |
+
config = yaml.safe_load(f)
|
| 32 |
+
|
| 33 |
+
# Check required fields
|
| 34 |
+
required_fields = ['model_name', 'model_path', 'dataset_path', 'lora_config', 'training_config']
|
| 35 |
+
for field in required_fields:
|
| 36 |
+
if field not in config:
|
| 37 |
+
print(f"❌ Missing field in config: {field}")
|
| 38 |
+
return False
|
| 39 |
+
|
| 40 |
+
print("✅ Training configuration is valid")
|
| 41 |
+
return True
|
| 42 |
+
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"❌ Error reading config: {e}")
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
def check_dataset():
|
| 48 |
+
"""Check dataset file"""
|
| 49 |
+
print("\n🔍 Checking dataset...")
|
| 50 |
+
|
| 51 |
+
config_path = "configs/training_config.yaml"
|
| 52 |
+
with open(config_path, 'r') as f:
|
| 53 |
+
config = yaml.safe_load(f)
|
| 54 |
+
|
| 55 |
+
dataset_path = config['dataset_path']
|
| 56 |
+
|
| 57 |
+
if not os.path.exists(dataset_path):
|
| 58 |
+
print(f"❌ Dataset not found: {dataset_path}")
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
# Check if it's a valid JSONL file
|
| 62 |
+
try:
|
| 63 |
+
import json
|
| 64 |
+
with open(dataset_path, 'r', encoding='utf-8') as f:
|
| 65 |
+
lines = f.readlines()
|
| 66 |
+
|
| 67 |
+
if not lines:
|
| 68 |
+
print("❌ Dataset is empty")
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
# Check first few lines
|
| 72 |
+
valid_lines = 0
|
| 73 |
+
for i, line in enumerate(lines[:5]): # Check first 5 lines
|
| 74 |
+
line = line.strip()
|
| 75 |
+
if line:
|
| 76 |
+
try:
|
| 77 |
+
json.loads(line)
|
| 78 |
+
valid_lines += 1
|
| 79 |
+
except json.JSONDecodeError:
|
| 80 |
+
print(f"❌ Invalid JSON at line {i+1}")
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
print(f"✅ Dataset found: {dataset_path}")
|
| 84 |
+
print(f" Total lines: {len(lines)}")
|
| 85 |
+
print(f" Valid JSON lines checked: {valid_lines}")
|
| 86 |
+
return True
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
print(f"❌ Error reading dataset: {e}")
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
def check_model():
|
| 93 |
+
"""Check if base model exists"""
|
| 94 |
+
print("\n🔍 Checking base model...")
|
| 95 |
+
|
| 96 |
+
config_path = "configs/training_config.yaml"
|
| 97 |
+
with open(config_path, 'r') as f:
|
| 98 |
+
config = yaml.safe_load(f)
|
| 99 |
+
|
| 100 |
+
model_path = config['model_path']
|
| 101 |
+
|
| 102 |
+
if not os.path.exists(model_path):
|
| 103 |
+
print(f"❌ Base model not found: {model_path}")
|
| 104 |
+
print(" Run: python scripts/setup_textilindo_training.py")
|
| 105 |
+
return False
|
| 106 |
+
|
| 107 |
+
# Check if it's a valid model directory
|
| 108 |
+
required_files = ['config.json']
|
| 109 |
+
optional_files = ['tokenizer.json', 'tokenizer_config.json']
|
| 110 |
+
|
| 111 |
+
for file in required_files:
|
| 112 |
+
if not os.path.exists(os.path.join(model_path, file)):
|
| 113 |
+
print(f"❌ Model file missing: {file}")
|
| 114 |
+
return False
|
| 115 |
+
|
| 116 |
+
# Check for at least one tokenizer file
|
| 117 |
+
tokenizer_found = any(os.path.exists(os.path.join(model_path, file)) for file in optional_files)
|
| 118 |
+
if not tokenizer_found:
|
| 119 |
+
print("❌ No tokenizer files found")
|
| 120 |
+
return False
|
| 121 |
+
|
| 122 |
+
print(f"✅ Base model found: {model_path}")
|
| 123 |
+
return True
|
| 124 |
+
|
| 125 |
+
def check_system_prompt():
|
| 126 |
+
"""Check system prompt file"""
|
| 127 |
+
print("\n🔍 Checking system prompt...")
|
| 128 |
+
|
| 129 |
+
system_prompt_path = "configs/system_prompt.md"
|
| 130 |
+
|
| 131 |
+
if not os.path.exists(system_prompt_path):
|
| 132 |
+
print(f"❌ System prompt not found: {system_prompt_path}")
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
try:
|
| 136 |
+
with open(system_prompt_path, 'r', encoding='utf-8') as f:
|
| 137 |
+
content = f.read()
|
| 138 |
+
|
| 139 |
+
if 'SYSTEM_PROMPT' not in content:
|
| 140 |
+
print("❌ SYSTEM_PROMPT not found in file")
|
| 141 |
+
return False
|
| 142 |
+
|
| 143 |
+
print(f"✅ System prompt found: {system_prompt_path}")
|
| 144 |
+
return True
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"❌ Error reading system prompt: {e}")
|
| 148 |
+
return False
|
| 149 |
+
|
| 150 |
+
def check_requirements():
|
| 151 |
+
"""Check Python requirements"""
|
| 152 |
+
print("\n🔍 Checking Python requirements...")
|
| 153 |
+
|
| 154 |
+
required_packages = [
|
| 155 |
+
'torch',
|
| 156 |
+
'transformers',
|
| 157 |
+
'peft',
|
| 158 |
+
'datasets',
|
| 159 |
+
'accelerate',
|
| 160 |
+
'bitsandbytes',
|
| 161 |
+
'yaml'
|
| 162 |
+
]
|
| 163 |
+
|
| 164 |
+
missing_packages = []
|
| 165 |
+
for package in required_packages:
|
| 166 |
+
try:
|
| 167 |
+
__import__(package)
|
| 168 |
+
print(f"✅ {package}")
|
| 169 |
+
except ImportError:
|
| 170 |
+
missing_packages.append(package)
|
| 171 |
+
print(f"❌ {package}")
|
| 172 |
+
|
| 173 |
+
if missing_packages:
|
| 174 |
+
print(f"\n❌ Missing packages: {', '.join(missing_packages)}")
|
| 175 |
+
print("Install with: pip install " + " ".join(missing_packages))
|
| 176 |
+
return False
|
| 177 |
+
|
| 178 |
+
return True
|
| 179 |
+
|
| 180 |
+
def main():
|
| 181 |
+
print("🔍 Textilindo AI Training - Readiness Check")
|
| 182 |
+
print("=" * 50)
|
| 183 |
+
|
| 184 |
+
all_ready = True
|
| 185 |
+
|
| 186 |
+
# Check all components
|
| 187 |
+
all_ready &= check_config()
|
| 188 |
+
all_ready &= check_dataset()
|
| 189 |
+
all_ready &= check_model()
|
| 190 |
+
all_ready &= check_system_prompt()
|
| 191 |
+
all_ready &= check_requirements()
|
| 192 |
+
|
| 193 |
+
print("\n" + "=" * 50)
|
| 194 |
+
|
| 195 |
+
if all_ready:
|
| 196 |
+
print("✅ Everything is ready for training!")
|
| 197 |
+
print("\n📋 Next steps:")
|
| 198 |
+
print("1. Run training: python scripts/train_textilindo_ai.py")
|
| 199 |
+
print("2. Or use runner: ./run_textilindo_training.sh")
|
| 200 |
+
else:
|
| 201 |
+
print("❌ Some components are missing or invalid")
|
| 202 |
+
print("Please fix the issues above before training")
|
| 203 |
+
sys.exit(1)
|
| 204 |
+
|
| 205 |
+
if __name__ == "__main__":
|
| 206 |
+
main()
|
scripts/create_sample_dataset.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk membuat sample dataset JSONL untuk training
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
def create_sample_dataset():
|
| 11 |
+
"""Create sample JSONL dataset"""
|
| 12 |
+
|
| 13 |
+
# Sample training data
|
| 14 |
+
sample_data = [
|
| 15 |
+
{
|
| 16 |
+
"text": "Apa itu machine learning? Machine learning adalah cabang dari artificial intelligence yang memungkinkan komputer belajar dari data tanpa diprogram secara eksplisit.",
|
| 17 |
+
"category": "education",
|
| 18 |
+
"language": "id"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"text": "Jelaskan tentang deep learning. Deep learning adalah subset dari machine learning yang menggunakan neural network dengan banyak layer untuk memproses data kompleks.",
|
| 22 |
+
"category": "education",
|
| 23 |
+
"language": "id"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"text": "Bagaimana cara kerja neural network? Neural network bekerja dengan menerima input, memproses melalui hidden layers, dan menghasilkan output berdasarkan weights yang telah dilatih.",
|
| 27 |
+
"category": "education",
|
| 28 |
+
"language": "id"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"text": "Apa keuntungan menggunakan Python untuk AI? Python memiliki library yang lengkap seperti TensorFlow, PyTorch, dan scikit-learn yang memudahkan development AI.",
|
| 32 |
+
"category": "programming",
|
| 33 |
+
"language": "id"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"text": "Jelaskan tentang transfer learning. Transfer learning adalah teknik menggunakan model yang sudah dilatih pada dataset besar dan mengadaptasinya untuk task yang lebih spesifik.",
|
| 37 |
+
"category": "education",
|
| 38 |
+
"language": "id"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"text": "Bagaimana cara optimize model machine learning? Optimasi dapat dilakukan dengan hyperparameter tuning, feature engineering, dan menggunakan teknik seperti cross-validation.",
|
| 42 |
+
"category": "optimization",
|
| 43 |
+
"language": "id"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"text": "Apa itu overfitting? Overfitting terjadi ketika model belajar terlalu detail dari training data sehingga performa pada data baru menurun.",
|
| 47 |
+
"category": "education",
|
| 48 |
+
"language": "id"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"text": "Jelaskan tentang regularization. Regularization adalah teknik untuk mencegah overfitting dengan menambahkan penalty pada model complexity.",
|
| 52 |
+
"category": "education",
|
| 53 |
+
"language": "id"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"text": "Bagaimana cara handle imbalanced dataset? Dataset tidak seimbang dapat diatasi dengan teknik sampling, class weights, atau menggunakan metrics yang tepat seperti F1-score.",
|
| 57 |
+
"category": "data_handling",
|
| 58 |
+
"language": "id"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"text": "Apa itu ensemble learning? Ensemble learning menggabungkan multiple model untuk meningkatkan performa prediksi dan mengurangi variance.",
|
| 62 |
+
"category": "education",
|
| 63 |
+
"language": "id"
|
| 64 |
+
}
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
# Create data directory
|
| 68 |
+
data_dir = Path("data")
|
| 69 |
+
data_dir.mkdir(exist_ok=True)
|
| 70 |
+
|
| 71 |
+
# Write to JSONL file
|
| 72 |
+
output_file = data_dir / "training_data.jsonl"
|
| 73 |
+
|
| 74 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 75 |
+
for item in sample_data:
|
| 76 |
+
json.dump(item, f, ensure_ascii=False)
|
| 77 |
+
f.write('\n')
|
| 78 |
+
|
| 79 |
+
print(f"✅ Sample dataset created: {output_file}")
|
| 80 |
+
print(f"📊 Total samples: {len(sample_data)}")
|
| 81 |
+
print(f"📁 File size: {output_file.stat().st_size / 1024:.2f} KB")
|
| 82 |
+
|
| 83 |
+
# Show sample content
|
| 84 |
+
print("\n📝 Sample content:")
|
| 85 |
+
print("-" * 50)
|
| 86 |
+
for i, item in enumerate(sample_data[:3], 1):
|
| 87 |
+
print(f"Sample {i}:")
|
| 88 |
+
print(f" Text: {item['text'][:100]}...")
|
| 89 |
+
print(f" Category: {item['category']}")
|
| 90 |
+
print(f" Language: {item['language']}")
|
| 91 |
+
print()
|
| 92 |
+
|
| 93 |
+
def create_custom_dataset():
|
| 94 |
+
"""Create custom dataset from user input"""
|
| 95 |
+
|
| 96 |
+
print("🔧 Create Custom Dataset")
|
| 97 |
+
print("=" * 40)
|
| 98 |
+
|
| 99 |
+
# Get dataset info
|
| 100 |
+
dataset_name = input("Dataset name (without extension): ").strip()
|
| 101 |
+
if not dataset_name:
|
| 102 |
+
dataset_name = "custom_dataset"
|
| 103 |
+
|
| 104 |
+
num_samples = input("Number of samples (default 10): ").strip()
|
| 105 |
+
try:
|
| 106 |
+
num_samples = int(num_samples) if num_samples else 10
|
| 107 |
+
except ValueError:
|
| 108 |
+
num_samples = 10
|
| 109 |
+
|
| 110 |
+
print(f"\n📝 Creating {num_samples} samples...")
|
| 111 |
+
print("Format: Enter text for each sample (empty line to finish early)")
|
| 112 |
+
|
| 113 |
+
custom_data = []
|
| 114 |
+
|
| 115 |
+
for i in range(num_samples):
|
| 116 |
+
print(f"\nSample {i+1}/{num_samples}:")
|
| 117 |
+
text = input("Text: ").strip()
|
| 118 |
+
|
| 119 |
+
if not text:
|
| 120 |
+
print("Empty text, finishing...")
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
category = input("Category (optional): ").strip() or "general"
|
| 124 |
+
language = input("Language (optional, default 'id'): ").strip() or "id"
|
| 125 |
+
|
| 126 |
+
sample = {
|
| 127 |
+
"text": text,
|
| 128 |
+
"category": category,
|
| 129 |
+
"language": language
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
custom_data.append(sample)
|
| 133 |
+
|
| 134 |
+
# Ask if user wants to continue
|
| 135 |
+
if i < num_samples - 1:
|
| 136 |
+
continue_input = input("Continue? (y/n, default y): ").strip().lower()
|
| 137 |
+
if continue_input in ['n', 'no']:
|
| 138 |
+
break
|
| 139 |
+
|
| 140 |
+
if not custom_data:
|
| 141 |
+
print("❌ No data entered, dataset not created")
|
| 142 |
+
return
|
| 143 |
+
|
| 144 |
+
# Create data directory
|
| 145 |
+
data_dir = Path("data")
|
| 146 |
+
data_dir.mkdir(exist_ok=True)
|
| 147 |
+
|
| 148 |
+
# Write to JSONL file
|
| 149 |
+
output_file = data_dir / f"{dataset_name}.jsonl"
|
| 150 |
+
|
| 151 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 152 |
+
for item in custom_data:
|
| 153 |
+
json.dump(item, f, ensure_ascii=False)
|
| 154 |
+
f.write('\n')
|
| 155 |
+
|
| 156 |
+
print(f"\n✅ Custom dataset created: {output_file}")
|
| 157 |
+
print(f"📊 Total samples: {len(custom_data)}")
|
| 158 |
+
|
| 159 |
+
def main():
|
| 160 |
+
print("📊 Dataset Creator for LLM Training")
|
| 161 |
+
print("=" * 50)
|
| 162 |
+
|
| 163 |
+
print("Pilih opsi:")
|
| 164 |
+
print("1. Create sample dataset (10 samples)")
|
| 165 |
+
print("2. Create custom dataset")
|
| 166 |
+
print("3. View existing datasets")
|
| 167 |
+
|
| 168 |
+
choice = input("\nPilihan (1-3): ").strip()
|
| 169 |
+
|
| 170 |
+
if choice == "1":
|
| 171 |
+
create_sample_dataset()
|
| 172 |
+
elif choice == "2":
|
| 173 |
+
create_custom_dataset()
|
| 174 |
+
elif choice == "3":
|
| 175 |
+
data_dir = Path("data")
|
| 176 |
+
if data_dir.exists():
|
| 177 |
+
jsonl_files = list(data_dir.glob("*.jsonl"))
|
| 178 |
+
if jsonl_files:
|
| 179 |
+
print(f"\n📁 Found {len(jsonl_files)} JSONL files:")
|
| 180 |
+
for file in jsonl_files:
|
| 181 |
+
size = file.stat().st_size / 1024
|
| 182 |
+
print(f" - {file.name} ({size:.2f} KB)")
|
| 183 |
+
else:
|
| 184 |
+
print("\n📁 No JSONL files found in data/ directory")
|
| 185 |
+
else:
|
| 186 |
+
print("\n📁 Data directory does not exist")
|
| 187 |
+
else:
|
| 188 |
+
print("❌ Pilihan tidak valid")
|
| 189 |
+
|
| 190 |
+
if __name__ == "__main__":
|
| 191 |
+
main()
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
|
scripts/download_alternative_models.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk download model alternatif yang lebih mudah diakses
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import subprocess
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
def check_huggingface_token():
|
| 12 |
+
"""Check if HuggingFace token is available"""
|
| 13 |
+
token = os.getenv('HUGGINGFACE_TOKEN')
|
| 14 |
+
if not token:
|
| 15 |
+
print("❌ HUGGINGFACE_TOKEN tidak ditemukan!")
|
| 16 |
+
print("Silakan set environment variable:")
|
| 17 |
+
print("export HUGGINGFACE_TOKEN='your_token_here'")
|
| 18 |
+
return False
|
| 19 |
+
return True
|
| 20 |
+
|
| 21 |
+
def download_model(model_name, model_path):
|
| 22 |
+
"""Download model menggunakan huggingface-cli"""
|
| 23 |
+
print(f"📥 Downloading model: {model_name}")
|
| 24 |
+
print(f"📁 Target directory: {model_path}")
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
cmd = [
|
| 28 |
+
"huggingface-cli", "download",
|
| 29 |
+
model_name,
|
| 30 |
+
"--local-dir", str(model_path),
|
| 31 |
+
"--local-dir-use-symlinks", "False"
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 35 |
+
|
| 36 |
+
if result.returncode == 0:
|
| 37 |
+
print("✅ Model berhasil didownload!")
|
| 38 |
+
return True
|
| 39 |
+
else:
|
| 40 |
+
print(f"❌ Error downloading model: {result.stderr}")
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
except FileNotFoundError:
|
| 44 |
+
print("❌ huggingface-cli tidak ditemukan!")
|
| 45 |
+
print("Silakan install dengan: pip install huggingface_hub")
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
def create_model_config(model_name, model_path):
|
| 49 |
+
"""Create model configuration file"""
|
| 50 |
+
config_dir = Path("configs")
|
| 51 |
+
config_dir.mkdir(exist_ok=True)
|
| 52 |
+
|
| 53 |
+
if "llama" in model_name.lower():
|
| 54 |
+
config_content = f"""# Model Configuration for {model_name}
|
| 55 |
+
model_name: "{model_name}"
|
| 56 |
+
model_path: "{model_path}"
|
| 57 |
+
max_length: 4096
|
| 58 |
+
temperature: 0.7
|
| 59 |
+
top_p: 0.9
|
| 60 |
+
top_k: 40
|
| 61 |
+
repetition_penalty: 1.1
|
| 62 |
+
|
| 63 |
+
# LoRA Configuration
|
| 64 |
+
lora_config:
|
| 65 |
+
r: 16
|
| 66 |
+
lora_alpha: 32
|
| 67 |
+
lora_dropout: 0.1
|
| 68 |
+
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
|
| 69 |
+
|
| 70 |
+
# Training Configuration
|
| 71 |
+
training_config:
|
| 72 |
+
learning_rate: 2e-4
|
| 73 |
+
batch_size: 4
|
| 74 |
+
gradient_accumulation_steps: 4
|
| 75 |
+
num_epochs: 3
|
| 76 |
+
warmup_steps: 100
|
| 77 |
+
save_steps: 500
|
| 78 |
+
eval_steps: 500
|
| 79 |
+
"""
|
| 80 |
+
else:
|
| 81 |
+
config_content = f"""# Model Configuration for {model_name}
|
| 82 |
+
model_name: "{model_name}"
|
| 83 |
+
model_path: "{model_path}"
|
| 84 |
+
max_length: 4096
|
| 85 |
+
temperature: 0.7
|
| 86 |
+
top_p: 0.9
|
| 87 |
+
top_k: 40
|
| 88 |
+
repetition_penalty: 1.1
|
| 89 |
+
|
| 90 |
+
# LoRA Configuration
|
| 91 |
+
lora_config:
|
| 92 |
+
r: 16
|
| 93 |
+
lora_alpha: 32
|
| 94 |
+
lora_dropout: 0.1
|
| 95 |
+
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
|
| 96 |
+
|
| 97 |
+
# Training Configuration
|
| 98 |
+
training_config:
|
| 99 |
+
learning_rate: 2e-4
|
| 100 |
+
batch_size: 4
|
| 101 |
+
gradient_accumulation_steps: 4
|
| 102 |
+
num_epochs: 3
|
| 103 |
+
warmup_steps: 100
|
| 104 |
+
save_steps: 500
|
| 105 |
+
eval_steps: 500
|
| 106 |
+
"""
|
| 107 |
+
|
| 108 |
+
config_file = config_dir / f"{model_name.split('/')[-1].lower().replace('-', '_')}_config.yaml"
|
| 109 |
+
with open(config_file, 'w') as f:
|
| 110 |
+
f.write(config_content)
|
| 111 |
+
|
| 112 |
+
print(f"✅ Model config created: {config_file}")
|
| 113 |
+
return str(config_file)
|
| 114 |
+
|
| 115 |
+
def main():
|
| 116 |
+
print("🚀 Download Alternative Models")
|
| 117 |
+
print("=" * 50)
|
| 118 |
+
|
| 119 |
+
if not check_huggingface_token():
|
| 120 |
+
sys.exit(1)
|
| 121 |
+
|
| 122 |
+
# Model options
|
| 123 |
+
models = [
|
| 124 |
+
{
|
| 125 |
+
"name": "meta-llama/Llama-3.2-1B-Instruct",
|
| 126 |
+
"path": "models/llama-3.2-1b-instruct",
|
| 127 |
+
"description": "Llama 3.2 1B Instruct - Lightweight and fast"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"name": "Qwen/Qwen3-4B-Instruct",
|
| 131 |
+
"path": "models/qwen3-4b-instruct",
|
| 132 |
+
"description": "Qwen3 4B Instruct - Good performance, reasonable size"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"name": "microsoft/DialoGPT-medium",
|
| 136 |
+
"path": "models/dialogpt-medium",
|
| 137 |
+
"description": "DialoGPT Medium - Conversational AI model"
|
| 138 |
+
}
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
print("📋 Pilih model yang ingin didownload:")
|
| 142 |
+
for i, model in enumerate(models, 1):
|
| 143 |
+
print(f"{i}. {model['name']}")
|
| 144 |
+
print(f" {model['description']}")
|
| 145 |
+
print()
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
choice = int(input("Pilihan (1-3): ").strip())
|
| 149 |
+
if choice < 1 or choice > len(models):
|
| 150 |
+
print("❌ Pilihan tidak valid")
|
| 151 |
+
return
|
| 152 |
+
|
| 153 |
+
selected_model = models[choice - 1]
|
| 154 |
+
|
| 155 |
+
print(f"\n🎯 Model yang dipilih: {selected_model['name']}")
|
| 156 |
+
print(f"📝 Deskripsi: {selected_model['description']}")
|
| 157 |
+
|
| 158 |
+
# Confirm download
|
| 159 |
+
confirm = input("\nLanjutkan download? (y/n): ").strip().lower()
|
| 160 |
+
if confirm not in ['y', 'yes']:
|
| 161 |
+
print("❌ Download dibatalkan")
|
| 162 |
+
return
|
| 163 |
+
|
| 164 |
+
# Download model
|
| 165 |
+
print(f"\n1️⃣ Downloading model...")
|
| 166 |
+
if download_model(selected_model['name'], selected_model['path']):
|
| 167 |
+
print(f"\n2️⃣ Creating model configuration...")
|
| 168 |
+
config_file = create_model_config(selected_model['name'], selected_model['path'])
|
| 169 |
+
|
| 170 |
+
print("\n3️�� Setup selesai!")
|
| 171 |
+
print(f"\n📋 Langkah selanjutnya:")
|
| 172 |
+
print(f"1. Model tersimpan di: {selected_model['path']}")
|
| 173 |
+
print(f"2. Config tersimpan di: {config_file}")
|
| 174 |
+
print("3. Jalankan: python scripts/finetune_lora.py")
|
| 175 |
+
print("4. Atau gunakan Novita AI: python scripts/novita_ai_setup.py")
|
| 176 |
+
|
| 177 |
+
except ValueError:
|
| 178 |
+
print("❌ Input tidak valid")
|
| 179 |
+
except KeyboardInterrupt:
|
| 180 |
+
print("\n👋 Download dibatalkan")
|
| 181 |
+
|
| 182 |
+
if __name__ == "__main__":
|
| 183 |
+
main()
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
|
scripts/download_model.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk download dan setup model Llama 3.1 8B
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import subprocess
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
def check_huggingface_token():
|
| 12 |
+
"""Check if HuggingFace token is available"""
|
| 13 |
+
token = os.getenv('HUGGINGFACE_TOKEN')
|
| 14 |
+
if not token:
|
| 15 |
+
print("❌ HUGGINGFACE_TOKEN tidak ditemukan!")
|
| 16 |
+
print("Silakan set environment variable:")
|
| 17 |
+
print("export HUGGINGFACE_TOKEN='your_token_here'")
|
| 18 |
+
print("\nAtau buat file .env dengan isi:")
|
| 19 |
+
print("HUGGINGFACE_TOKEN=your_token_here")
|
| 20 |
+
return False
|
| 21 |
+
return True
|
| 22 |
+
|
| 23 |
+
def download_model():
|
| 24 |
+
"""Download model menggunakan huggingface-cli"""
|
| 25 |
+
model_name = "meta-llama/Llama-3.1-8B-Instruct"
|
| 26 |
+
models_dir = Path("models")
|
| 27 |
+
|
| 28 |
+
if not models_dir.exists():
|
| 29 |
+
models_dir.mkdir(parents=True)
|
| 30 |
+
|
| 31 |
+
print(f"📥 Downloading model: {model_name}")
|
| 32 |
+
print(f"📁 Target directory: {models_dir.absolute()}")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
cmd = [
|
| 36 |
+
"huggingface-cli", "download",
|
| 37 |
+
model_name,
|
| 38 |
+
"--local-dir", str(models_dir / "llama-3.1-8b-instruct"),
|
| 39 |
+
"--local-dir-use-symlinks", "False"
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 43 |
+
|
| 44 |
+
if result.returncode == 0:
|
| 45 |
+
print("✅ Model berhasil didownload!")
|
| 46 |
+
else:
|
| 47 |
+
print(f"❌ Error downloading model: {result.stderr}")
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
except FileNotFoundError:
|
| 51 |
+
print("❌ huggingface-cli tidak ditemukan!")
|
| 52 |
+
print("Silakan install dengan: pip install huggingface_hub")
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
return True
|
| 56 |
+
|
| 57 |
+
def create_model_config():
|
| 58 |
+
"""Create model configuration file"""
|
| 59 |
+
config_dir = Path("configs")
|
| 60 |
+
config_dir.mkdir(exist_ok=True)
|
| 61 |
+
|
| 62 |
+
config_content = """# Model Configuration for Llama 3.1 8B
|
| 63 |
+
model_name: "meta-llama/Llama-3.1-8B-Instruct"
|
| 64 |
+
model_path: "./models/llama-3.1-8b-instruct"
|
| 65 |
+
max_length: 8192
|
| 66 |
+
temperature: 0.7
|
| 67 |
+
top_p: 0.9
|
| 68 |
+
top_k: 40
|
| 69 |
+
repetition_penalty: 1.1
|
| 70 |
+
|
| 71 |
+
# LoRA Configuration
|
| 72 |
+
lora_config:
|
| 73 |
+
r: 16
|
| 74 |
+
lora_alpha: 32
|
| 75 |
+
lora_dropout: 0.1
|
| 76 |
+
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
|
| 77 |
+
|
| 78 |
+
# Training Configuration
|
| 79 |
+
training_config:
|
| 80 |
+
learning_rate: 2e-4
|
| 81 |
+
batch_size: 4
|
| 82 |
+
gradient_accumulation_steps: 4
|
| 83 |
+
num_epochs: 3
|
| 84 |
+
warmup_steps: 100
|
| 85 |
+
save_steps: 500
|
| 86 |
+
eval_steps: 500
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
config_file = config_dir / "llama_config.yaml"
|
| 90 |
+
with open(config_file, 'w') as f:
|
| 91 |
+
f.write(config_content)
|
| 92 |
+
|
| 93 |
+
print(f"✅ Model config created: {config_file}")
|
| 94 |
+
|
| 95 |
+
def main():
|
| 96 |
+
print("🚀 Setup Base LLM - Llama 3.1 8B")
|
| 97 |
+
print("=" * 50)
|
| 98 |
+
|
| 99 |
+
if not check_huggingface_token():
|
| 100 |
+
sys.exit(1)
|
| 101 |
+
|
| 102 |
+
print("\n1️⃣ Downloading model...")
|
| 103 |
+
if not download_model():
|
| 104 |
+
sys.exit(1)
|
| 105 |
+
|
| 106 |
+
print("\n2️⃣ Creating model configuration...")
|
| 107 |
+
create_model_config()
|
| 108 |
+
|
| 109 |
+
print("\n3️⃣ Setup selesai!")
|
| 110 |
+
print("\n📋 Langkah selanjutnya:")
|
| 111 |
+
print("1. Jalankan: docker-compose up -d")
|
| 112 |
+
print("2. Test API: curl http://localhost:8000/health")
|
| 113 |
+
print("3. Mulai fine-tuning dengan LoRA")
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
main()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
|
scripts/download_open_models.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk download model yang benar-benar open source dan mudah diakses
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import subprocess
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
def check_huggingface_token():
|
| 12 |
+
"""Check if HuggingFace token is available"""
|
| 13 |
+
token = os.getenv('HUGGINGFACE_TOKEN')
|
| 14 |
+
if not token:
|
| 15 |
+
print("❌ HUGGINGFACE_TOKEN tidak ditemukan!")
|
| 16 |
+
print("Silakan set environment variable:")
|
| 17 |
+
print("export HUGGINGFACE_TOKEN='your_token_here'")
|
| 18 |
+
return False
|
| 19 |
+
return True
|
| 20 |
+
|
| 21 |
+
def download_model(model_name, model_path):
|
| 22 |
+
"""Download model menggunakan huggingface-cli"""
|
| 23 |
+
print(f"📥 Downloading model: {model_name}")
|
| 24 |
+
print(f"📁 Target directory: {model_path}")
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
cmd = [
|
| 28 |
+
"huggingface-cli", "download",
|
| 29 |
+
model_name,
|
| 30 |
+
"--local-dir", str(model_path),
|
| 31 |
+
"--local-dir-use-symlinks", "False"
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 35 |
+
|
| 36 |
+
if result.returncode == 0:
|
| 37 |
+
print("✅ Model berhasil didownload!")
|
| 38 |
+
return True
|
| 39 |
+
else:
|
| 40 |
+
print(f"❌ Error downloading model: {result.stderr}")
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
except FileNotFoundError:
|
| 44 |
+
print("❌ huggingface-cli tidak ditemukan!")
|
| 45 |
+
print("Silakan install dengan: pip install huggingface_hub")
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
def create_model_config(model_name, model_path):
|
| 49 |
+
"""Create model configuration file"""
|
| 50 |
+
config_dir = Path("configs")
|
| 51 |
+
config_dir.mkdir(exist_ok=True)
|
| 52 |
+
|
| 53 |
+
config_content = f"""# Model Configuration for {model_name}
|
| 54 |
+
model_name: "{model_name}"
|
| 55 |
+
model_path: "{model_path}"
|
| 56 |
+
max_length: 2048
|
| 57 |
+
temperature: 0.7
|
| 58 |
+
top_p: 0.9
|
| 59 |
+
top_k: 40
|
| 60 |
+
repetition_penalty: 1.1
|
| 61 |
+
|
| 62 |
+
# LoRA Configuration
|
| 63 |
+
lora_config:
|
| 64 |
+
r: 16
|
| 65 |
+
lora_alpha: 32
|
| 66 |
+
lora_dropout: 0.1
|
| 67 |
+
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
|
| 68 |
+
|
| 69 |
+
# Training Configuration
|
| 70 |
+
training_config:
|
| 71 |
+
learning_rate: 2e-4
|
| 72 |
+
batch_size: 4
|
| 73 |
+
gradient_accumulation_steps: 4
|
| 74 |
+
num_epochs: 3
|
| 75 |
+
warmup_steps: 100
|
| 76 |
+
save_steps: 500
|
| 77 |
+
eval_steps: 500
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
config_file = config_dir / f"{model_name.split('/')[-1].lower().replace('-', '_')}_config.yaml"
|
| 81 |
+
with open(config_file, 'w') as f:
|
| 82 |
+
f.write(config_content)
|
| 83 |
+
|
| 84 |
+
print(f"✅ Model config created: {config_file}")
|
| 85 |
+
return str(config_file)
|
| 86 |
+
|
| 87 |
+
def main():
|
| 88 |
+
print("🚀 Download Open Source Models")
|
| 89 |
+
print("=" * 50)
|
| 90 |
+
|
| 91 |
+
if not check_huggingface_token():
|
| 92 |
+
sys.exit(1)
|
| 93 |
+
|
| 94 |
+
# Model options - truly open source
|
| 95 |
+
models = [
|
| 96 |
+
{
|
| 97 |
+
"name": "microsoft/DialoGPT-medium",
|
| 98 |
+
"path": "models/dialogpt-medium",
|
| 99 |
+
"description": "DialoGPT Medium - Conversational AI model (355M parameters)"
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"name": "distilgpt2",
|
| 103 |
+
"path": "models/distilgpt2",
|
| 104 |
+
"description": "DistilGPT2 - Lightweight GPT-2 model (82M parameters)"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"name": "gpt2",
|
| 108 |
+
"path": "models/gpt2",
|
| 109 |
+
"description": "GPT-2 - Original GPT-2 model (124M parameters)"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"name": "EleutherAI/gpt-neo-125M",
|
| 113 |
+
"path": "models/gpt-neo-125m",
|
| 114 |
+
"description": "GPT-Neo 125M - Small but capable model (125M parameters)"
|
| 115 |
+
}
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
print("📋 Pilih model yang ingin didownload:")
|
| 119 |
+
for i, model in enumerate(models, 1):
|
| 120 |
+
print(f"{i}. {model['name']}")
|
| 121 |
+
print(f" {model['description']}")
|
| 122 |
+
print()
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
choice = int(input("Pilihan (1-4): ").strip())
|
| 126 |
+
if choice < 1 or choice > len(models):
|
| 127 |
+
print("❌ Pilihan tidak valid")
|
| 128 |
+
return
|
| 129 |
+
|
| 130 |
+
selected_model = models[choice - 1]
|
| 131 |
+
|
| 132 |
+
print(f"\n🎯 Model yang dipilih: {selected_model['name']}")
|
| 133 |
+
print(f"📝 Deskripsi: {selected_model['description']}")
|
| 134 |
+
|
| 135 |
+
# Confirm download
|
| 136 |
+
confirm = input("\nLanjutkan download? (y/n): ").strip().lower()
|
| 137 |
+
if confirm not in ['y', 'yes']:
|
| 138 |
+
print("❌ Download dibatalkan")
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
# Download model
|
| 142 |
+
print(f"\n1️⃣ Downloading model...")
|
| 143 |
+
if download_model(selected_model['name'], selected_model['path']):
|
| 144 |
+
print(f"\n2️⃣ Creating model configuration...")
|
| 145 |
+
config_file = create_model_config(selected_model['name'], selected_model['path'])
|
| 146 |
+
|
| 147 |
+
print("\n3️⃣ Setup selesai!")
|
| 148 |
+
print(f"\n📋 Langkah selanjutnya:")
|
| 149 |
+
print(f"1. Model tersimpan di: {selected_model['path']}")
|
| 150 |
+
print(f"2. Config tersimpan di: {config_file}")
|
| 151 |
+
print("3. Jalankan: python scripts/finetune_lora.py")
|
| 152 |
+
print("4. Atau gunakan Novita AI: python scripts/novita_ai_setup.py")
|
| 153 |
+
|
| 154 |
+
except ValueError:
|
| 155 |
+
print("❌ Input tidak valid")
|
| 156 |
+
except KeyboardInterrupt:
|
| 157 |
+
print("\n👋 Download dibatalkan")
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
main()
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
|
scripts/finetune_lora.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk fine-tuning model Llama 3.1 8B dengan LoRA
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import yaml
|
| 9 |
+
import json
|
| 10 |
+
import torch
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from transformers import (
|
| 13 |
+
AutoTokenizer,
|
| 14 |
+
AutoModelForCausalLM,
|
| 15 |
+
TrainingArguments,
|
| 16 |
+
Trainer,
|
| 17 |
+
DataCollatorForLanguageModeling
|
| 18 |
+
)
|
| 19 |
+
from peft import (
|
| 20 |
+
LoraConfig,
|
| 21 |
+
get_peft_model,
|
| 22 |
+
TaskType,
|
| 23 |
+
prepare_model_for_kbit_training
|
| 24 |
+
)
|
| 25 |
+
from datasets import Dataset
|
| 26 |
+
import logging
|
| 27 |
+
|
| 28 |
+
# Setup logging
|
| 29 |
+
logging.basicConfig(level=logging.INFO)
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
def load_config(config_path):
|
| 33 |
+
"""Load configuration from YAML file"""
|
| 34 |
+
try:
|
| 35 |
+
with open(config_path, 'r') as f:
|
| 36 |
+
config = yaml.safe_load(f)
|
| 37 |
+
return config
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Error loading config: {e}")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
def load_model_and_tokenizer(config):
|
| 43 |
+
"""Load base model and tokenizer"""
|
| 44 |
+
model_path = config['model_path']
|
| 45 |
+
|
| 46 |
+
logger.info(f"Loading model from: {model_path}")
|
| 47 |
+
|
| 48 |
+
# Load tokenizer
|
| 49 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 50 |
+
model_path,
|
| 51 |
+
trust_remote_code=True,
|
| 52 |
+
padding_side="right"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
if tokenizer.pad_token is None:
|
| 56 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 57 |
+
|
| 58 |
+
# Load model
|
| 59 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 60 |
+
model_path,
|
| 61 |
+
torch_dtype=torch.float16,
|
| 62 |
+
device_map="auto",
|
| 63 |
+
trust_remote_code=True
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
# Prepare model for k-bit training
|
| 67 |
+
model = prepare_model_for_kbit_training(model)
|
| 68 |
+
|
| 69 |
+
return model, tokenizer
|
| 70 |
+
|
| 71 |
+
def setup_lora_config(config):
|
| 72 |
+
"""Setup LoRA configuration"""
|
| 73 |
+
lora_config = config['lora_config']
|
| 74 |
+
|
| 75 |
+
peft_config = LoraConfig(
|
| 76 |
+
task_type=TaskType.CAUSAL_LM,
|
| 77 |
+
r=lora_config['r'],
|
| 78 |
+
lora_alpha=lora_config['lora_alpha'],
|
| 79 |
+
lora_dropout=lora_config['lora_dropout'],
|
| 80 |
+
target_modules=lora_config['target_modules'],
|
| 81 |
+
bias="none",
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
return peft_config
|
| 85 |
+
|
| 86 |
+
def prepare_dataset(data_path, tokenizer, max_length=512):
|
| 87 |
+
"""Prepare dataset for training"""
|
| 88 |
+
logger.info(f"Loading dataset from: {data_path}")
|
| 89 |
+
|
| 90 |
+
# Load your dataset here
|
| 91 |
+
# Support for JSONL format (one JSON object per line)
|
| 92 |
+
if data_path.endswith('.jsonl'):
|
| 93 |
+
# Read JSONL file line by line
|
| 94 |
+
data = []
|
| 95 |
+
with open(data_path, 'r', encoding='utf-8') as f:
|
| 96 |
+
for line_num, line in enumerate(f, 1):
|
| 97 |
+
line = line.strip()
|
| 98 |
+
if line:
|
| 99 |
+
try:
|
| 100 |
+
json_obj = json.loads(line)
|
| 101 |
+
data.append(json_obj)
|
| 102 |
+
except json.JSONDecodeError as e:
|
| 103 |
+
logger.warning(f"Invalid JSON at line {line_num}: {e}")
|
| 104 |
+
continue
|
| 105 |
+
|
| 106 |
+
if not data:
|
| 107 |
+
raise ValueError("No valid JSON objects found in JSONL file")
|
| 108 |
+
|
| 109 |
+
# Convert to Dataset
|
| 110 |
+
dataset = Dataset.from_list(data)
|
| 111 |
+
logger.info(f"Loaded {len(dataset)} samples from JSONL file")
|
| 112 |
+
|
| 113 |
+
elif data_path.endswith('.json'):
|
| 114 |
+
dataset = Dataset.from_json(data_path)
|
| 115 |
+
elif data_path.endswith('.csv'):
|
| 116 |
+
dataset = Dataset.from_csv(data_path)
|
| 117 |
+
else:
|
| 118 |
+
raise ValueError("Unsupported data format. Use .jsonl, .json, or .csv")
|
| 119 |
+
|
| 120 |
+
# Validate dataset structure
|
| 121 |
+
if 'text' not in dataset.column_names:
|
| 122 |
+
logger.warning("Column 'text' not found in dataset")
|
| 123 |
+
logger.info(f"Available columns: {dataset.column_names}")
|
| 124 |
+
# Try to find alternative text column
|
| 125 |
+
text_columns = [col for col in dataset.column_names if 'text' in col.lower() or 'content' in col.lower()]
|
| 126 |
+
if text_columns:
|
| 127 |
+
logger.info(f"Found potential text columns: {text_columns}")
|
| 128 |
+
# Use first found text column
|
| 129 |
+
text_column = text_columns[0]
|
| 130 |
+
else:
|
| 131 |
+
raise ValueError("No text column found. Dataset must contain a 'text' column or similar")
|
| 132 |
+
else:
|
| 133 |
+
text_column = 'text'
|
| 134 |
+
|
| 135 |
+
def tokenize_function(examples):
|
| 136 |
+
# Tokenize the texts
|
| 137 |
+
tokenized = tokenizer(
|
| 138 |
+
examples[text_column],
|
| 139 |
+
truncation=True,
|
| 140 |
+
padding=True,
|
| 141 |
+
max_length=max_length,
|
| 142 |
+
return_tensors="pt"
|
| 143 |
+
)
|
| 144 |
+
return tokenized
|
| 145 |
+
|
| 146 |
+
# Tokenize dataset
|
| 147 |
+
tokenized_dataset = dataset.map(
|
| 148 |
+
tokenize_function,
|
| 149 |
+
batched=True,
|
| 150 |
+
remove_columns=dataset.column_names
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
return tokenized_dataset
|
| 154 |
+
|
| 155 |
+
def train_model(model, tokenizer, dataset, config, output_dir):
|
| 156 |
+
"""Train the model with LoRA"""
|
| 157 |
+
training_config = config['training_config']
|
| 158 |
+
|
| 159 |
+
# Setup training arguments
|
| 160 |
+
training_args = TrainingArguments(
|
| 161 |
+
output_dir=output_dir,
|
| 162 |
+
num_train_epochs=training_config['num_epochs'],
|
| 163 |
+
per_device_train_batch_size=training_config['batch_size'],
|
| 164 |
+
gradient_accumulation_steps=training_config['gradient_accumulation_steps'],
|
| 165 |
+
learning_rate=training_config['learning_rate'],
|
| 166 |
+
warmup_steps=training_config['warmup_steps'],
|
| 167 |
+
save_steps=training_config['save_steps'],
|
| 168 |
+
eval_steps=training_config['eval_steps'],
|
| 169 |
+
logging_steps=10,
|
| 170 |
+
save_total_limit=3,
|
| 171 |
+
prediction_loss_only=True,
|
| 172 |
+
remove_unused_columns=False,
|
| 173 |
+
push_to_hub=False,
|
| 174 |
+
report_to=None,
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
# Setup data collator
|
| 178 |
+
data_collator = DataCollatorForLanguageModeling(
|
| 179 |
+
tokenizer=tokenizer,
|
| 180 |
+
mlm=False,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Setup trainer
|
| 184 |
+
trainer = Trainer(
|
| 185 |
+
model=model,
|
| 186 |
+
args=training_args,
|
| 187 |
+
train_dataset=dataset,
|
| 188 |
+
data_collator=data_collator,
|
| 189 |
+
tokenizer=tokenizer,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
# Start training
|
| 193 |
+
logger.info("Starting training...")
|
| 194 |
+
trainer.train()
|
| 195 |
+
|
| 196 |
+
# Save the model
|
| 197 |
+
trainer.save_model()
|
| 198 |
+
logger.info(f"Model saved to: {output_dir}")
|
| 199 |
+
|
| 200 |
+
def main():
|
| 201 |
+
print("🚀 LoRA Fine-tuning - Llama 3.1 8B")
|
| 202 |
+
print("=" * 50)
|
| 203 |
+
|
| 204 |
+
# Load configuration
|
| 205 |
+
config_path = "configs/llama_config.yaml"
|
| 206 |
+
if not os.path.exists(config_path):
|
| 207 |
+
print(f"❌ Config file tidak ditemukan: {config_path}")
|
| 208 |
+
print("Jalankan download_model.py terlebih dahulu")
|
| 209 |
+
sys.exit(1)
|
| 210 |
+
|
| 211 |
+
config = load_config(config_path)
|
| 212 |
+
if not config:
|
| 213 |
+
sys.exit(1)
|
| 214 |
+
|
| 215 |
+
# Setup paths
|
| 216 |
+
output_dir = Path("models/finetuned-llama-lora")
|
| 217 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 218 |
+
|
| 219 |
+
# Load model and tokenizer
|
| 220 |
+
print("1️⃣ Loading model and tokenizer...")
|
| 221 |
+
model, tokenizer = load_model_and_tokenizer(config)
|
| 222 |
+
|
| 223 |
+
# Setup LoRA
|
| 224 |
+
print("2️⃣ Setting up LoRA configuration...")
|
| 225 |
+
peft_config = setup_lora_config(config)
|
| 226 |
+
model = get_peft_model(model, peft_config)
|
| 227 |
+
|
| 228 |
+
# Print trainable parameters
|
| 229 |
+
model.print_trainable_parameters()
|
| 230 |
+
|
| 231 |
+
# Prepare dataset (placeholder - replace with your data)
|
| 232 |
+
print("3️⃣ Preparing dataset...")
|
| 233 |
+
data_path = "data/training_data.jsonl" # Default to JSONL format
|
| 234 |
+
|
| 235 |
+
if not os.path.exists(data_path):
|
| 236 |
+
print(f"⚠️ Data file tidak ditemukan: {data_path}")
|
| 237 |
+
print("Buat dataset terlebih dahulu atau update path di script")
|
| 238 |
+
print("Skipping training...")
|
| 239 |
+
return
|
| 240 |
+
|
| 241 |
+
dataset = prepare_dataset(data_path, tokenizer)
|
| 242 |
+
|
| 243 |
+
# Train model
|
| 244 |
+
print("4️⃣ Starting training...")
|
| 245 |
+
train_model(model, tokenizer, dataset, config, output_dir)
|
| 246 |
+
|
| 247 |
+
print("✅ Training selesai!")
|
| 248 |
+
print(f"📁 Model tersimpan di: {output_dir}")
|
| 249 |
+
|
| 250 |
+
if __name__ == "__main__":
|
| 251 |
+
main()
|
scripts/inference_textilindo_ai.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Inference script untuk Textilindo AI Assistant
|
| 4 |
+
Menggunakan model yang sudah di-fine-tune dengan LoRA
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import torch
|
| 10 |
+
import argparse
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 13 |
+
from peft import PeftModel
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
logging.basicConfig(level=logging.INFO)
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
def load_system_prompt(system_prompt_path):
|
| 20 |
+
"""Load system prompt from markdown file"""
|
| 21 |
+
try:
|
| 22 |
+
with open(system_prompt_path, 'r', encoding='utf-8') as f:
|
| 23 |
+
content = f.read()
|
| 24 |
+
|
| 25 |
+
# Extract SYSTEM_PROMPT from markdown
|
| 26 |
+
if 'SYSTEM_PROMPT = """' in content:
|
| 27 |
+
start = content.find('SYSTEM_PROMPT = """') + len('SYSTEM_PROMPT = """')
|
| 28 |
+
end = content.find('"""', start)
|
| 29 |
+
system_prompt = content[start:end].strip()
|
| 30 |
+
else:
|
| 31 |
+
# Fallback: use entire content
|
| 32 |
+
system_prompt = content.strip()
|
| 33 |
+
|
| 34 |
+
return system_prompt
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Error loading system prompt: {e}")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
def load_model(model_path, lora_path=None):
|
| 40 |
+
"""Load model with optional LoRA weights"""
|
| 41 |
+
logger.info(f"Loading base model from: {model_path}")
|
| 42 |
+
|
| 43 |
+
# Load tokenizer
|
| 44 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 45 |
+
model_path,
|
| 46 |
+
trust_remote_code=True
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
if tokenizer.pad_token is None:
|
| 50 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 51 |
+
|
| 52 |
+
# Load base model
|
| 53 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 54 |
+
model_path,
|
| 55 |
+
torch_dtype=torch.float16,
|
| 56 |
+
device_map="auto",
|
| 57 |
+
trust_remote_code=True
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
# Load LoRA weights if provided
|
| 61 |
+
if lora_path and os.path.exists(lora_path):
|
| 62 |
+
logger.info(f"Loading LoRA weights from: {lora_path}")
|
| 63 |
+
model = PeftModel.from_pretrained(model, lora_path)
|
| 64 |
+
else:
|
| 65 |
+
logger.warning("No LoRA weights found, using base model")
|
| 66 |
+
|
| 67 |
+
return model, tokenizer
|
| 68 |
+
|
| 69 |
+
def generate_response(model, tokenizer, user_input, system_prompt, max_length=512):
|
| 70 |
+
"""Generate response from the model"""
|
| 71 |
+
# Create full prompt with system prompt
|
| 72 |
+
full_prompt = f"<|system|>\n{system_prompt}\n<|user|>\n{user_input}\n<|assistant|>\n"
|
| 73 |
+
|
| 74 |
+
inputs = tokenizer(full_prompt, return_tensors="pt").to(model.device)
|
| 75 |
+
|
| 76 |
+
with torch.no_grad():
|
| 77 |
+
outputs = model.generate(
|
| 78 |
+
**inputs,
|
| 79 |
+
max_length=max_length,
|
| 80 |
+
temperature=0.7,
|
| 81 |
+
top_p=0.9,
|
| 82 |
+
top_k=40,
|
| 83 |
+
repetition_penalty=1.1,
|
| 84 |
+
do_sample=True,
|
| 85 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 86 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 87 |
+
stop_strings=["<|end|>", "<|user|>"]
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 91 |
+
|
| 92 |
+
# Extract only the assistant's response
|
| 93 |
+
if "<|assistant|>" in response:
|
| 94 |
+
assistant_response = response.split("<|assistant|>")[-1].strip()
|
| 95 |
+
# Remove any remaining special tokens
|
| 96 |
+
assistant_response = assistant_response.replace("<|end|>", "").strip()
|
| 97 |
+
return assistant_response
|
| 98 |
+
else:
|
| 99 |
+
return response
|
| 100 |
+
|
| 101 |
+
def interactive_chat(model, tokenizer, system_prompt):
|
| 102 |
+
"""Interactive chat mode"""
|
| 103 |
+
print("🤖 Textilindo AI Assistant - Chat Mode")
|
| 104 |
+
print("=" * 60)
|
| 105 |
+
print("Type 'quit' to exit")
|
| 106 |
+
print("-" * 60)
|
| 107 |
+
|
| 108 |
+
while True:
|
| 109 |
+
try:
|
| 110 |
+
user_input = input("\n👤 Customer: ").strip()
|
| 111 |
+
|
| 112 |
+
if user_input.lower() in ['quit', 'exit', 'q']:
|
| 113 |
+
print("👋 Terima kasih! Sampai jumpa!")
|
| 114 |
+
break
|
| 115 |
+
|
| 116 |
+
if not user_input:
|
| 117 |
+
continue
|
| 118 |
+
|
| 119 |
+
print("\n🤖 Textilindo AI: ", end="", flush=True)
|
| 120 |
+
response = generate_response(model, tokenizer, user_input, system_prompt)
|
| 121 |
+
print(response)
|
| 122 |
+
|
| 123 |
+
except KeyboardInterrupt:
|
| 124 |
+
print("\n👋 Terima kasih! Sampai jumpa!")
|
| 125 |
+
break
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error(f"Error generating response: {e}")
|
| 128 |
+
print(f"❌ Error: {e}")
|
| 129 |
+
|
| 130 |
+
def main():
|
| 131 |
+
parser = argparse.ArgumentParser(description='Textilindo AI Assistant Inference')
|
| 132 |
+
parser.add_argument('--model_path', type=str, default='./models/llama-3.1-8b-instruct',
|
| 133 |
+
help='Path to base model')
|
| 134 |
+
parser.add_argument('--lora_path', type=str, default=None,
|
| 135 |
+
help='Path to LoRA weights')
|
| 136 |
+
parser.add_argument('--system_prompt', type=str, default='configs/system_prompt.md',
|
| 137 |
+
help='Path to system prompt file')
|
| 138 |
+
parser.add_argument('--prompt', type=str, default=None,
|
| 139 |
+
help='Single prompt to process')
|
| 140 |
+
|
| 141 |
+
args = parser.parse_args()
|
| 142 |
+
|
| 143 |
+
print("🤖 Textilindo AI Assistant - Inference")
|
| 144 |
+
print("=" * 60)
|
| 145 |
+
|
| 146 |
+
# Load system prompt
|
| 147 |
+
system_prompt = load_system_prompt(args.system_prompt)
|
| 148 |
+
if not system_prompt:
|
| 149 |
+
print(f"❌ System prompt tidak ditemukan: {args.system_prompt}")
|
| 150 |
+
sys.exit(1)
|
| 151 |
+
|
| 152 |
+
# Check if model exists
|
| 153 |
+
if not os.path.exists(args.model_path):
|
| 154 |
+
print(f"❌ Base model tidak ditemukan: {args.model_path}")
|
| 155 |
+
print("Jalankan setup_textilindo_training.py terlebih dahulu")
|
| 156 |
+
sys.exit(1)
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
# Load model
|
| 160 |
+
print("1️⃣ Loading model...")
|
| 161 |
+
model, tokenizer = load_model(args.model_path, args.lora_path)
|
| 162 |
+
print("✅ Model loaded successfully!")
|
| 163 |
+
|
| 164 |
+
if args.prompt:
|
| 165 |
+
# Single prompt mode
|
| 166 |
+
print(f"\n📝 Processing prompt: {args.prompt}")
|
| 167 |
+
response = generate_response(model, tokenizer, args.prompt, system_prompt)
|
| 168 |
+
print(f"\n🤖 Response: {response}")
|
| 169 |
+
else:
|
| 170 |
+
# Interactive mode
|
| 171 |
+
interactive_chat(model, tokenizer, system_prompt)
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
logger.error(f"Error: {e}")
|
| 175 |
+
print(f"❌ Error loading model: {e}")
|
| 176 |
+
|
| 177 |
+
if __name__ == "__main__":
|
| 178 |
+
main()
|
scripts/local_training_setup.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk setup training lokal dengan model yang lebih kecil
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import subprocess
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
logging.basicConfig(level=logging.INFO)
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
def check_system_requirements():
|
| 16 |
+
"""Check system requirements untuk training lokal"""
|
| 17 |
+
print("🔍 Checking System Requirements...")
|
| 18 |
+
print("=" * 50)
|
| 19 |
+
|
| 20 |
+
# Check Python version
|
| 21 |
+
python_version = sys.version_info
|
| 22 |
+
print(f"🐍 Python: {python_version.major}.{python_version.minor}.{python_version.micro}")
|
| 23 |
+
|
| 24 |
+
if python_version < (3, 8):
|
| 25 |
+
print("❌ Python 3.8+ required")
|
| 26 |
+
return False
|
| 27 |
+
else:
|
| 28 |
+
print("✅ Python version OK")
|
| 29 |
+
|
| 30 |
+
# Check available memory
|
| 31 |
+
try:
|
| 32 |
+
import psutil
|
| 33 |
+
memory = psutil.virtual_memory()
|
| 34 |
+
memory_gb = memory.total / (1024**3)
|
| 35 |
+
print(f"💾 RAM: {memory_gb:.1f} GB")
|
| 36 |
+
|
| 37 |
+
if memory_gb < 8:
|
| 38 |
+
print("⚠️ Warning: Less than 8GB RAM may cause issues")
|
| 39 |
+
else:
|
| 40 |
+
print("✅ RAM sufficient")
|
| 41 |
+
except ImportError:
|
| 42 |
+
print("⚠️ psutil not available, cannot check memory")
|
| 43 |
+
|
| 44 |
+
# Check disk space
|
| 45 |
+
try:
|
| 46 |
+
disk = psutil.disk_usage('.')
|
| 47 |
+
disk_gb = disk.free / (1024**3)
|
| 48 |
+
print(f"💿 Free Disk: {disk_gb:.1f} GB")
|
| 49 |
+
|
| 50 |
+
if disk_gb < 10:
|
| 51 |
+
print("⚠️ Warning: Less than 10GB free space")
|
| 52 |
+
else:
|
| 53 |
+
print("✅ Disk space sufficient")
|
| 54 |
+
except:
|
| 55 |
+
print("⚠️ Cannot check disk space")
|
| 56 |
+
|
| 57 |
+
# Check CUDA (optional)
|
| 58 |
+
try:
|
| 59 |
+
import torch
|
| 60 |
+
if torch.cuda.is_available():
|
| 61 |
+
gpu_count = torch.cuda.device_count()
|
| 62 |
+
print(f"🎮 CUDA GPUs: {gpu_count}")
|
| 63 |
+
for i in range(gpu_count):
|
| 64 |
+
gpu_name = torch.cuda.get_device_name(i)
|
| 65 |
+
gpu_memory = torch.cuda.get_device_properties(i).total_memory / (1024**3)
|
| 66 |
+
print(f" GPU {i}: {gpu_name} ({gpu_memory:.1f} GB)")
|
| 67 |
+
print("✅ CUDA available - Fast training possible")
|
| 68 |
+
else:
|
| 69 |
+
print("⚠️ CUDA not available - Training will be slower (CPU only)")
|
| 70 |
+
except ImportError:
|
| 71 |
+
print("⚠️ PyTorch not available")
|
| 72 |
+
|
| 73 |
+
return True
|
| 74 |
+
|
| 75 |
+
def download_small_model():
|
| 76 |
+
"""Download model yang cocok untuk training lokal"""
|
| 77 |
+
print("\n📥 Downloading Small Model for Local Training...")
|
| 78 |
+
print("=" * 50)
|
| 79 |
+
|
| 80 |
+
# Model options yang cocok untuk training lokal
|
| 81 |
+
small_models = [
|
| 82 |
+
{
|
| 83 |
+
"name": "distilgpt2",
|
| 84 |
+
"path": "models/distilgpt2",
|
| 85 |
+
"size_mb": 82,
|
| 86 |
+
"description": "DistilGPT2 - Very lightweight (82M parameters)"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"name": "microsoft/DialoGPT-small",
|
| 90 |
+
"path": "models/dialogpt-small",
|
| 91 |
+
"size_mb": 117,
|
| 92 |
+
"description": "DialoGPT Small - Conversational (117M parameters)"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"name": "EleutherAI/gpt-neo-125M",
|
| 96 |
+
"path": "models/gpt-neo-125m",
|
| 97 |
+
"size_mb": 125,
|
| 98 |
+
"description": "GPT-Neo 125M - Good balance (125M parameters)"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"name": "gpt2",
|
| 102 |
+
"path": "models/gpt2",
|
| 103 |
+
"size_mb": 124,
|
| 104 |
+
"description": "GPT-2 - Original but small (124M parameters)"
|
| 105 |
+
}
|
| 106 |
+
]
|
| 107 |
+
|
| 108 |
+
print("📋 Available small models:")
|
| 109 |
+
for i, model in enumerate(small_models, 1):
|
| 110 |
+
print(f"{i}. {model['name']}")
|
| 111 |
+
print(f" {model['description']}")
|
| 112 |
+
print(f" Size: ~{model['size_mb']} MB")
|
| 113 |
+
print()
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
choice = int(input("Pilih model (1-4): ").strip())
|
| 117 |
+
if choice < 1 or choice > len(small_models):
|
| 118 |
+
print("❌ Pilihan tidak valid, menggunakan default: distilgpt2")
|
| 119 |
+
choice = 1
|
| 120 |
+
|
| 121 |
+
selected_model = small_models[choice - 1]
|
| 122 |
+
print(f"\n🎯 Selected: {selected_model['name']}")
|
| 123 |
+
|
| 124 |
+
# Download model
|
| 125 |
+
print(f"\n📥 Downloading {selected_model['name']}...")
|
| 126 |
+
if download_model_with_transformers(selected_model['name'], selected_model['path']):
|
| 127 |
+
print(f"✅ Model downloaded successfully!")
|
| 128 |
+
return selected_model
|
| 129 |
+
else:
|
| 130 |
+
print("❌ Download failed")
|
| 131 |
+
return None
|
| 132 |
+
|
| 133 |
+
except (ValueError, KeyboardInterrupt):
|
| 134 |
+
print("\n❌ Download cancelled")
|
| 135 |
+
return None
|
| 136 |
+
|
| 137 |
+
def download_model_with_transformers(model_name, model_path):
|
| 138 |
+
"""Download model menggunakan transformers library"""
|
| 139 |
+
try:
|
| 140 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 141 |
+
|
| 142 |
+
print(f"Downloading tokenizer...")
|
| 143 |
+
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 144 |
+
tokenizer.save_pretrained(model_path)
|
| 145 |
+
|
| 146 |
+
print(f"Downloading model...")
|
| 147 |
+
model = AutoModelForCausalLM.from_pretrained(model_name)
|
| 148 |
+
model.save_pretrained(model_path)
|
| 149 |
+
|
| 150 |
+
return True
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Error downloading model: {e}")
|
| 154 |
+
return False
|
| 155 |
+
|
| 156 |
+
def create_local_training_config(model_info):
|
| 157 |
+
"""Create configuration untuk training lokal"""
|
| 158 |
+
config_dir = Path("configs")
|
| 159 |
+
config_dir.mkdir(exist_ok=True)
|
| 160 |
+
|
| 161 |
+
config_content = f"""# Local Training Configuration for {model_info['name']}
|
| 162 |
+
model_name: "{model_info['name']}"
|
| 163 |
+
model_path: "{model_info['path']}"
|
| 164 |
+
max_length: 512
|
| 165 |
+
temperature: 0.7
|
| 166 |
+
top_p: 0.9
|
| 167 |
+
top_k: 40
|
| 168 |
+
repetition_penalty: 1.1
|
| 169 |
+
|
| 170 |
+
# LoRA Configuration (for memory efficiency)
|
| 171 |
+
lora_config:
|
| 172 |
+
r: 8 # Reduced for smaller models
|
| 173 |
+
lora_alpha: 16
|
| 174 |
+
lora_dropout: 0.1
|
| 175 |
+
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj"]
|
| 176 |
+
|
| 177 |
+
# Training Configuration (optimized for local training)
|
| 178 |
+
training_config:
|
| 179 |
+
learning_rate: 1e-4 # Lower learning rate for stability
|
| 180 |
+
batch_size: 2 # Smaller batch size for memory
|
| 181 |
+
gradient_accumulation_steps: 8 # Accumulate gradients
|
| 182 |
+
num_epochs: 3
|
| 183 |
+
warmup_steps: 50
|
| 184 |
+
save_steps: 100
|
| 185 |
+
eval_steps: 100
|
| 186 |
+
max_grad_norm: 1.0
|
| 187 |
+
weight_decay: 0.01
|
| 188 |
+
|
| 189 |
+
# Hardware Configuration
|
| 190 |
+
hardware_config:
|
| 191 |
+
device: "auto" # Will use GPU if available
|
| 192 |
+
mixed_precision: true # Use mixed precision for memory efficiency
|
| 193 |
+
gradient_checkpointing: true # Save memory during training
|
| 194 |
+
"""
|
| 195 |
+
|
| 196 |
+
config_file = config_dir / f"local_training_{model_info['name'].split('/')[-1].lower().replace('-', '_')}.yaml"
|
| 197 |
+
with open(config_file, 'w') as f:
|
| 198 |
+
f.write(config_content)
|
| 199 |
+
|
| 200 |
+
print(f"✅ Local training config created: {config_file}")
|
| 201 |
+
return str(config_file)
|
| 202 |
+
|
| 203 |
+
def setup_local_training_environment():
|
| 204 |
+
"""Setup environment untuk training lokal"""
|
| 205 |
+
print("\n🔧 Setting up Local Training Environment...")
|
| 206 |
+
print("=" * 50)
|
| 207 |
+
|
| 208 |
+
# Install required packages
|
| 209 |
+
packages = [
|
| 210 |
+
"torch",
|
| 211 |
+
"transformers",
|
| 212 |
+
"datasets",
|
| 213 |
+
"accelerate",
|
| 214 |
+
"peft",
|
| 215 |
+
"bitsandbytes",
|
| 216 |
+
"scipy",
|
| 217 |
+
"scikit-learn"
|
| 218 |
+
]
|
| 219 |
+
|
| 220 |
+
print("📦 Installing required packages...")
|
| 221 |
+
for package in packages:
|
| 222 |
+
try:
|
| 223 |
+
subprocess.run([sys.executable, "-m", "pip", "install", package],
|
| 224 |
+
check=True, capture_output=True)
|
| 225 |
+
print(f"✅ {package} installed")
|
| 226 |
+
except subprocess.CalledProcessError:
|
| 227 |
+
print(f"⚠️ Failed to install {package}")
|
| 228 |
+
|
| 229 |
+
print("\n✅ Local training environment setup complete!")
|
| 230 |
+
|
| 231 |
+
def main():
|
| 232 |
+
print("🚀 Local Training Setup")
|
| 233 |
+
print("=" * 50)
|
| 234 |
+
|
| 235 |
+
# Check system requirements
|
| 236 |
+
if not check_system_requirements():
|
| 237 |
+
print("❌ System requirements not met")
|
| 238 |
+
return
|
| 239 |
+
|
| 240 |
+
# Setup training environment
|
| 241 |
+
setup_local_training_environment()
|
| 242 |
+
|
| 243 |
+
# Download small model
|
| 244 |
+
model_info = download_small_model()
|
| 245 |
+
if not model_info:
|
| 246 |
+
print("❌ Model download failed")
|
| 247 |
+
return
|
| 248 |
+
|
| 249 |
+
# Create training config
|
| 250 |
+
config_file = create_local_training_config(model_info)
|
| 251 |
+
|
| 252 |
+
print(f"\n🎉 Local Training Setup Complete!")
|
| 253 |
+
print("=" * 50)
|
| 254 |
+
print(f"📁 Model: {model_info['path']}")
|
| 255 |
+
print(f"⚙️ Config: {config_file}")
|
| 256 |
+
print(f"📊 Dataset: data/lora_dataset_20250829_113330.jsonl")
|
| 257 |
+
|
| 258 |
+
print(f"\n📋 Next steps:")
|
| 259 |
+
print("1. Review configuration: cat configs/local_training_*.yaml")
|
| 260 |
+
print("2. Start training: python scripts/finetune_lora.py")
|
| 261 |
+
print("3. Monitor training: tail -f logs/training.log")
|
| 262 |
+
|
| 263 |
+
print(f"\n💡 Tips for local training:")
|
| 264 |
+
print("- Use smaller batch sizes if you run out of memory")
|
| 265 |
+
print("- Enable gradient checkpointing for memory efficiency")
|
| 266 |
+
print("- Monitor GPU memory usage with nvidia-smi")
|
| 267 |
+
print("- Consider using mixed precision training")
|
| 268 |
+
|
| 269 |
+
if __name__ == "__main__":
|
| 270 |
+
main()
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
|
scripts/novita_ai_setup.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk setup dan menggunakan Novita AI
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import requests
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO)
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
class NovitaAIClient:
|
| 17 |
+
def __init__(self, api_key):
|
| 18 |
+
self.api_key = api_key
|
| 19 |
+
self.base_url = "https://api.novita.ai"
|
| 20 |
+
self.headers = {
|
| 21 |
+
"Authorization": f"Bearer {api_key}",
|
| 22 |
+
"Content-Type": "application/json"
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
def test_connection(self):
|
| 26 |
+
"""Test koneksi ke Novita AI API"""
|
| 27 |
+
try:
|
| 28 |
+
response = requests.get(
|
| 29 |
+
f"{self.base_url}/v1/models",
|
| 30 |
+
headers=self.headers
|
| 31 |
+
)
|
| 32 |
+
if response.status_code == 200:
|
| 33 |
+
logger.info("✅ Koneksi ke Novita AI berhasil!")
|
| 34 |
+
return True
|
| 35 |
+
else:
|
| 36 |
+
logger.error(f"❌ Error: {response.status_code} - {response.text}")
|
| 37 |
+
return False
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"❌ Error koneksi: {e}")
|
| 40 |
+
return False
|
| 41 |
+
|
| 42 |
+
def get_available_models(self):
|
| 43 |
+
"""Dapatkan daftar model yang tersedia"""
|
| 44 |
+
try:
|
| 45 |
+
response = requests.get(
|
| 46 |
+
f"{self.base_url}/v1/models",
|
| 47 |
+
headers=self.headers
|
| 48 |
+
)
|
| 49 |
+
if response.status_code == 200:
|
| 50 |
+
models = response.json()
|
| 51 |
+
logger.info("📋 Model yang tersedia:")
|
| 52 |
+
for model in models.get('data', []):
|
| 53 |
+
logger.info(f" - {model.get('id', 'Unknown')}: {model.get('name', 'Unknown')}")
|
| 54 |
+
return models
|
| 55 |
+
else:
|
| 56 |
+
logger.error(f"❌ Error: {response.status_code}")
|
| 57 |
+
return None
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"❌ Error: {e}")
|
| 60 |
+
return None
|
| 61 |
+
|
| 62 |
+
def create_fine_tuning_job(self, model_name, training_file, validation_file=None):
|
| 63 |
+
"""Buat fine-tuning job"""
|
| 64 |
+
try:
|
| 65 |
+
payload = {
|
| 66 |
+
"model": model_name,
|
| 67 |
+
"training_file": training_file,
|
| 68 |
+
"validation_file": validation_file,
|
| 69 |
+
"hyperparameters": {
|
| 70 |
+
"n_epochs": 3,
|
| 71 |
+
"batch_size": 4,
|
| 72 |
+
"learning_rate_multiplier": 1.0
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
response = requests.post(
|
| 77 |
+
f"{self.base_url}/v1/fine_tuning/jobs",
|
| 78 |
+
headers=self.headers,
|
| 79 |
+
json=payload
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if response.status_code == 200:
|
| 83 |
+
job = response.json()
|
| 84 |
+
logger.info(f"✅ Fine-tuning job created: {job.get('id')}")
|
| 85 |
+
return job
|
| 86 |
+
else:
|
| 87 |
+
logger.error(f"❌ Error: {response.status_code} - {response.text}")
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logger.error(f"❌ Error: {e}")
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
def list_fine_tuning_jobs(self):
|
| 95 |
+
"""List semua fine-tuning jobs"""
|
| 96 |
+
try:
|
| 97 |
+
response = requests.get(
|
| 98 |
+
f"{self.base_url}/v1/fine_tuning/jobs",
|
| 99 |
+
headers=self.headers
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
if response.status_code == 200:
|
| 103 |
+
jobs = response.json()
|
| 104 |
+
logger.info("📋 Fine-tuning jobs:")
|
| 105 |
+
for job in jobs.get('data', []):
|
| 106 |
+
status = job.get('status', 'unknown')
|
| 107 |
+
model = job.get('model', 'unknown')
|
| 108 |
+
job_id = job.get('id', 'unknown')
|
| 109 |
+
logger.info(f" - {job_id}: {model} ({status})")
|
| 110 |
+
return jobs
|
| 111 |
+
else:
|
| 112 |
+
logger.error(f"❌ Error: {response.status_code}")
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"❌ Error: {e}")
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
def get_fine_tuning_job(self, job_id):
|
| 120 |
+
"""Dapatkan detail fine-tuning job"""
|
| 121 |
+
try:
|
| 122 |
+
response = requests.get(
|
| 123 |
+
f"{self.base_url}/v1/fine_tuning/jobs/{job_id}",
|
| 124 |
+
headers=self.headers
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
if response.status_code == 200:
|
| 128 |
+
job = response.json()
|
| 129 |
+
logger.info(f"📋 Job {job_id}:")
|
| 130 |
+
logger.info(f" Status: {job.get('status')}")
|
| 131 |
+
logger.info(f" Model: {job.get('model')}")
|
| 132 |
+
logger.info(f" Created: {job.get('created_at')}")
|
| 133 |
+
return job
|
| 134 |
+
else:
|
| 135 |
+
logger.error(f"❌ Error: {response.status_code}")
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logger.error(f"❌ Error: {e}")
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
def setup_novita_environment():
|
| 143 |
+
"""Setup environment untuk Novita AI"""
|
| 144 |
+
print("🚀 Setup Novita AI Environment")
|
| 145 |
+
print("=" * 40)
|
| 146 |
+
|
| 147 |
+
# Check API key
|
| 148 |
+
api_key = os.getenv('NOVITA_API_KEY')
|
| 149 |
+
if not api_key:
|
| 150 |
+
print("⚠️ NOVITA_API_KEY tidak ditemukan")
|
| 151 |
+
api_key = input("Masukkan Novita AI API key: ").strip()
|
| 152 |
+
if api_key:
|
| 153 |
+
os.environ['NOVITA_API_KEY'] = api_key
|
| 154 |
+
else:
|
| 155 |
+
print("❌ API key diperlukan untuk melanjutkan")
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
# Test connection
|
| 159 |
+
client = NovitaAIClient(api_key)
|
| 160 |
+
if not client.test_connection():
|
| 161 |
+
print("❌ Gagal koneksi ke Novita AI")
|
| 162 |
+
return None
|
| 163 |
+
|
| 164 |
+
return client
|
| 165 |
+
|
| 166 |
+
def create_sample_dataset():
|
| 167 |
+
"""Buat sample dataset untuk fine-tuning"""
|
| 168 |
+
data_dir = Path("data")
|
| 169 |
+
data_dir.mkdir(exist_ok=True)
|
| 170 |
+
|
| 171 |
+
# Sample data untuk fine-tuning
|
| 172 |
+
sample_data = [
|
| 173 |
+
{
|
| 174 |
+
"messages": [
|
| 175 |
+
{"role": "system", "content": "Anda adalah asisten AI yang membantu dengan pertanyaan dalam bahasa Indonesia."},
|
| 176 |
+
{"role": "user", "content": "Apa itu machine learning?"},
|
| 177 |
+
{"role": "assistant", "content": "Machine learning adalah cabang dari artificial intelligence yang memungkinkan komputer belajar dari data tanpa diprogram secara eksplisit."}
|
| 178 |
+
]
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"messages": [
|
| 182 |
+
{"role": "system", "content": "Anda adalah asisten AI yang membantu dengan pertanyaan dalam bahasa Indonesia."},
|
| 183 |
+
{"role": "user", "content": "Jelaskan tentang deep learning"},
|
| 184 |
+
{"role": "assistant", "content": "Deep learning adalah subset dari machine learning yang menggunakan neural network dengan banyak layer untuk memproses data kompleks."}
|
| 185 |
+
]
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"messages": [
|
| 189 |
+
{"role": "system", "content": "Anda adalah asisten AI yang membantu dengan pertanyaan dalam bahasa Indonesia."},
|
| 190 |
+
{"role": "user", "content": "Bagaimana cara kerja neural network?"},
|
| 191 |
+
{"role": "assistant", "content": "Neural network bekerja dengan menerima input, memproses melalui hidden layers, dan menghasilkan output berdasarkan weights yang telah dilatih."}
|
| 192 |
+
]
|
| 193 |
+
}
|
| 194 |
+
]
|
| 195 |
+
|
| 196 |
+
# Save sebagai JSONL
|
| 197 |
+
output_file = data_dir / "training_data.jsonl"
|
| 198 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 199 |
+
for item in sample_data:
|
| 200 |
+
json.dump(item, f, ensure_ascii=False)
|
| 201 |
+
f.write('\n')
|
| 202 |
+
|
| 203 |
+
print(f"✅ Sample dataset created: {output_file}")
|
| 204 |
+
return str(output_file)
|
| 205 |
+
|
| 206 |
+
def main():
|
| 207 |
+
print("🤖 Novita AI Setup & Fine-tuning")
|
| 208 |
+
print("=" * 50)
|
| 209 |
+
|
| 210 |
+
# Setup environment
|
| 211 |
+
client = setup_novita_environment()
|
| 212 |
+
if not client:
|
| 213 |
+
return
|
| 214 |
+
|
| 215 |
+
# Get available models
|
| 216 |
+
print("\n1️⃣ Getting available models...")
|
| 217 |
+
models = client.get_available_models()
|
| 218 |
+
|
| 219 |
+
# Create sample dataset
|
| 220 |
+
print("\n2️⃣ Creating sample dataset...")
|
| 221 |
+
training_file = create_sample_dataset()
|
| 222 |
+
|
| 223 |
+
# Show menu
|
| 224 |
+
while True:
|
| 225 |
+
print("\n📋 Menu:")
|
| 226 |
+
print("1. List fine-tuning jobs")
|
| 227 |
+
print("2. Create fine-tuning job")
|
| 228 |
+
print("3. Check job status")
|
| 229 |
+
print("4. Exit")
|
| 230 |
+
|
| 231 |
+
choice = input("\nPilihan (1-4): ").strip()
|
| 232 |
+
|
| 233 |
+
if choice == "1":
|
| 234 |
+
client.list_fine_tuning_jobs()
|
| 235 |
+
elif choice == "2":
|
| 236 |
+
if models and models.get('data'):
|
| 237 |
+
model_id = input("Masukkan model ID: ").strip()
|
| 238 |
+
job = client.create_fine_tuning_job(model_id, training_file)
|
| 239 |
+
if job:
|
| 240 |
+
print(f"✅ Job created: {job.get('id')}")
|
| 241 |
+
else:
|
| 242 |
+
print("❌ Tidak ada model tersedia")
|
| 243 |
+
elif choice == "3":
|
| 244 |
+
job_id = input("Masukkan job ID: ").strip()
|
| 245 |
+
client.get_fine_tuning_job(job_id)
|
| 246 |
+
elif choice == "4":
|
| 247 |
+
print("👋 Goodbye!")
|
| 248 |
+
break
|
| 249 |
+
else:
|
| 250 |
+
print("❌ Pilihan tidak valid")
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
main()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
|
scripts/novita_ai_setup_v2.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk setup dan menggunakan Novita AI (Updated Version)
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import requests
|
| 9 |
+
import json
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
logging.basicConfig(level=logging.INFO)
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
class NovitaAIClient:
|
| 17 |
+
def __init__(self, api_key):
|
| 18 |
+
self.api_key = api_key
|
| 19 |
+
# Use correct Novita AI endpoint
|
| 20 |
+
self.possible_endpoints = [
|
| 21 |
+
"https://api.novita.ai/openai",
|
| 22 |
+
"https://api.novita.ai",
|
| 23 |
+
"https://api.novita.com/openai"
|
| 24 |
+
]
|
| 25 |
+
self.base_url = None
|
| 26 |
+
self.headers = {
|
| 27 |
+
"Authorization": f"Bearer {api_key}",
|
| 28 |
+
"Content-Type": "application/json"
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
def find_working_endpoint(self):
|
| 32 |
+
"""Find working API endpoint"""
|
| 33 |
+
for endpoint in self.possible_endpoints:
|
| 34 |
+
try:
|
| 35 |
+
logger.info(f"🔍 Testing endpoint: {endpoint}")
|
| 36 |
+
response = requests.get(
|
| 37 |
+
f"{endpoint}/v1/models",
|
| 38 |
+
headers=self.headers,
|
| 39 |
+
timeout=10
|
| 40 |
+
)
|
| 41 |
+
if response.status_code == 200:
|
| 42 |
+
self.base_url = endpoint
|
| 43 |
+
logger.info(f"✅ Working endpoint found: {endpoint}")
|
| 44 |
+
return True
|
| 45 |
+
else:
|
| 46 |
+
logger.info(f"⚠️ Endpoint {endpoint} returned {response.status_code}")
|
| 47 |
+
except Exception as e:
|
| 48 |
+
logger.info(f"❌ Endpoint {endpoint} failed: {e}")
|
| 49 |
+
continue
|
| 50 |
+
|
| 51 |
+
return False
|
| 52 |
+
|
| 53 |
+
def test_connection(self):
|
| 54 |
+
"""Test koneksi ke Novita AI API"""
|
| 55 |
+
if not self.find_working_endpoint():
|
| 56 |
+
logger.error("❌ Tidak ada endpoint yang berfungsi")
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
# Use OpenAI-compatible paths
|
| 61 |
+
test_paths = [
|
| 62 |
+
"/models",
|
| 63 |
+
"/v1/models",
|
| 64 |
+
"/chat/completions"
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
for path in test_paths:
|
| 68 |
+
try:
|
| 69 |
+
response = requests.get(
|
| 70 |
+
f"{self.base_url}{path}",
|
| 71 |
+
headers=self.headers,
|
| 72 |
+
timeout=10
|
| 73 |
+
)
|
| 74 |
+
if response.status_code == 200:
|
| 75 |
+
logger.info(f"✅ Koneksi ke Novita AI berhasil! Endpoint: {self.base_url}{path}")
|
| 76 |
+
return True
|
| 77 |
+
elif response.status_code == 401:
|
| 78 |
+
logger.error("❌ Unauthorized - API key mungkin salah")
|
| 79 |
+
return False
|
| 80 |
+
elif response.status_code == 404:
|
| 81 |
+
logger.info(f"⚠️ Path {path} tidak ditemukan, mencoba yang lain...")
|
| 82 |
+
continue
|
| 83 |
+
else:
|
| 84 |
+
logger.info(f"⚠️ Endpoint {path} returned {response.status_code}")
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logger.info(f"⚠️ Path {path} failed: {e}")
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
logger.error("❌ Tidak ada endpoint yang berfungsi")
|
| 90 |
+
return False
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"❌ Error koneksi: {e}")
|
| 94 |
+
return False
|
| 95 |
+
|
| 96 |
+
def get_available_models(self):
|
| 97 |
+
"""Dapatkan daftar model yang tersedia"""
|
| 98 |
+
if not self.base_url:
|
| 99 |
+
logger.error("❌ Base URL belum diset")
|
| 100 |
+
return None
|
| 101 |
+
|
| 102 |
+
try:
|
| 103 |
+
# Use OpenAI-compatible model endpoints
|
| 104 |
+
model_paths = [
|
| 105 |
+
"/models",
|
| 106 |
+
"/v1/models"
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
for path in model_paths:
|
| 110 |
+
try:
|
| 111 |
+
response = requests.get(
|
| 112 |
+
f"{self.base_url}{path}",
|
| 113 |
+
headers=self.headers,
|
| 114 |
+
timeout=10
|
| 115 |
+
)
|
| 116 |
+
if response.status_code == 200:
|
| 117 |
+
models = response.json()
|
| 118 |
+
logger.info("📋 Model yang tersedia:")
|
| 119 |
+
if isinstance(models, dict) and 'data' in models:
|
| 120 |
+
for model in models['data']:
|
| 121 |
+
logger.info(f" - {model.get('id', 'Unknown')}: {model.get('name', 'Unknown')}")
|
| 122 |
+
elif isinstance(models, list):
|
| 123 |
+
for model in models:
|
| 124 |
+
logger.info(f" - {model.get('id', 'Unknown')}: {model.get('name', 'Unknown')}")
|
| 125 |
+
else:
|
| 126 |
+
logger.info(f" Response format: {type(models)}")
|
| 127 |
+
logger.info(f" Content: {models}")
|
| 128 |
+
return models
|
| 129 |
+
else:
|
| 130 |
+
logger.info(f"⚠️ Path {path} returned {response.status_code}")
|
| 131 |
+
except Exception as e:
|
| 132 |
+
logger.info(f"⚠️ Path {path} failed: {e}")
|
| 133 |
+
continue
|
| 134 |
+
|
| 135 |
+
logger.error("❌ Tidak bisa mendapatkan daftar model")
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
except Exception as e:
|
| 139 |
+
logger.error(f"❌ Error: {e}")
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
def create_fine_tuning_job(self, model_name, training_file, validation_file=None):
|
| 143 |
+
"""Buat fine-tuning job"""
|
| 144 |
+
if not self.base_url:
|
| 145 |
+
logger.error("❌ Base URL belum diset")
|
| 146 |
+
return None
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
payload = {
|
| 150 |
+
"model": model_name,
|
| 151 |
+
"training_file": training_file,
|
| 152 |
+
"validation_file": validation_file,
|
| 153 |
+
"hyperparameters": {
|
| 154 |
+
"n_epochs": 3,
|
| 155 |
+
"batch_size": 4,
|
| 156 |
+
"learning_rate_multiplier": 1.0
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
# Use OpenAI-compatible fine-tuning endpoints
|
| 161 |
+
ft_paths = [
|
| 162 |
+
"/fine_tuning/jobs",
|
| 163 |
+
"/v1/fine_tuning/jobs"
|
| 164 |
+
]
|
| 165 |
+
|
| 166 |
+
for path in ft_paths:
|
| 167 |
+
try:
|
| 168 |
+
response = requests.post(
|
| 169 |
+
f"{self.base_url}{path}",
|
| 170 |
+
headers=self.headers,
|
| 171 |
+
json=payload,
|
| 172 |
+
timeout=30
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
if response.status_code == 200:
|
| 176 |
+
job = response.json()
|
| 177 |
+
logger.info(f"✅ Fine-tuning job created: {job.get('id')}")
|
| 178 |
+
return job
|
| 179 |
+
elif response.status_code == 404:
|
| 180 |
+
logger.info(f"⚠️ Path {path} tidak ditemukan, mencoba yang lain...")
|
| 181 |
+
continue
|
| 182 |
+
else:
|
| 183 |
+
logger.error(f"❌ Error: {response.status_code} - {response.text}")
|
| 184 |
+
continue
|
| 185 |
+
|
| 186 |
+
except Exception as e:
|
| 187 |
+
logger.info(f"⚠️ Path {path} failed: {e}")
|
| 188 |
+
continue
|
| 189 |
+
|
| 190 |
+
logger.error("❌ Tidak bisa membuat fine-tuning job")
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
except Exception as e:
|
| 194 |
+
logger.error(f"❌ Error: {e}")
|
| 195 |
+
return None
|
| 196 |
+
|
| 197 |
+
def list_fine_tuning_jobs(self):
|
| 198 |
+
"""List semua fine-tuning jobs"""
|
| 199 |
+
if not self.base_url:
|
| 200 |
+
logger.error("❌ Base URL belum diset")
|
| 201 |
+
return None
|
| 202 |
+
|
| 203 |
+
try:
|
| 204 |
+
# Use OpenAI-compatible job listing endpoints
|
| 205 |
+
job_paths = [
|
| 206 |
+
"/fine_tuning/jobs",
|
| 207 |
+
"/v1/fine_tuning/jobs"
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
for path in job_paths:
|
| 211 |
+
try:
|
| 212 |
+
response = requests.get(
|
| 213 |
+
f"{self.base_url}{path}",
|
| 214 |
+
headers=self.headers,
|
| 215 |
+
timeout=10
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
if response.status_code == 200:
|
| 219 |
+
jobs = response.json()
|
| 220 |
+
logger.info("📋 Fine-tuning jobs:")
|
| 221 |
+
if isinstance(jobs, dict) and 'data' in jobs:
|
| 222 |
+
for job in jobs['data']:
|
| 223 |
+
status = job.get('status', 'unknown')
|
| 224 |
+
model = job.get('model', 'unknown')
|
| 225 |
+
job_id = job.get('id', 'unknown')
|
| 226 |
+
logger.info(f" - {job_id}: {model} ({status})")
|
| 227 |
+
elif isinstance(jobs, list):
|
| 228 |
+
for job in jobs:
|
| 229 |
+
status = job.get('status', 'unknown')
|
| 230 |
+
model = job.get('model', 'unknown')
|
| 231 |
+
job_id = job.get('id', 'unknown')
|
| 232 |
+
logger.info(f" - {job_id}: {model} ({status})")
|
| 233 |
+
else:
|
| 234 |
+
logger.info(f" Response format: {type(jobs)}")
|
| 235 |
+
logger.info(f" Content: {jobs}")
|
| 236 |
+
return jobs
|
| 237 |
+
elif response.status_code == 404:
|
| 238 |
+
logger.info(f"⚠️ Path {path} tidak ditemukan, mencoba yang lain...")
|
| 239 |
+
continue
|
| 240 |
+
else:
|
| 241 |
+
logger.error(f"❌ Error: {response.status_code}")
|
| 242 |
+
continue
|
| 243 |
+
|
| 244 |
+
except Exception as e:
|
| 245 |
+
logger.info(f"⚠️ Path {path} failed: {e}")
|
| 246 |
+
continue
|
| 247 |
+
|
| 248 |
+
logger.error("❌ Tidak bisa mendapatkan daftar jobs")
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
except Exception as e:
|
| 252 |
+
logger.error(f"❌ Error: {e}")
|
| 253 |
+
return None
|
| 254 |
+
|
| 255 |
+
def setup_novita_environment():
|
| 256 |
+
"""Setup environment untuk Novita AI"""
|
| 257 |
+
print("🚀 Setup Novita AI Environment")
|
| 258 |
+
print("=" * 40)
|
| 259 |
+
|
| 260 |
+
# Check API key
|
| 261 |
+
api_key = os.getenv('NOVITA_API_KEY')
|
| 262 |
+
if not api_key:
|
| 263 |
+
print("⚠️ NOVITA_API_KEY tidak ditemukan")
|
| 264 |
+
api_key = input("Masukkan Novita AI API key: ").strip()
|
| 265 |
+
if api_key:
|
| 266 |
+
os.environ['NOVITA_API_KEY'] = api_key
|
| 267 |
+
else:
|
| 268 |
+
print("❌ API key diperlukan untuk melanjutkan")
|
| 269 |
+
return None
|
| 270 |
+
|
| 271 |
+
# Test connection
|
| 272 |
+
client = NovitaAIClient(api_key)
|
| 273 |
+
if not client.test_connection():
|
| 274 |
+
print("❌ Gagal koneksi ke Novita AI")
|
| 275 |
+
print("💡 Tips:")
|
| 276 |
+
print("- Pastikan API key benar")
|
| 277 |
+
print("- Cek koneksi internet")
|
| 278 |
+
print("- Cek dokumentasi Novita AI untuk endpoint yang benar")
|
| 279 |
+
return None
|
| 280 |
+
|
| 281 |
+
return client
|
| 282 |
+
|
| 283 |
+
def create_sample_dataset():
|
| 284 |
+
"""Gunakan dataset yang sudah ada atau buat yang baru jika tidak ada"""
|
| 285 |
+
data_dir = Path("data")
|
| 286 |
+
data_dir.mkdir(exist_ok=True)
|
| 287 |
+
|
| 288 |
+
# Cek apakah dataset sudah ada
|
| 289 |
+
existing_dataset = data_dir / "lora_dataset_20250829_113330.jsonl"
|
| 290 |
+
if existing_dataset.exists():
|
| 291 |
+
print(f"✅ Dataset sudah ada: {existing_dataset}")
|
| 292 |
+
print(f"📊 File size: {existing_dataset.stat().st_size / 1024:.2f} KB")
|
| 293 |
+
return str(existing_dataset)
|
| 294 |
+
|
| 295 |
+
# Jika tidak ada, buat sample dataset
|
| 296 |
+
print("⚠️ Dataset tidak ditemukan, membuat sample dataset...")
|
| 297 |
+
sample_data = [
|
| 298 |
+
{
|
| 299 |
+
"messages": [
|
| 300 |
+
{"role": "system", "content": "Anda adalah asisten AI yang membantu dengan pertanyaan dalam bahasa Indonesia."},
|
| 301 |
+
{"role": "user", "content": "Apa itu machine learning?"},
|
| 302 |
+
{"role": "assistant", "content": "Machine learning adalah cabang dari artificial intelligence yang memungkinkan komputer belajar dari data tanpa diprogram secara eksplisit."}
|
| 303 |
+
]
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"messages": [
|
| 307 |
+
{"role": "system", "content": "Anda adalah asisten AI yang membantu dengan pertanyaan dalam bahasa Indonesia."},
|
| 308 |
+
{"role": "user", "content": "Jelaskan tentang deep learning"},
|
| 309 |
+
{"role": "assistant", "content": "Deep learning adalah subset dari machine learning yang menggunakan neural network dengan banyak layer untuk memproses data kompleks."}
|
| 310 |
+
]
|
| 311 |
+
}
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
# Save sebagai JSONL
|
| 315 |
+
output_file = data_dir / "training_data.jsonl"
|
| 316 |
+
with open(output_file, 'w', encoding='utf-8') as f:
|
| 317 |
+
for item in sample_data:
|
| 318 |
+
json.dump(item, f, ensure_ascii=False)
|
| 319 |
+
f.write('\n')
|
| 320 |
+
|
| 321 |
+
print(f"✅ Sample dataset created: {output_file}")
|
| 322 |
+
return str(output_file)
|
| 323 |
+
|
| 324 |
+
def main():
|
| 325 |
+
print("🤖 Novita AI Setup & Fine-tuning (Updated)")
|
| 326 |
+
print("=" * 50)
|
| 327 |
+
|
| 328 |
+
# Setup environment
|
| 329 |
+
client = setup_novita_environment()
|
| 330 |
+
if not client:
|
| 331 |
+
return
|
| 332 |
+
|
| 333 |
+
# Get available models
|
| 334 |
+
print("\n1️⃣ Getting available models...")
|
| 335 |
+
models = client.get_available_models()
|
| 336 |
+
|
| 337 |
+
# Create sample dataset
|
| 338 |
+
print("\n2️⃣ Creating sample dataset...")
|
| 339 |
+
training_file = create_sample_dataset()
|
| 340 |
+
|
| 341 |
+
# Show menu
|
| 342 |
+
while True:
|
| 343 |
+
print("\n📋 Menu:")
|
| 344 |
+
print("1. List fine-tuning jobs")
|
| 345 |
+
print("2. Create fine-tuning job")
|
| 346 |
+
print("3. Check job status")
|
| 347 |
+
print("4. Test API endpoints")
|
| 348 |
+
print("5. Exit")
|
| 349 |
+
|
| 350 |
+
choice = input("\nPilihan (1-5): ").strip()
|
| 351 |
+
|
| 352 |
+
if choice == "1":
|
| 353 |
+
client.list_fine_tuning_jobs()
|
| 354 |
+
elif choice == "2":
|
| 355 |
+
if models:
|
| 356 |
+
model_id = input("Masukkan model ID: ").strip()
|
| 357 |
+
job = client.create_fine_tuning_job(model_id, training_file)
|
| 358 |
+
if job:
|
| 359 |
+
print(f"✅ Job created: {job.get('id')}")
|
| 360 |
+
else:
|
| 361 |
+
print("❌ Tidak ada model tersedia")
|
| 362 |
+
elif choice == "3":
|
| 363 |
+
job_id = input("Masukkan job ID: ").strip()
|
| 364 |
+
# This would need to be implemented based on actual API
|
| 365 |
+
print("⚠️ Check job status belum diimplementasikan")
|
| 366 |
+
elif choice == "4":
|
| 367 |
+
print("🔍 Testing API endpoints...")
|
| 368 |
+
client.test_connection()
|
| 369 |
+
elif choice == "5":
|
| 370 |
+
print("👋 Goodbye!")
|
| 371 |
+
break
|
| 372 |
+
else:
|
| 373 |
+
print("❌ Pilihan tidak valid")
|
| 374 |
+
|
| 375 |
+
if __name__ == "__main__":
|
| 376 |
+
main()
|
scripts/run_novita_finetuning.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script sederhana untuk menjalankan fine-tuning Novita AI
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# Import NovitaAIClient dari script yang sudah ada
|
| 11 |
+
sys.path.append('scripts')
|
| 12 |
+
from novita_ai_setup_v2 import NovitaAIClient, create_sample_dataset
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
print("🚀 Novita AI Fine-tuning - Auto Run")
|
| 16 |
+
print("=" * 50)
|
| 17 |
+
|
| 18 |
+
# Check environment variables
|
| 19 |
+
api_key = os.getenv('NOVITA_API_KEY')
|
| 20 |
+
if not api_key:
|
| 21 |
+
print("❌ NOVITA_API_KEY tidak ditemukan")
|
| 22 |
+
print("Silakan set: export NOVITA_API_KEY='your_key'")
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
+
base_url = os.getenv('NOVITA_BASE_URL', 'https://api.novita.ai/openai')
|
| 26 |
+
print(f"🔑 API Key: {api_key[:10]}...{api_key[-10:]}")
|
| 27 |
+
print(f"🌐 Base URL: {base_url}")
|
| 28 |
+
|
| 29 |
+
# Create client
|
| 30 |
+
client = NovitaAIClient(api_key)
|
| 31 |
+
client.base_url = base_url
|
| 32 |
+
|
| 33 |
+
# Test connection
|
| 34 |
+
print("\n1️⃣ Testing connection...")
|
| 35 |
+
if not client.test_connection():
|
| 36 |
+
print("❌ Koneksi gagal")
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
# Get available models
|
| 40 |
+
print("\n2️⃣ Getting available models...")
|
| 41 |
+
models = client.get_available_models()
|
| 42 |
+
|
| 43 |
+
if not models:
|
| 44 |
+
print("❌ Tidak bisa mendapatkan daftar model")
|
| 45 |
+
return
|
| 46 |
+
|
| 47 |
+
# Select model automatically (Llama 3.2 1B Instruct if available)
|
| 48 |
+
selected_model = None
|
| 49 |
+
preferred_models = [
|
| 50 |
+
"meta-llama/llama-3.2-1b-instruct",
|
| 51 |
+
"meta-llama/llama-3.2-3b-instruct",
|
| 52 |
+
"qwen/qwen3-4b-fp8",
|
| 53 |
+
"qwen/qwen3-8b-fp8"
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
print("\n🎯 Selecting model...")
|
| 57 |
+
for preferred in preferred_models:
|
| 58 |
+
if isinstance(models, dict) and 'data' in models:
|
| 59 |
+
for model in models['data']:
|
| 60 |
+
if model.get('id') == preferred:
|
| 61 |
+
selected_model = preferred
|
| 62 |
+
print(f"✅ Selected: {preferred}")
|
| 63 |
+
break
|
| 64 |
+
elif isinstance(models, list):
|
| 65 |
+
for model in models:
|
| 66 |
+
if model.get('id') == preferred:
|
| 67 |
+
selected_model = preferred
|
| 68 |
+
print(f"✅ Selected: {preferred}")
|
| 69 |
+
break
|
| 70 |
+
|
| 71 |
+
if selected_model:
|
| 72 |
+
break
|
| 73 |
+
|
| 74 |
+
if not selected_model:
|
| 75 |
+
# Fallback to first available model
|
| 76 |
+
if isinstance(models, dict) and 'data' in models and models['data']:
|
| 77 |
+
selected_model = models['data'][0].get('id')
|
| 78 |
+
elif isinstance(models, list) and models:
|
| 79 |
+
selected_model = models[0].get('id')
|
| 80 |
+
|
| 81 |
+
if selected_model:
|
| 82 |
+
print(f"⚠️ Fallback to: {selected_model}")
|
| 83 |
+
else:
|
| 84 |
+
print("❌ Tidak ada model yang tersedia")
|
| 85 |
+
return
|
| 86 |
+
|
| 87 |
+
# Create dataset
|
| 88 |
+
print("\n3️⃣ Preparing dataset...")
|
| 89 |
+
training_file = create_sample_dataset()
|
| 90 |
+
|
| 91 |
+
# Create fine-tuning job
|
| 92 |
+
print(f"\n4️⃣ Creating fine-tuning job...")
|
| 93 |
+
print(f" Model: {selected_model}")
|
| 94 |
+
print(f" Training file: {training_file}")
|
| 95 |
+
|
| 96 |
+
job = client.create_fine_tuning_job(selected_model, training_file)
|
| 97 |
+
|
| 98 |
+
if job:
|
| 99 |
+
print(f"\n✅ Fine-tuning job created successfully!")
|
| 100 |
+
print(f" Job ID: {job.get('id')}")
|
| 101 |
+
print(f" Status: {job.get('status', 'unknown')}")
|
| 102 |
+
print(f" Model: {job.get('model', 'unknown')}")
|
| 103 |
+
|
| 104 |
+
print(f"\n📋 Next steps:")
|
| 105 |
+
print(f"1. Monitor job status")
|
| 106 |
+
print(f"2. Check logs for progress")
|
| 107 |
+
print(f"3. Download fine-tuned model when complete")
|
| 108 |
+
|
| 109 |
+
else:
|
| 110 |
+
print("\n❌ Failed to create fine-tuning job")
|
| 111 |
+
print("💡 Check the error messages above")
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
main()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
|
scripts/setup_textilindo_training.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Setup script untuk Textilindo AI Assistant training
|
| 4 |
+
Download model dan prepare environment
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import yaml
|
| 10 |
+
import torch
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(level=logging.INFO)
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
def load_config(config_path):
|
| 19 |
+
"""Load configuration from YAML file"""
|
| 20 |
+
try:
|
| 21 |
+
with open(config_path, 'r') as f:
|
| 22 |
+
config = yaml.safe_load(f)
|
| 23 |
+
return config
|
| 24 |
+
except Exception as e:
|
| 25 |
+
logger.error(f"Error loading config: {e}")
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
def download_model(config):
|
| 29 |
+
"""Download base model"""
|
| 30 |
+
model_name = config['model_name']
|
| 31 |
+
model_path = config['model_path']
|
| 32 |
+
|
| 33 |
+
logger.info(f"Downloading model: {model_name}")
|
| 34 |
+
logger.info(f"Target path: {model_path}")
|
| 35 |
+
|
| 36 |
+
# Create models directory
|
| 37 |
+
Path(model_path).mkdir(parents=True, exist_ok=True)
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
# Download tokenizer
|
| 41 |
+
logger.info("Downloading tokenizer...")
|
| 42 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 43 |
+
model_name,
|
| 44 |
+
trust_remote_code=True,
|
| 45 |
+
cache_dir=model_path
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Download model with memory optimization
|
| 49 |
+
logger.info("Downloading model...")
|
| 50 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 51 |
+
model_name,
|
| 52 |
+
torch_dtype=torch.float16,
|
| 53 |
+
trust_remote_code=True,
|
| 54 |
+
cache_dir=model_path,
|
| 55 |
+
low_cpu_mem_usage=True,
|
| 56 |
+
load_in_8bit=True # Use 8-bit quantization for memory efficiency
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# Save to local path
|
| 60 |
+
logger.info(f"Saving model to: {model_path}")
|
| 61 |
+
tokenizer.save_pretrained(model_path)
|
| 62 |
+
model.save_pretrained(model_path)
|
| 63 |
+
|
| 64 |
+
logger.info("✅ Model downloaded successfully!")
|
| 65 |
+
return True
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.error(f"Error downloading model: {e}")
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
def check_requirements():
|
| 72 |
+
"""Check if all requirements are met"""
|
| 73 |
+
print("🔍 Checking requirements...")
|
| 74 |
+
|
| 75 |
+
# Check Python version
|
| 76 |
+
if sys.version_info < (3, 8):
|
| 77 |
+
print("❌ Python 3.8+ required")
|
| 78 |
+
return False
|
| 79 |
+
|
| 80 |
+
# Check PyTorch
|
| 81 |
+
try:
|
| 82 |
+
import torch
|
| 83 |
+
print(f"✅ PyTorch {torch.__version__}")
|
| 84 |
+
except ImportError:
|
| 85 |
+
print("❌ PyTorch not installed")
|
| 86 |
+
return False
|
| 87 |
+
|
| 88 |
+
# Check CUDA availability
|
| 89 |
+
if torch.cuda.is_available():
|
| 90 |
+
print(f"✅ CUDA available: {torch.cuda.get_device_name(0)}")
|
| 91 |
+
print(f" GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")
|
| 92 |
+
else:
|
| 93 |
+
print("⚠️ CUDA not available - training will be slower on CPU")
|
| 94 |
+
|
| 95 |
+
# Check required packages
|
| 96 |
+
required_packages = [
|
| 97 |
+
'transformers',
|
| 98 |
+
'peft',
|
| 99 |
+
'datasets',
|
| 100 |
+
'accelerate',
|
| 101 |
+
'bitsandbytes'
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
missing_packages = []
|
| 105 |
+
for package in required_packages:
|
| 106 |
+
try:
|
| 107 |
+
__import__(package)
|
| 108 |
+
print(f"✅ {package}")
|
| 109 |
+
except ImportError:
|
| 110 |
+
missing_packages.append(package)
|
| 111 |
+
print(f"❌ {package}")
|
| 112 |
+
|
| 113 |
+
if missing_packages:
|
| 114 |
+
print(f"\n❌ Missing packages: {', '.join(missing_packages)}")
|
| 115 |
+
print("Install with: pip install " + " ".join(missing_packages))
|
| 116 |
+
return False
|
| 117 |
+
|
| 118 |
+
return True
|
| 119 |
+
|
| 120 |
+
def main():
|
| 121 |
+
print("🚀 Textilindo AI Assistant - Setup")
|
| 122 |
+
print("=" * 50)
|
| 123 |
+
|
| 124 |
+
# Check requirements
|
| 125 |
+
if not check_requirements():
|
| 126 |
+
print("\n❌ Requirements not met. Please install missing packages.")
|
| 127 |
+
sys.exit(1)
|
| 128 |
+
|
| 129 |
+
# Load configuration
|
| 130 |
+
config_path = "configs/training_config.yaml"
|
| 131 |
+
if not os.path.exists(config_path):
|
| 132 |
+
print(f"❌ Config file tidak ditemukan: {config_path}")
|
| 133 |
+
sys.exit(1)
|
| 134 |
+
|
| 135 |
+
config = load_config(config_path)
|
| 136 |
+
if not config:
|
| 137 |
+
sys.exit(1)
|
| 138 |
+
|
| 139 |
+
# Check if model already exists
|
| 140 |
+
model_path = config['model_path']
|
| 141 |
+
if os.path.exists(model_path) and os.path.exists(os.path.join(model_path, "config.json")):
|
| 142 |
+
print(f"✅ Model already exists: {model_path}")
|
| 143 |
+
print("Skipping download...")
|
| 144 |
+
else:
|
| 145 |
+
# Download model
|
| 146 |
+
print("1️⃣ Downloading base model...")
|
| 147 |
+
if not download_model(config):
|
| 148 |
+
print("❌ Failed to download model")
|
| 149 |
+
sys.exit(1)
|
| 150 |
+
|
| 151 |
+
# Check dataset
|
| 152 |
+
dataset_path = config['dataset_path']
|
| 153 |
+
if not os.path.exists(dataset_path):
|
| 154 |
+
print(f"❌ Dataset tidak ditemukan: {dataset_path}")
|
| 155 |
+
print("Please ensure your dataset is in the correct location")
|
| 156 |
+
sys.exit(1)
|
| 157 |
+
else:
|
| 158 |
+
print(f"✅ Dataset found: {dataset_path}")
|
| 159 |
+
|
| 160 |
+
# Check system prompt
|
| 161 |
+
system_prompt_path = "configs/system_prompt.md"
|
| 162 |
+
if not os.path.exists(system_prompt_path):
|
| 163 |
+
print(f"❌ System prompt tidak ditemukan: {system_prompt_path}")
|
| 164 |
+
sys.exit(1)
|
| 165 |
+
else:
|
| 166 |
+
print(f"✅ System prompt found: {system_prompt_path}")
|
| 167 |
+
|
| 168 |
+
print("\n✅ Setup completed successfully!")
|
| 169 |
+
print("\n📋 Next steps:")
|
| 170 |
+
print("1. Run training: python scripts/train_textilindo_ai.py")
|
| 171 |
+
print("2. Test model: python scripts/test_textilindo_ai.py")
|
| 172 |
+
print("3. Test with LoRA: python scripts/test_textilindo_ai.py --lora_path models/textilindo-ai-lora-YYYYMMDD_HHMMSS")
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
main()
|
scripts/test_model.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk testing model yang sudah di-fine-tune
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import yaml
|
| 9 |
+
import torch
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 12 |
+
from peft import PeftModel
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(level=logging.INFO)
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
def load_finetuned_model(model_path, lora_weights_path):
|
| 19 |
+
"""Load fine-tuned model with LoRA weights"""
|
| 20 |
+
logger.info(f"Loading base model from: {model_path}")
|
| 21 |
+
|
| 22 |
+
# Load base model
|
| 23 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 24 |
+
model_path,
|
| 25 |
+
torch_dtype=torch.float16,
|
| 26 |
+
device_map="auto",
|
| 27 |
+
trust_remote_code=True
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# Load LoRA weights
|
| 31 |
+
logger.info(f"Loading LoRA weights from: {lora_weights_path}")
|
| 32 |
+
model = PeftModel.from_pretrained(model, lora_weights_path)
|
| 33 |
+
|
| 34 |
+
# Load tokenizer
|
| 35 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 36 |
+
model_path,
|
| 37 |
+
trust_remote_code=True
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
if tokenizer.pad_token is None:
|
| 41 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 42 |
+
|
| 43 |
+
return model, tokenizer
|
| 44 |
+
|
| 45 |
+
def generate_response(model, tokenizer, prompt, max_length=512):
|
| 46 |
+
"""Generate response from the model"""
|
| 47 |
+
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
| 48 |
+
|
| 49 |
+
with torch.no_grad():
|
| 50 |
+
outputs = model.generate(
|
| 51 |
+
**inputs,
|
| 52 |
+
max_length=max_length,
|
| 53 |
+
temperature=0.7,
|
| 54 |
+
top_p=0.9,
|
| 55 |
+
top_k=40,
|
| 56 |
+
repetition_penalty=1.1,
|
| 57 |
+
do_sample=True,
|
| 58 |
+
pad_token_id=tokenizer.eos_token_id
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 62 |
+
return response
|
| 63 |
+
|
| 64 |
+
def interactive_test(model, tokenizer):
|
| 65 |
+
"""Interactive testing mode"""
|
| 66 |
+
print("🤖 Interactive Testing Mode")
|
| 67 |
+
print("Type 'quit' to exit")
|
| 68 |
+
print("-" * 50)
|
| 69 |
+
|
| 70 |
+
while True:
|
| 71 |
+
try:
|
| 72 |
+
user_input = input("\n👤 You: ").strip()
|
| 73 |
+
|
| 74 |
+
if user_input.lower() in ['quit', 'exit', 'q']:
|
| 75 |
+
print("👋 Goodbye!")
|
| 76 |
+
break
|
| 77 |
+
|
| 78 |
+
if not user_input:
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
print("\n🤖 Assistant: ", end="")
|
| 82 |
+
response = generate_response(model, tokenizer, user_input)
|
| 83 |
+
|
| 84 |
+
# Extract only the generated part (remove input)
|
| 85 |
+
if user_input in response:
|
| 86 |
+
generated_part = response.split(user_input)[-1].strip()
|
| 87 |
+
print(generated_part)
|
| 88 |
+
else:
|
| 89 |
+
print(response)
|
| 90 |
+
|
| 91 |
+
except KeyboardInterrupt:
|
| 92 |
+
print("\n👋 Goodbye!")
|
| 93 |
+
break
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.error(f"Error generating response: {e}")
|
| 96 |
+
print(f"❌ Error: {e}")
|
| 97 |
+
|
| 98 |
+
def batch_test(model, tokenizer, test_cases):
|
| 99 |
+
"""Batch testing with predefined test cases"""
|
| 100 |
+
print("🧪 Batch Testing Mode")
|
| 101 |
+
print("=" * 50)
|
| 102 |
+
|
| 103 |
+
for i, test_case in enumerate(test_cases, 1):
|
| 104 |
+
print(f"\n📝 Test Case {i}: {test_case['prompt']}")
|
| 105 |
+
print("-" * 40)
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
response = generate_response(model, tokenizer, test_case['prompt'])
|
| 109 |
+
print(f"🤖 Response: {response}")
|
| 110 |
+
|
| 111 |
+
if 'expected' in test_case:
|
| 112 |
+
print(f"🎯 Expected: {test_case['expected']}")
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"Error in test case {i}: {e}")
|
| 116 |
+
print(f"❌ Error: {e}")
|
| 117 |
+
|
| 118 |
+
def main():
|
| 119 |
+
print("🧪 Model Testing - Fine-tuned Llama 3.1 8B")
|
| 120 |
+
print("=" * 50)
|
| 121 |
+
|
| 122 |
+
# Check if model exists
|
| 123 |
+
base_model_path = "models/llama-3.1-8b-instruct"
|
| 124 |
+
lora_weights_path = "models/finetuned-llama-lora"
|
| 125 |
+
|
| 126 |
+
if not os.path.exists(base_model_path):
|
| 127 |
+
print(f"❌ Base model tidak ditemukan: {base_model_path}")
|
| 128 |
+
print("Jalankan download_model.py terlebih dahulu")
|
| 129 |
+
sys.exit(1)
|
| 130 |
+
|
| 131 |
+
if not os.path.exists(lora_weights_path):
|
| 132 |
+
print(f"⚠️ LoRA weights tidak ditemukan: {lora_weights_path}")
|
| 133 |
+
print("Model akan menggunakan base model tanpa fine-tuning")
|
| 134 |
+
lora_weights_path = None
|
| 135 |
+
|
| 136 |
+
try:
|
| 137 |
+
# Load model
|
| 138 |
+
print("1️⃣ Loading model...")
|
| 139 |
+
if lora_weights_path:
|
| 140 |
+
model, tokenizer = load_finetuned_model(base_model_path, lora_weights_path)
|
| 141 |
+
else:
|
| 142 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 143 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 144 |
+
base_model_path,
|
| 145 |
+
torch_dtype=torch.float16,
|
| 146 |
+
device_map="auto",
|
| 147 |
+
trust_remote_code=True
|
| 148 |
+
)
|
| 149 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 150 |
+
base_model_path,
|
| 151 |
+
trust_remote_code=True
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
print("✅ Model loaded successfully!")
|
| 155 |
+
|
| 156 |
+
# Test cases
|
| 157 |
+
test_cases = [
|
| 158 |
+
{
|
| 159 |
+
"prompt": "Apa itu machine learning?",
|
| 160 |
+
"expected": "Penjelasan tentang machine learning"
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"prompt": "Jelaskan tentang deep learning dalam bahasa Indonesia",
|
| 164 |
+
"expected": "Penjelasan tentang deep learning"
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"prompt": "Buat puisi tentang teknologi",
|
| 168 |
+
"expected": "Puisi tentang teknologi"
|
| 169 |
+
}
|
| 170 |
+
]
|
| 171 |
+
|
| 172 |
+
# Choose testing mode
|
| 173 |
+
print("\n2️⃣ Pilih mode testing:")
|
| 174 |
+
print("1. Interactive mode (chat)")
|
| 175 |
+
print("2. Batch testing")
|
| 176 |
+
print("3. Custom prompt")
|
| 177 |
+
|
| 178 |
+
choice = input("\nPilihan (1-3): ").strip()
|
| 179 |
+
|
| 180 |
+
if choice == "1":
|
| 181 |
+
interactive_test(model, tokenizer)
|
| 182 |
+
elif choice == "2":
|
| 183 |
+
batch_test(model, tokenizer, test_cases)
|
| 184 |
+
elif choice == "3":
|
| 185 |
+
custom_prompt = input("Masukkan prompt custom: ").strip()
|
| 186 |
+
if custom_prompt:
|
| 187 |
+
response = generate_response(model, tokenizer, custom_prompt)
|
| 188 |
+
print(f"\n🤖 Response: {response}")
|
| 189 |
+
else:
|
| 190 |
+
print("❌ Pilihan tidak valid")
|
| 191 |
+
|
| 192 |
+
except Exception as e:
|
| 193 |
+
logger.error(f"Error: {e}")
|
| 194 |
+
print(f"❌ Error loading model: {e}")
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
main()
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
|
scripts/test_novita_connection.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Simple script untuk test koneksi Novita AI
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import requests
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
def test_novita_connection():
|
| 11 |
+
"""Test koneksi ke Novita AI dengan berbagai cara"""
|
| 12 |
+
|
| 13 |
+
api_key = os.getenv('NOVITA_API_KEY')
|
| 14 |
+
if not api_key:
|
| 15 |
+
print("❌ NOVITA_API_KEY tidak ditemukan")
|
| 16 |
+
return
|
| 17 |
+
|
| 18 |
+
print(f"🔑 API Key: {api_key[:10]}...{api_key[-10:]}")
|
| 19 |
+
print("🔍 Testing koneksi ke Novita AI...")
|
| 20 |
+
|
| 21 |
+
# Test different possible endpoints
|
| 22 |
+
endpoints_to_test = [
|
| 23 |
+
"https://api.novita.ai",
|
| 24 |
+
"https://api.novita.com",
|
| 25 |
+
"https://novita.ai/api",
|
| 26 |
+
"https://novita.com/api",
|
| 27 |
+
"https://api.novita.ai/v1",
|
| 28 |
+
"https://api.novita.com/v1",
|
| 29 |
+
"https://novita.ai/api/v1",
|
| 30 |
+
"https://novita.com/api/v1"
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
headers = {
|
| 34 |
+
"Authorization": f"Bearer {api_key}",
|
| 35 |
+
"Content-Type": "application/json"
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
working_endpoints = []
|
| 39 |
+
|
| 40 |
+
for endpoint in endpoints_to_test:
|
| 41 |
+
print(f"\n🔍 Testing: {endpoint}")
|
| 42 |
+
|
| 43 |
+
# Test basic connectivity
|
| 44 |
+
try:
|
| 45 |
+
# Test GET request
|
| 46 |
+
response = requests.get(f"{endpoint}/models", headers=headers, timeout=10)
|
| 47 |
+
print(f" GET /models: {response.status_code}")
|
| 48 |
+
|
| 49 |
+
if response.status_code == 200:
|
| 50 |
+
print(f" ✅ Success! Response: {response.text[:200]}...")
|
| 51 |
+
working_endpoints.append(endpoint)
|
| 52 |
+
elif response.status_code == 401:
|
| 53 |
+
print(f" ⚠️ Unauthorized - API key mungkin salah")
|
| 54 |
+
elif response.status_code == 404:
|
| 55 |
+
print(f" ⚠️ Not Found - Endpoint tidak ada")
|
| 56 |
+
else:
|
| 57 |
+
print(f" ⚠️ Status: {response.status_code}")
|
| 58 |
+
|
| 59 |
+
except requests.exceptions.ConnectionError as e:
|
| 60 |
+
print(f" ❌ Connection Error: {e}")
|
| 61 |
+
except requests.exceptions.Timeout as e:
|
| 62 |
+
print(f" ⏰ Timeout: {e}")
|
| 63 |
+
except Exception as e:
|
| 64 |
+
print(f" ❌ Error: {e}")
|
| 65 |
+
|
| 66 |
+
# Test POST request
|
| 67 |
+
try:
|
| 68 |
+
test_data = {"test": "connection"}
|
| 69 |
+
response = requests.post(f"{endpoint}/test", headers=headers, json=test_data, timeout=10)
|
| 70 |
+
print(f" POST /test: {response.status_code}")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f" ❌ POST Error: {e}")
|
| 73 |
+
|
| 74 |
+
print(f"\n📊 Summary:")
|
| 75 |
+
if working_endpoints:
|
| 76 |
+
print(f"✅ Working endpoints: {len(working_endpoints)}")
|
| 77 |
+
for endpoint in working_endpoints:
|
| 78 |
+
print(f" - {endpoint}")
|
| 79 |
+
else:
|
| 80 |
+
print("❌ No working endpoints found")
|
| 81 |
+
print("\n💡 Suggestions:")
|
| 82 |
+
print("1. Check if the API key is correct")
|
| 83 |
+
print("2. Check Novita AI documentation for correct endpoints")
|
| 84 |
+
print("3. Try using a different API key")
|
| 85 |
+
print("4. Check if there are any IP restrictions")
|
| 86 |
+
|
| 87 |
+
return working_endpoints
|
| 88 |
+
|
| 89 |
+
def test_openai_compatible():
|
| 90 |
+
"""Test if Novita AI is OpenAI compatible"""
|
| 91 |
+
print("\n🤖 Testing OpenAI compatibility...")
|
| 92 |
+
|
| 93 |
+
api_key = os.getenv('NOVITA_API_KEY')
|
| 94 |
+
if not api_key:
|
| 95 |
+
print("❌ NOVITA_API_KEY tidak ditemukan")
|
| 96 |
+
return
|
| 97 |
+
|
| 98 |
+
# Try OpenAI-compatible endpoints
|
| 99 |
+
openai_endpoints = [
|
| 100 |
+
"https://api.novita.ai/v1",
|
| 101 |
+
"https://api.novita.com/v1",
|
| 102 |
+
"https://novita.ai/api/v1",
|
| 103 |
+
"https://novita.com/api/v1"
|
| 104 |
+
]
|
| 105 |
+
|
| 106 |
+
headers = {
|
| 107 |
+
"Authorization": f"Bearer {api_key}",
|
| 108 |
+
"Content-Type": "application/json"
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
for endpoint in openai_endpoints:
|
| 112 |
+
print(f"\n🔍 Testing OpenAI endpoint: {endpoint}")
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
# Test models endpoint
|
| 116 |
+
response = requests.get(f"{endpoint}/models", headers=headers, timeout=10)
|
| 117 |
+
print(f" GET /models: {response.status_code}")
|
| 118 |
+
|
| 119 |
+
if response.status_code == 200:
|
| 120 |
+
print(f" ✅ Success!")
|
| 121 |
+
try:
|
| 122 |
+
models = response.json()
|
| 123 |
+
print(f" 📋 Models: {json.dumps(models, indent=2)[:300]}...")
|
| 124 |
+
except:
|
| 125 |
+
print(f" 📋 Response: {response.text[:200]}...")
|
| 126 |
+
elif response.status_code == 401:
|
| 127 |
+
print(f" ⚠️ Unauthorized")
|
| 128 |
+
elif response.status_code == 404:
|
| 129 |
+
print(f" ⚠️ Not Found")
|
| 130 |
+
else:
|
| 131 |
+
print(f" ⚠️ Status: {response.status_code}")
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(f" ❌ Error: {e}")
|
| 135 |
+
|
| 136 |
+
def main():
|
| 137 |
+
print("🔍 Novita AI Connection Tester")
|
| 138 |
+
print("=" * 40)
|
| 139 |
+
|
| 140 |
+
# Test basic connection
|
| 141 |
+
working_endpoints = test_novita_connection()
|
| 142 |
+
|
| 143 |
+
# Test OpenAI compatibility
|
| 144 |
+
test_openai_compatible()
|
| 145 |
+
|
| 146 |
+
print(f"\n🎯 Next Steps:")
|
| 147 |
+
if working_endpoints:
|
| 148 |
+
print("✅ Koneksi berhasil! Anda bisa melanjutkan dengan fine-tuning")
|
| 149 |
+
print("💡 Gunakan endpoint yang berfungsi untuk setup selanjutnya")
|
| 150 |
+
else:
|
| 151 |
+
print("❌ Koneksi gagal. Cek dokumentasi Novita AI")
|
| 152 |
+
print("💡 Atau gunakan alternatif lain seperti local models")
|
| 153 |
+
|
| 154 |
+
if __name__ == "__main__":
|
| 155 |
+
main()
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
|
scripts/test_textilindo_ai.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk testing Textilindo AI Assistant yang sudah di-fine-tune
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import yaml
|
| 9 |
+
import torch
|
| 10 |
+
import argparse
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from transformers import AutoTokenizer, AutoModelForCausalLM
|
| 13 |
+
from peft import PeftModel
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
logging.basicConfig(level=logging.INFO)
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
def load_system_prompt(system_prompt_path):
|
| 20 |
+
"""Load system prompt from markdown file"""
|
| 21 |
+
try:
|
| 22 |
+
with open(system_prompt_path, 'r', encoding='utf-8') as f:
|
| 23 |
+
content = f.read()
|
| 24 |
+
|
| 25 |
+
# Extract SYSTEM_PROMPT from markdown
|
| 26 |
+
if 'SYSTEM_PROMPT = """' in content:
|
| 27 |
+
start = content.find('SYSTEM_PROMPT = """') + len('SYSTEM_PROMPT = """')
|
| 28 |
+
end = content.find('"""', start)
|
| 29 |
+
system_prompt = content[start:end].strip()
|
| 30 |
+
else:
|
| 31 |
+
# Fallback: use entire content
|
| 32 |
+
system_prompt = content.strip()
|
| 33 |
+
|
| 34 |
+
return system_prompt
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Error loading system prompt: {e}")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
def load_finetuned_model(model_path, lora_weights_path, system_prompt):
|
| 40 |
+
"""Load fine-tuned model with LoRA weights"""
|
| 41 |
+
logger.info(f"Loading base model from: {model_path}")
|
| 42 |
+
|
| 43 |
+
# Load base model
|
| 44 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 45 |
+
model_path,
|
| 46 |
+
torch_dtype=torch.float16,
|
| 47 |
+
device_map="auto",
|
| 48 |
+
trust_remote_code=True
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Load LoRA weights if available
|
| 52 |
+
if lora_weights_path and os.path.exists(lora_weights_path):
|
| 53 |
+
logger.info(f"Loading LoRA weights from: {lora_weights_path}")
|
| 54 |
+
model = PeftModel.from_pretrained(model, lora_weights_path)
|
| 55 |
+
else:
|
| 56 |
+
logger.warning("No LoRA weights found, using base model")
|
| 57 |
+
|
| 58 |
+
# Load tokenizer
|
| 59 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 60 |
+
model_path,
|
| 61 |
+
trust_remote_code=True
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
if tokenizer.pad_token is None:
|
| 65 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 66 |
+
|
| 67 |
+
return model, tokenizer
|
| 68 |
+
|
| 69 |
+
def generate_response(model, tokenizer, user_input, system_prompt, max_length=512):
|
| 70 |
+
"""Generate response from the model"""
|
| 71 |
+
# Create full prompt with system prompt
|
| 72 |
+
full_prompt = f"<|system|>\n{system_prompt}\n<|user|>\n{user_input}\n<|assistant|>\n"
|
| 73 |
+
|
| 74 |
+
inputs = tokenizer(full_prompt, return_tensors="pt").to(model.device)
|
| 75 |
+
|
| 76 |
+
with torch.no_grad():
|
| 77 |
+
outputs = model.generate(
|
| 78 |
+
**inputs,
|
| 79 |
+
max_length=max_length,
|
| 80 |
+
temperature=0.7,
|
| 81 |
+
top_p=0.9,
|
| 82 |
+
top_k=40,
|
| 83 |
+
repetition_penalty=1.1,
|
| 84 |
+
do_sample=True,
|
| 85 |
+
pad_token_id=tokenizer.eos_token_id,
|
| 86 |
+
eos_token_id=tokenizer.eos_token_id,
|
| 87 |
+
stop_strings=["<|end|>", "<|user|>"]
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 91 |
+
|
| 92 |
+
# Extract only the assistant's response
|
| 93 |
+
if "<|assistant|>" in response:
|
| 94 |
+
assistant_response = response.split("<|assistant|>")[-1].strip()
|
| 95 |
+
# Remove any remaining special tokens
|
| 96 |
+
assistant_response = assistant_response.replace("<|end|>", "").strip()
|
| 97 |
+
return assistant_response
|
| 98 |
+
else:
|
| 99 |
+
return response
|
| 100 |
+
|
| 101 |
+
def interactive_test(model, tokenizer, system_prompt):
|
| 102 |
+
"""Interactive testing mode"""
|
| 103 |
+
print("🤖 Textilindo AI Assistant - Interactive Mode")
|
| 104 |
+
print("=" * 60)
|
| 105 |
+
print("Type 'quit' to exit")
|
| 106 |
+
print("-" * 60)
|
| 107 |
+
|
| 108 |
+
while True:
|
| 109 |
+
try:
|
| 110 |
+
user_input = input("\n👤 Customer: ").strip()
|
| 111 |
+
|
| 112 |
+
if user_input.lower() in ['quit', 'exit', 'q']:
|
| 113 |
+
print("👋 Terima kasih! Sampai jumpa!")
|
| 114 |
+
break
|
| 115 |
+
|
| 116 |
+
if not user_input:
|
| 117 |
+
continue
|
| 118 |
+
|
| 119 |
+
print("\n🤖 Textilindo AI: ", end="", flush=True)
|
| 120 |
+
response = generate_response(model, tokenizer, user_input, system_prompt)
|
| 121 |
+
print(response)
|
| 122 |
+
|
| 123 |
+
except KeyboardInterrupt:
|
| 124 |
+
print("\n👋 Terima kasih! Sampai jumpa!")
|
| 125 |
+
break
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.error(f"Error generating response: {e}")
|
| 128 |
+
print(f"❌ Error: {e}")
|
| 129 |
+
|
| 130 |
+
def batch_test(model, tokenizer, system_prompt, test_cases):
|
| 131 |
+
"""Batch testing with predefined test cases"""
|
| 132 |
+
print("🧪 Textilindo AI Assistant - Batch Testing")
|
| 133 |
+
print("=" * 60)
|
| 134 |
+
|
| 135 |
+
for i, test_case in enumerate(test_cases, 1):
|
| 136 |
+
print(f"\n📝 Test Case {i}: {test_case['prompt']}")
|
| 137 |
+
print("-" * 40)
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
response = generate_response(model, tokenizer, test_case['prompt'], system_prompt)
|
| 141 |
+
print(f"🤖 Response: {response}")
|
| 142 |
+
|
| 143 |
+
if 'expected' in test_case:
|
| 144 |
+
print(f"🎯 Expected: {test_case['expected']}")
|
| 145 |
+
|
| 146 |
+
except Exception as e:
|
| 147 |
+
logger.error(f"Error in test case {i}: {e}")
|
| 148 |
+
print(f"❌ Error: {e}")
|
| 149 |
+
|
| 150 |
+
def main():
|
| 151 |
+
parser = argparse.ArgumentParser(description='Test Textilindo AI Assistant')
|
| 152 |
+
parser.add_argument('--model_path', type=str, default='./models/llama-3.1-8b-instruct',
|
| 153 |
+
help='Path to base model')
|
| 154 |
+
parser.add_argument('--lora_path', type=str, default=None,
|
| 155 |
+
help='Path to LoRA weights')
|
| 156 |
+
parser.add_argument('--system_prompt', type=str, default='configs/system_prompt.md',
|
| 157 |
+
help='Path to system prompt file')
|
| 158 |
+
|
| 159 |
+
args = parser.parse_args()
|
| 160 |
+
|
| 161 |
+
print("🧪 Textilindo AI Assistant Testing")
|
| 162 |
+
print("=" * 60)
|
| 163 |
+
|
| 164 |
+
# Load system prompt
|
| 165 |
+
system_prompt = load_system_prompt(args.system_prompt)
|
| 166 |
+
if not system_prompt:
|
| 167 |
+
print(f"❌ System prompt tidak ditemukan: {args.system_prompt}")
|
| 168 |
+
sys.exit(1)
|
| 169 |
+
|
| 170 |
+
# Check if model exists
|
| 171 |
+
if not os.path.exists(args.model_path):
|
| 172 |
+
print(f"❌ Base model tidak ditemukan: {args.model_path}")
|
| 173 |
+
print("Jalankan download_model.py terlebih dahulu")
|
| 174 |
+
sys.exit(1)
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
# Load model
|
| 178 |
+
print("1️⃣ Loading model...")
|
| 179 |
+
model, tokenizer = load_finetuned_model(args.model_path, args.lora_path, system_prompt)
|
| 180 |
+
print("✅ Model loaded successfully!")
|
| 181 |
+
|
| 182 |
+
# Test cases specific to Textilindo
|
| 183 |
+
test_cases = [
|
| 184 |
+
{
|
| 185 |
+
"prompt": "dimana lokasi textilindo?",
|
| 186 |
+
"expected": "Textilindo berkantor pusat di Jl. Raya Prancis No.39, Kosambi Tim., Kec. Kosambi, Kabupaten Tangerang, Banten 15213"
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"prompt": "Jam berapa textilindo beroperasional?",
|
| 190 |
+
"expected": "Jam operasional Senin-Jumat 08:00-17:00, Sabtu 08:00-12:00."
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"prompt": "Berapa ketentuan pembelian?",
|
| 194 |
+
"expected": "Minimal order 1 roll per jenis kain"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"prompt": "bagimana dengan pembayarannya?",
|
| 198 |
+
"expected": "Pembayaran dapat dilakukan via transfer bank atau cash on delivery"
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"prompt": "apa ada gratis ongkir?",
|
| 202 |
+
"expected": "Gratis ongkir untuk order minimal 5 roll."
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"prompt": "Apa bisa dikirimkan sample? apa gratis?",
|
| 206 |
+
"expected": "hallo kak untuk sampel kita bisa kirimkan gratis ya kak 😊"
|
| 207 |
+
}
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
# Choose testing mode
|
| 211 |
+
print("\n2️⃣ Pilih mode testing:")
|
| 212 |
+
print("1. Interactive mode (chat)")
|
| 213 |
+
print("2. Batch testing")
|
| 214 |
+
print("3. Custom prompt")
|
| 215 |
+
|
| 216 |
+
choice = input("\nPilihan (1-3): ").strip()
|
| 217 |
+
|
| 218 |
+
if choice == "1":
|
| 219 |
+
interactive_test(model, tokenizer, system_prompt)
|
| 220 |
+
elif choice == "2":
|
| 221 |
+
batch_test(model, tokenizer, system_prompt, test_cases)
|
| 222 |
+
elif choice == "3":
|
| 223 |
+
custom_prompt = input("Masukkan prompt custom: ").strip()
|
| 224 |
+
if custom_prompt:
|
| 225 |
+
response = generate_response(model, tokenizer, custom_prompt, system_prompt)
|
| 226 |
+
print(f"\n🤖 Response: {response}")
|
| 227 |
+
else:
|
| 228 |
+
print("❌ Pilihan tidak valid")
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logger.error(f"Error: {e}")
|
| 232 |
+
print(f"❌ Error loading model: {e}")
|
| 233 |
+
|
| 234 |
+
if __name__ == "__main__":
|
| 235 |
+
main()
|
scripts/train_textilindo_ai.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk fine-tuning Llama 3.2 1B dengan LoRA untuk Textilindo AI Assistant
|
| 4 |
+
Menggunakan system prompt dan dataset khusus Textilindo
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import yaml
|
| 10 |
+
import json
|
| 11 |
+
import torch
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from transformers import (
|
| 14 |
+
AutoTokenizer,
|
| 15 |
+
AutoModelForCausalLM,
|
| 16 |
+
TrainingArguments,
|
| 17 |
+
Trainer,
|
| 18 |
+
DataCollatorForLanguageModeling
|
| 19 |
+
)
|
| 20 |
+
from peft import (
|
| 21 |
+
LoraConfig,
|
| 22 |
+
get_peft_model,
|
| 23 |
+
TaskType,
|
| 24 |
+
prepare_model_for_kbit_training
|
| 25 |
+
)
|
| 26 |
+
from datasets import Dataset
|
| 27 |
+
import logging
|
| 28 |
+
from datetime import datetime
|
| 29 |
+
|
| 30 |
+
# Setup logging
|
| 31 |
+
logging.basicConfig(level=logging.INFO)
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
def load_config(config_path):
|
| 35 |
+
"""Load configuration from YAML file"""
|
| 36 |
+
try:
|
| 37 |
+
with open(config_path, 'r') as f:
|
| 38 |
+
config = yaml.safe_load(f)
|
| 39 |
+
return config
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Error loading config: {e}")
|
| 42 |
+
return None
|
| 43 |
+
|
| 44 |
+
def load_system_prompt(system_prompt_path):
|
| 45 |
+
"""Load system prompt from markdown file"""
|
| 46 |
+
try:
|
| 47 |
+
with open(system_prompt_path, 'r', encoding='utf-8') as f:
|
| 48 |
+
content = f.read()
|
| 49 |
+
|
| 50 |
+
# Extract SYSTEM_PROMPT from markdown
|
| 51 |
+
if 'SYSTEM_PROMPT = """' in content:
|
| 52 |
+
start = content.find('SYSTEM_PROMPT = """') + len('SYSTEM_PROMPT = """')
|
| 53 |
+
end = content.find('"""', start)
|
| 54 |
+
system_prompt = content[start:end].strip()
|
| 55 |
+
else:
|
| 56 |
+
# Fallback: use entire content
|
| 57 |
+
system_prompt = content.strip()
|
| 58 |
+
|
| 59 |
+
return system_prompt
|
| 60 |
+
except Exception as e:
|
| 61 |
+
logger.error(f"Error loading system prompt: {e}")
|
| 62 |
+
return None
|
| 63 |
+
|
| 64 |
+
def load_model_and_tokenizer(config):
|
| 65 |
+
"""Load base model and tokenizer"""
|
| 66 |
+
model_path = config['model_path']
|
| 67 |
+
|
| 68 |
+
logger.info(f"Loading model from: {model_path}")
|
| 69 |
+
|
| 70 |
+
# Load tokenizer
|
| 71 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 72 |
+
model_path,
|
| 73 |
+
trust_remote_code=True,
|
| 74 |
+
padding_side="right"
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
if tokenizer.pad_token is None:
|
| 78 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 79 |
+
|
| 80 |
+
# Load model with memory optimization
|
| 81 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 82 |
+
model_path,
|
| 83 |
+
torch_dtype=torch.float16,
|
| 84 |
+
device_map="auto",
|
| 85 |
+
trust_remote_code=True,
|
| 86 |
+
low_cpu_mem_usage=True,
|
| 87 |
+
load_in_8bit=True # Use 8-bit quantization for memory efficiency
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Prepare model for k-bit training
|
| 91 |
+
model = prepare_model_for_kbit_training(model)
|
| 92 |
+
|
| 93 |
+
return model, tokenizer
|
| 94 |
+
|
| 95 |
+
def setup_lora_config(config):
|
| 96 |
+
"""Setup LoRA configuration"""
|
| 97 |
+
lora_config = config['lora_config']
|
| 98 |
+
|
| 99 |
+
peft_config = LoraConfig(
|
| 100 |
+
task_type=TaskType.CAUSAL_LM,
|
| 101 |
+
r=lora_config['r'],
|
| 102 |
+
lora_alpha=lora_config['lora_alpha'],
|
| 103 |
+
lora_dropout=lora_config['lora_dropout'],
|
| 104 |
+
target_modules=lora_config['target_modules'],
|
| 105 |
+
bias="none",
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
return peft_config
|
| 109 |
+
|
| 110 |
+
def prepare_textilindo_dataset(data_path, tokenizer, system_prompt, max_length=2048):
|
| 111 |
+
"""Prepare Textilindo dataset for training with system prompt"""
|
| 112 |
+
logger.info(f"Loading dataset from: {data_path}")
|
| 113 |
+
|
| 114 |
+
# Load JSONL dataset
|
| 115 |
+
data = []
|
| 116 |
+
with open(data_path, 'r', encoding='utf-8') as f:
|
| 117 |
+
for line_num, line in enumerate(f, 1):
|
| 118 |
+
line = line.strip()
|
| 119 |
+
if line:
|
| 120 |
+
try:
|
| 121 |
+
json_obj = json.loads(line)
|
| 122 |
+
data.append(json_obj)
|
| 123 |
+
except json.JSONDecodeError as e:
|
| 124 |
+
logger.warning(f"Invalid JSON at line {line_num}: {e}")
|
| 125 |
+
continue
|
| 126 |
+
|
| 127 |
+
if not data:
|
| 128 |
+
raise ValueError("No valid JSON objects found in JSONL file")
|
| 129 |
+
|
| 130 |
+
logger.info(f"Loaded {len(data)} samples from JSONL file")
|
| 131 |
+
|
| 132 |
+
# Convert to training format with system prompt
|
| 133 |
+
training_data = []
|
| 134 |
+
for item in data:
|
| 135 |
+
# Extract instruction and output
|
| 136 |
+
instruction = item.get('instruction', '')
|
| 137 |
+
output = item.get('output', '')
|
| 138 |
+
|
| 139 |
+
if not instruction or not output:
|
| 140 |
+
continue
|
| 141 |
+
|
| 142 |
+
# Create training text with system prompt
|
| 143 |
+
training_text = f"<|system|>\n{system_prompt}\n<|user|>\n{instruction}\n<|assistant|>\n{output}<|end|>"
|
| 144 |
+
|
| 145 |
+
training_data.append({
|
| 146 |
+
'text': training_text,
|
| 147 |
+
'instruction': instruction,
|
| 148 |
+
'output': output
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
# Convert to Dataset
|
| 152 |
+
dataset = Dataset.from_list(training_data)
|
| 153 |
+
logger.info(f"Prepared {len(dataset)} training samples")
|
| 154 |
+
|
| 155 |
+
def tokenize_function(examples):
|
| 156 |
+
# Tokenize the texts
|
| 157 |
+
tokenized = tokenizer(
|
| 158 |
+
examples['text'],
|
| 159 |
+
truncation=True,
|
| 160 |
+
padding=True,
|
| 161 |
+
max_length=max_length,
|
| 162 |
+
return_tensors="pt"
|
| 163 |
+
)
|
| 164 |
+
return tokenized
|
| 165 |
+
|
| 166 |
+
# Tokenize dataset
|
| 167 |
+
tokenized_dataset = dataset.map(
|
| 168 |
+
tokenize_function,
|
| 169 |
+
batched=True,
|
| 170 |
+
remove_columns=dataset.column_names
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
return tokenized_dataset
|
| 174 |
+
|
| 175 |
+
def train_model(model, tokenizer, dataset, config, output_dir):
|
| 176 |
+
"""Train the model with LoRA"""
|
| 177 |
+
training_config = config['training_config']
|
| 178 |
+
|
| 179 |
+
# Setup training arguments
|
| 180 |
+
training_args = TrainingArguments(
|
| 181 |
+
output_dir=output_dir,
|
| 182 |
+
num_train_epochs=training_config['num_epochs'],
|
| 183 |
+
per_device_train_batch_size=training_config['batch_size'],
|
| 184 |
+
gradient_accumulation_steps=training_config['gradient_accumulation_steps'],
|
| 185 |
+
learning_rate=training_config['learning_rate'],
|
| 186 |
+
warmup_steps=training_config['warmup_steps'],
|
| 187 |
+
save_steps=training_config['save_steps'],
|
| 188 |
+
eval_steps=training_config['eval_steps'],
|
| 189 |
+
logging_steps=training_config.get('logging_steps', 10),
|
| 190 |
+
save_total_limit=training_config.get('save_total_limit', 3),
|
| 191 |
+
prediction_loss_only=training_config.get('prediction_loss_only', True),
|
| 192 |
+
remove_unused_columns=training_config.get('remove_unused_columns', False),
|
| 193 |
+
push_to_hub=training_config.get('push_to_hub', False),
|
| 194 |
+
report_to=training_config.get('report_to', None),
|
| 195 |
+
fp16=True, # Enable mixed precision training
|
| 196 |
+
dataloader_pin_memory=False, # Reduce memory usage
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
# Setup data collator
|
| 200 |
+
data_collator = DataCollatorForLanguageModeling(
|
| 201 |
+
tokenizer=tokenizer,
|
| 202 |
+
mlm=False,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# Setup trainer
|
| 206 |
+
trainer = Trainer(
|
| 207 |
+
model=model,
|
| 208 |
+
args=training_args,
|
| 209 |
+
train_dataset=dataset,
|
| 210 |
+
data_collator=data_collator,
|
| 211 |
+
tokenizer=tokenizer,
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Start training
|
| 215 |
+
logger.info("Starting training...")
|
| 216 |
+
trainer.train()
|
| 217 |
+
|
| 218 |
+
# Save the model
|
| 219 |
+
trainer.save_model()
|
| 220 |
+
logger.info(f"Model saved to: {output_dir}")
|
| 221 |
+
|
| 222 |
+
def main():
|
| 223 |
+
print("🚀 Textilindo AI Assistant - LoRA Fine-tuning")
|
| 224 |
+
print("=" * 60)
|
| 225 |
+
|
| 226 |
+
# Load configuration
|
| 227 |
+
config_path = "configs/training_config.yaml"
|
| 228 |
+
if not os.path.exists(config_path):
|
| 229 |
+
print(f"❌ Config file tidak ditemukan: {config_path}")
|
| 230 |
+
sys.exit(1)
|
| 231 |
+
|
| 232 |
+
config = load_config(config_path)
|
| 233 |
+
if not config:
|
| 234 |
+
sys.exit(1)
|
| 235 |
+
|
| 236 |
+
# Load system prompt
|
| 237 |
+
system_prompt_path = "configs/system_prompt.md"
|
| 238 |
+
if not os.path.exists(system_prompt_path):
|
| 239 |
+
print(f"❌ System prompt tidak ditemukan: {system_prompt_path}")
|
| 240 |
+
sys.exit(1)
|
| 241 |
+
|
| 242 |
+
system_prompt = load_system_prompt(system_prompt_path)
|
| 243 |
+
if not system_prompt:
|
| 244 |
+
sys.exit(1)
|
| 245 |
+
|
| 246 |
+
# Setup paths
|
| 247 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 248 |
+
output_dir = Path(f"models/textilindo-ai-lora-{timestamp}")
|
| 249 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 250 |
+
|
| 251 |
+
# Check if dataset exists
|
| 252 |
+
data_path = config['dataset_path']
|
| 253 |
+
if not os.path.exists(data_path):
|
| 254 |
+
print(f"❌ Dataset tidak ditemukan: {data_path}")
|
| 255 |
+
sys.exit(1)
|
| 256 |
+
|
| 257 |
+
# Load model and tokenizer
|
| 258 |
+
print("1️⃣ Loading model and tokenizer...")
|
| 259 |
+
model, tokenizer = load_model_and_tokenizer(config)
|
| 260 |
+
|
| 261 |
+
# Setup LoRA
|
| 262 |
+
print("2️⃣ Setting up LoRA configuration...")
|
| 263 |
+
peft_config = setup_lora_config(config)
|
| 264 |
+
model = get_peft_model(model, peft_config)
|
| 265 |
+
|
| 266 |
+
# Print trainable parameters
|
| 267 |
+
model.print_trainable_parameters()
|
| 268 |
+
|
| 269 |
+
# Prepare dataset
|
| 270 |
+
print("3️⃣ Preparing Textilindo dataset...")
|
| 271 |
+
dataset = prepare_textilindo_dataset(data_path, tokenizer, system_prompt, config['max_length'])
|
| 272 |
+
|
| 273 |
+
# Train model
|
| 274 |
+
print("4️⃣ Starting training...")
|
| 275 |
+
train_model(model, tokenizer, dataset, config, output_dir)
|
| 276 |
+
|
| 277 |
+
print("✅ Training selesai!")
|
| 278 |
+
print(f"📁 Model tersimpan di: {output_dir}")
|
| 279 |
+
print(f"🔧 Untuk testing, jalankan: python scripts/test_textilindo_ai.py --model_path {output_dir}")
|
| 280 |
+
|
| 281 |
+
if __name__ == "__main__":
|
| 282 |
+
main()
|
scripts/train_textilindo_ai_optimized.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Memory-optimized training script untuk Textilindo AI Assistant
|
| 4 |
+
Optimized untuk Llama 3.1 8B dengan LoRA pada laptop
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import yaml
|
| 10 |
+
import json
|
| 11 |
+
import torch
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from transformers import (
|
| 14 |
+
AutoTokenizer,
|
| 15 |
+
AutoModelForCausalLM,
|
| 16 |
+
TrainingArguments,
|
| 17 |
+
Trainer,
|
| 18 |
+
DataCollatorForLanguageModeling,
|
| 19 |
+
BitsAndBytesConfig
|
| 20 |
+
)
|
| 21 |
+
from peft import (
|
| 22 |
+
LoraConfig,
|
| 23 |
+
get_peft_model,
|
| 24 |
+
TaskType,
|
| 25 |
+
prepare_model_for_kbit_training
|
| 26 |
+
)
|
| 27 |
+
from datasets import Dataset
|
| 28 |
+
import logging
|
| 29 |
+
from datetime import datetime
|
| 30 |
+
import gc
|
| 31 |
+
|
| 32 |
+
# Setup logging
|
| 33 |
+
logging.basicConfig(level=logging.INFO)
|
| 34 |
+
logger = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
def load_config(config_path):
|
| 37 |
+
"""Load configuration from YAML file"""
|
| 38 |
+
try:
|
| 39 |
+
with open(config_path, 'r') as f:
|
| 40 |
+
config = yaml.safe_load(f)
|
| 41 |
+
return config
|
| 42 |
+
except Exception as e:
|
| 43 |
+
logger.error(f"Error loading config: {e}")
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
def load_system_prompt(system_prompt_path):
|
| 47 |
+
"""Load system prompt from markdown file"""
|
| 48 |
+
try:
|
| 49 |
+
with open(system_prompt_path, 'r', encoding='utf-8') as f:
|
| 50 |
+
content = f.read()
|
| 51 |
+
|
| 52 |
+
# Extract SYSTEM_PROMPT from markdown
|
| 53 |
+
if 'SYSTEM_PROMPT = """' in content:
|
| 54 |
+
start = content.find('SYSTEM_PROMPT = """') + len('SYSTEM_PROMPT = """')
|
| 55 |
+
end = content.find('"""', start)
|
| 56 |
+
system_prompt = content[start:end].strip()
|
| 57 |
+
else:
|
| 58 |
+
# Fallback: use entire content
|
| 59 |
+
system_prompt = content.strip()
|
| 60 |
+
|
| 61 |
+
return system_prompt
|
| 62 |
+
except Exception as e:
|
| 63 |
+
logger.error(f"Error loading system prompt: {e}")
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
def load_model_and_tokenizer(config):
|
| 67 |
+
"""Load base model and tokenizer with memory optimization"""
|
| 68 |
+
model_path = config['model_path']
|
| 69 |
+
|
| 70 |
+
logger.info(f"Loading model from: {model_path}")
|
| 71 |
+
|
| 72 |
+
# Configure quantization for memory efficiency
|
| 73 |
+
bnb_config = BitsAndBytesConfig(
|
| 74 |
+
load_in_4bit=True,
|
| 75 |
+
bnb_4bit_use_double_quant=True,
|
| 76 |
+
bnb_4bit_quant_type="nf4",
|
| 77 |
+
bnb_4bit_compute_dtype=torch.float16
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# Load tokenizer
|
| 81 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 82 |
+
model_path,
|
| 83 |
+
trust_remote_code=True,
|
| 84 |
+
padding_side="right"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
if tokenizer.pad_token is None:
|
| 88 |
+
tokenizer.pad_token = tokenizer.eos_token
|
| 89 |
+
|
| 90 |
+
# Load model with 4-bit quantization
|
| 91 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 92 |
+
model_path,
|
| 93 |
+
quantization_config=bnb_config,
|
| 94 |
+
device_map="auto",
|
| 95 |
+
trust_remote_code=True,
|
| 96 |
+
low_cpu_mem_usage=True
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
# Prepare model for k-bit training
|
| 100 |
+
model = prepare_model_for_kbit_training(model)
|
| 101 |
+
|
| 102 |
+
return model, tokenizer
|
| 103 |
+
|
| 104 |
+
def setup_lora_config(config):
|
| 105 |
+
"""Setup LoRA configuration optimized for 8B model"""
|
| 106 |
+
lora_config = config['lora_config']
|
| 107 |
+
|
| 108 |
+
peft_config = LoraConfig(
|
| 109 |
+
task_type=TaskType.CAUSAL_LM,
|
| 110 |
+
r=lora_config['r'],
|
| 111 |
+
lora_alpha=lora_config['lora_alpha'],
|
| 112 |
+
lora_dropout=lora_config['lora_dropout'],
|
| 113 |
+
target_modules=lora_config['target_modules'],
|
| 114 |
+
bias="none",
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
return peft_config
|
| 118 |
+
|
| 119 |
+
def prepare_textilindo_dataset(data_path, tokenizer, system_prompt, max_length=1024):
|
| 120 |
+
"""Prepare Textilindo dataset for training with system prompt"""
|
| 121 |
+
logger.info(f"Loading dataset from: {data_path}")
|
| 122 |
+
|
| 123 |
+
# Load JSONL dataset
|
| 124 |
+
data = []
|
| 125 |
+
with open(data_path, 'r', encoding='utf-8') as f:
|
| 126 |
+
for line_num, line in enumerate(f, 1):
|
| 127 |
+
line = line.strip()
|
| 128 |
+
if line:
|
| 129 |
+
try:
|
| 130 |
+
json_obj = json.loads(line)
|
| 131 |
+
data.append(json_obj)
|
| 132 |
+
except json.JSONDecodeError as e:
|
| 133 |
+
logger.warning(f"Invalid JSON at line {line_num}: {e}")
|
| 134 |
+
continue
|
| 135 |
+
|
| 136 |
+
if not data:
|
| 137 |
+
raise ValueError("No valid JSON objects found in JSONL file")
|
| 138 |
+
|
| 139 |
+
logger.info(f"Loaded {len(data)} samples from JSONL file")
|
| 140 |
+
|
| 141 |
+
# Convert to training format with system prompt
|
| 142 |
+
training_data = []
|
| 143 |
+
for item in data:
|
| 144 |
+
# Extract instruction and output
|
| 145 |
+
instruction = item.get('instruction', '')
|
| 146 |
+
output = item.get('output', '')
|
| 147 |
+
|
| 148 |
+
if not instruction or not output:
|
| 149 |
+
continue
|
| 150 |
+
|
| 151 |
+
# Create training text with system prompt (shorter for memory efficiency)
|
| 152 |
+
training_text = f"<|system|>\n{system_prompt[:500]}...\n<|user|>\n{instruction}\n<|assistant|>\n{output}<|end|>"
|
| 153 |
+
|
| 154 |
+
training_data.append({
|
| 155 |
+
'text': training_text,
|
| 156 |
+
'instruction': instruction,
|
| 157 |
+
'output': output
|
| 158 |
+
})
|
| 159 |
+
|
| 160 |
+
# Convert to Dataset
|
| 161 |
+
dataset = Dataset.from_list(training_data)
|
| 162 |
+
logger.info(f"Prepared {len(dataset)} training samples")
|
| 163 |
+
|
| 164 |
+
def tokenize_function(examples):
|
| 165 |
+
# Tokenize the texts
|
| 166 |
+
tokenized = tokenizer(
|
| 167 |
+
examples['text'],
|
| 168 |
+
truncation=True,
|
| 169 |
+
padding=True,
|
| 170 |
+
max_length=max_length,
|
| 171 |
+
return_tensors="pt"
|
| 172 |
+
)
|
| 173 |
+
return tokenized
|
| 174 |
+
|
| 175 |
+
# Tokenize dataset
|
| 176 |
+
tokenized_dataset = dataset.map(
|
| 177 |
+
tokenize_function,
|
| 178 |
+
batched=True,
|
| 179 |
+
remove_columns=dataset.column_names
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
return tokenized_dataset
|
| 183 |
+
|
| 184 |
+
def train_model(model, tokenizer, dataset, config, output_dir):
|
| 185 |
+
"""Train the model with LoRA - memory optimized"""
|
| 186 |
+
training_config = config['training_config']
|
| 187 |
+
|
| 188 |
+
# Setup training arguments optimized for memory
|
| 189 |
+
training_args = TrainingArguments(
|
| 190 |
+
output_dir=output_dir,
|
| 191 |
+
num_train_epochs=training_config['num_epochs'],
|
| 192 |
+
per_device_train_batch_size=training_config['batch_size'],
|
| 193 |
+
gradient_accumulation_steps=training_config['gradient_accumulation_steps'],
|
| 194 |
+
learning_rate=training_config['learning_rate'],
|
| 195 |
+
warmup_steps=training_config['warmup_steps'],
|
| 196 |
+
save_steps=training_config['save_steps'],
|
| 197 |
+
eval_steps=training_config['eval_steps'],
|
| 198 |
+
logging_steps=training_config.get('logging_steps', 5),
|
| 199 |
+
save_total_limit=training_config.get('save_total_limit', 2),
|
| 200 |
+
prediction_loss_only=training_config.get('prediction_loss_only', True),
|
| 201 |
+
remove_unused_columns=training_config.get('remove_unused_columns', False),
|
| 202 |
+
push_to_hub=training_config.get('push_to_hub', False),
|
| 203 |
+
report_to=training_config.get('report_to', None),
|
| 204 |
+
fp16=True, # Enable mixed precision training
|
| 205 |
+
dataloader_pin_memory=False, # Reduce memory usage
|
| 206 |
+
dataloader_num_workers=0, # Reduce memory usage
|
| 207 |
+
gradient_checkpointing=True, # Enable gradient checkpointing for memory
|
| 208 |
+
optim="adamw_torch", # Use AdamW optimizer
|
| 209 |
+
max_grad_norm=1.0, # Gradient clipping
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
# Setup data collator
|
| 213 |
+
data_collator = DataCollatorForLanguageModeling(
|
| 214 |
+
tokenizer=tokenizer,
|
| 215 |
+
mlm=False,
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Setup trainer
|
| 219 |
+
trainer = Trainer(
|
| 220 |
+
model=model,
|
| 221 |
+
args=training_args,
|
| 222 |
+
train_dataset=dataset,
|
| 223 |
+
data_collator=data_collator,
|
| 224 |
+
tokenizer=tokenizer,
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Start training
|
| 228 |
+
logger.info("Starting training...")
|
| 229 |
+
trainer.train()
|
| 230 |
+
|
| 231 |
+
# Save the model
|
| 232 |
+
trainer.save_model()
|
| 233 |
+
logger.info(f"Model saved to: {output_dir}")
|
| 234 |
+
|
| 235 |
+
def main():
|
| 236 |
+
print("🚀 Textilindo AI Assistant - Memory Optimized LoRA Training")
|
| 237 |
+
print("=" * 70)
|
| 238 |
+
|
| 239 |
+
# Load configuration
|
| 240 |
+
config_path = "configs/training_config.yaml"
|
| 241 |
+
if not os.path.exists(config_path):
|
| 242 |
+
print(f"❌ Config file tidak ditemukan: {config_path}")
|
| 243 |
+
sys.exit(1)
|
| 244 |
+
|
| 245 |
+
config = load_config(config_path)
|
| 246 |
+
if not config:
|
| 247 |
+
sys.exit(1)
|
| 248 |
+
|
| 249 |
+
# Load system prompt
|
| 250 |
+
system_prompt_path = "configs/system_prompt.md"
|
| 251 |
+
if not os.path.exists(system_prompt_path):
|
| 252 |
+
print(f"❌ System prompt tidak ditemukan: {system_prompt_path}")
|
| 253 |
+
sys.exit(1)
|
| 254 |
+
|
| 255 |
+
system_prompt = load_system_prompt(system_prompt_path)
|
| 256 |
+
if not system_prompt:
|
| 257 |
+
sys.exit(1)
|
| 258 |
+
|
| 259 |
+
# Setup paths
|
| 260 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 261 |
+
output_dir = Path(f"models/textilindo-ai-lora-8b-{timestamp}")
|
| 262 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 263 |
+
|
| 264 |
+
# Check if dataset exists
|
| 265 |
+
data_path = config['dataset_path']
|
| 266 |
+
if not os.path.exists(data_path):
|
| 267 |
+
print(f"❌ Dataset tidak ditemukan: {data_path}")
|
| 268 |
+
sys.exit(1)
|
| 269 |
+
|
| 270 |
+
# Load model and tokenizer
|
| 271 |
+
print("1️⃣ Loading model and tokenizer (4-bit quantized)...")
|
| 272 |
+
model, tokenizer = load_model_and_tokenizer(config)
|
| 273 |
+
|
| 274 |
+
# Setup LoRA
|
| 275 |
+
print("2️⃣ Setting up LoRA configuration...")
|
| 276 |
+
peft_config = setup_lora_config(config)
|
| 277 |
+
model = get_peft_model(model, peft_config)
|
| 278 |
+
|
| 279 |
+
# Print trainable parameters
|
| 280 |
+
model.print_trainable_parameters()
|
| 281 |
+
|
| 282 |
+
# Prepare dataset
|
| 283 |
+
print("3️⃣ Preparing Textilindo dataset...")
|
| 284 |
+
dataset = prepare_textilindo_dataset(data_path, tokenizer, system_prompt, config['max_length'])
|
| 285 |
+
|
| 286 |
+
# Train model
|
| 287 |
+
print("4️⃣ Starting training...")
|
| 288 |
+
print("⚠️ This may take several hours. Monitor GPU memory usage.")
|
| 289 |
+
train_model(model, tokenizer, dataset, config, output_dir)
|
| 290 |
+
|
| 291 |
+
print("✅ Training selesai!")
|
| 292 |
+
print(f"📁 Model tersimpan di: {output_dir}")
|
| 293 |
+
print(f"🔧 Untuk testing, jalankan: python scripts/test_textilindo_ai.py --model_path {output_dir}")
|
| 294 |
+
|
| 295 |
+
if __name__ == "__main__":
|
| 296 |
+
main()
|
scripts/train_with_monitoring.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script untuk training dengan monitoring GPU dan logging yang lengkap
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import time
|
| 9 |
+
import json
|
| 10 |
+
import psutil
|
| 11 |
+
import GPUtil
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
import logging
|
| 15 |
+
from finetune_lora import main as finetune_main
|
| 16 |
+
|
| 17 |
+
def setup_logging():
|
| 18 |
+
"""Setup logging dengan format yang lengkap"""
|
| 19 |
+
log_dir = Path("logs")
|
| 20 |
+
log_dir.mkdir(exist_ok=True)
|
| 21 |
+
|
| 22 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 23 |
+
log_file = log_dir / f"training_{timestamp}.log"
|
| 24 |
+
|
| 25 |
+
# Setup logging format
|
| 26 |
+
logging.basicConfig(
|
| 27 |
+
level=logging.INFO,
|
| 28 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 29 |
+
handlers=[
|
| 30 |
+
logging.FileHandler(log_file, encoding='utf-8'),
|
| 31 |
+
logging.StreamHandler(sys.stdout)
|
| 32 |
+
]
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
return logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
def get_system_info():
|
| 38 |
+
"""Get system information"""
|
| 39 |
+
info = {
|
| 40 |
+
"timestamp": datetime.now().isoformat(),
|
| 41 |
+
"cpu_count": psutil.cpu_count(),
|
| 42 |
+
"memory_total_gb": round(psutil.virtual_memory().total / (1024**3), 2),
|
| 43 |
+
"memory_available_gb": round(psutil.virtual_memory().available / (1024**3), 2),
|
| 44 |
+
"disk_usage": {}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
# Disk usage
|
| 48 |
+
for partition in psutil.disk_partitions():
|
| 49 |
+
try:
|
| 50 |
+
usage = psutil.disk_usage(partition.mountpoint)
|
| 51 |
+
info["disk_usage"][partition.mountpoint] = {
|
| 52 |
+
"total_gb": round(usage.total / (1024**3), 2),
|
| 53 |
+
"used_gb": round(usage.used / (1024**3), 2),
|
| 54 |
+
"free_gb": round(usage.free / (1024**3), 2),
|
| 55 |
+
"percent": usage.percent
|
| 56 |
+
}
|
| 57 |
+
except PermissionError:
|
| 58 |
+
continue
|
| 59 |
+
|
| 60 |
+
return info
|
| 61 |
+
|
| 62 |
+
def get_gpu_info():
|
| 63 |
+
"""Get GPU information"""
|
| 64 |
+
try:
|
| 65 |
+
gpus = GPUtil.getGPUs()
|
| 66 |
+
gpu_info = []
|
| 67 |
+
|
| 68 |
+
for gpu in gpus:
|
| 69 |
+
gpu_info.append({
|
| 70 |
+
"id": gpu.id,
|
| 71 |
+
"name": gpu.name,
|
| 72 |
+
"memory_total_mb": gpu.memoryTotal,
|
| 73 |
+
"memory_used_mb": gpu.memoryUsed,
|
| 74 |
+
"memory_free_mb": gpu.memoryFree,
|
| 75 |
+
"memory_utilization_percent": gpu.memoryUtil * 100,
|
| 76 |
+
"gpu_utilization_percent": gpu.load * 100,
|
| 77 |
+
"temperature_celsius": gpu.temperature
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
return gpu_info
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logging.warning(f"Could not get GPU info: {e}")
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
def monitor_resources(logger, interval=30):
|
| 86 |
+
"""Monitor system resources during training"""
|
| 87 |
+
logger.info("🔍 Starting resource monitoring...")
|
| 88 |
+
|
| 89 |
+
start_time = time.time()
|
| 90 |
+
monitoring_data = []
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
while True:
|
| 94 |
+
# Get current resource usage
|
| 95 |
+
current_time = time.time()
|
| 96 |
+
elapsed_time = current_time - start_time
|
| 97 |
+
|
| 98 |
+
# System info
|
| 99 |
+
system_info = get_system_info()
|
| 100 |
+
system_info["elapsed_time_seconds"] = elapsed_time
|
| 101 |
+
|
| 102 |
+
# GPU info
|
| 103 |
+
gpu_info = get_gpu_info()
|
| 104 |
+
|
| 105 |
+
# Memory usage
|
| 106 |
+
memory = psutil.virtual_memory()
|
| 107 |
+
system_info["memory_used_gb"] = round(memory.used / (1024**3), 2)
|
| 108 |
+
system_info["memory_percent"] = memory.percent
|
| 109 |
+
|
| 110 |
+
# CPU usage
|
| 111 |
+
system_info["cpu_percent"] = psutil.cpu_percent(interval=1)
|
| 112 |
+
|
| 113 |
+
# Combine all info
|
| 114 |
+
monitoring_entry = {
|
| 115 |
+
"timestamp": datetime.now().isoformat(),
|
| 116 |
+
"elapsed_time_seconds": elapsed_time,
|
| 117 |
+
"system": system_info,
|
| 118 |
+
"gpu": gpu_info
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
monitoring_data.append(monitoring_entry)
|
| 122 |
+
|
| 123 |
+
# Log summary
|
| 124 |
+
logger.info(f"⏱️ Elapsed: {elapsed_time/60:.1f}min | "
|
| 125 |
+
f"CPU: {system_info['cpu_percent']:.1f}% | "
|
| 126 |
+
f"RAM: {system_info['memory_percent']:.1f}%")
|
| 127 |
+
|
| 128 |
+
if gpu_info:
|
| 129 |
+
for gpu in gpu_info:
|
| 130 |
+
logger.info(f"🎮 GPU {gpu['id']}: "
|
| 131 |
+
f"Util: {gpu['gpu_utilization_percent']:.1f}% | "
|
| 132 |
+
f"Memory: {gpu['memory_utilization_percent']:.1f}% | "
|
| 133 |
+
f"Temp: {gpu['temperature_celsius']:.1f}°C")
|
| 134 |
+
|
| 135 |
+
# Save monitoring data periodically
|
| 136 |
+
if len(monitoring_data) % 10 == 0: # Every 10 entries
|
| 137 |
+
monitoring_file = Path("logs") / f"monitoring_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 138 |
+
with open(monitoring_file, 'w') as f:
|
| 139 |
+
json.dump(monitoring_data, f, indent=2)
|
| 140 |
+
logger.info(f"💾 Monitoring data saved: {monitoring_file}")
|
| 141 |
+
|
| 142 |
+
time.sleep(interval)
|
| 143 |
+
|
| 144 |
+
except KeyboardInterrupt:
|
| 145 |
+
logger.info("⏹️ Resource monitoring stopped by user")
|
| 146 |
+
|
| 147 |
+
return monitoring_data
|
| 148 |
+
|
| 149 |
+
def main():
|
| 150 |
+
"""Main function untuk training dengan monitoring"""
|
| 151 |
+
print("🚀 Training dengan Monitoring - Llama 3.1 8B LoRA")
|
| 152 |
+
print("=" * 60)
|
| 153 |
+
|
| 154 |
+
# Setup logging
|
| 155 |
+
logger = setup_logging()
|
| 156 |
+
|
| 157 |
+
# Log system information
|
| 158 |
+
logger.info("🖥️ System Information:")
|
| 159 |
+
system_info = get_system_info()
|
| 160 |
+
for key, value in system_info.items():
|
| 161 |
+
if key != "disk_usage":
|
| 162 |
+
logger.info(f" {key}: {value}")
|
| 163 |
+
|
| 164 |
+
# Log GPU information
|
| 165 |
+
gpu_info = get_gpu_info()
|
| 166 |
+
if gpu_info:
|
| 167 |
+
logger.info("🎮 GPU Information:")
|
| 168 |
+
for gpu in gpu_info:
|
| 169 |
+
logger.info(f" GPU {gpu['id']}: {gpu['name']}")
|
| 170 |
+
logger.info(f" Memory: {gpu['memory_total_mb']}MB total")
|
| 171 |
+
logger.info(f" Temperature: {gpu['temperature_celsius']}°C")
|
| 172 |
+
else:
|
| 173 |
+
logger.warning("⚠️ No GPU detected. Training will be very slow on CPU!")
|
| 174 |
+
|
| 175 |
+
# Check prerequisites
|
| 176 |
+
logger.info("🔍 Checking prerequisites...")
|
| 177 |
+
|
| 178 |
+
# Check if model exists
|
| 179 |
+
model_path = Path("models/llama-3.1-8b-instruct")
|
| 180 |
+
if not model_path.exists():
|
| 181 |
+
logger.error("❌ Base model not found. Please run download_model.py first!")
|
| 182 |
+
return
|
| 183 |
+
|
| 184 |
+
# Check if dataset exists
|
| 185 |
+
data_path = Path("data/training_data.jsonl")
|
| 186 |
+
if not data_path.exists():
|
| 187 |
+
logger.error("❌ Training dataset not found. Please run create_sample_dataset.py first!")
|
| 188 |
+
return
|
| 189 |
+
|
| 190 |
+
# Check if config exists
|
| 191 |
+
config_path = Path("configs/llama_config.yaml")
|
| 192 |
+
if not config_path.exists():
|
| 193 |
+
logger.error("❌ Model configuration not found. Please run download_model.py first!")
|
| 194 |
+
return
|
| 195 |
+
|
| 196 |
+
logger.info("✅ All prerequisites met!")
|
| 197 |
+
|
| 198 |
+
# Start resource monitoring in background
|
| 199 |
+
import threading
|
| 200 |
+
monitoring_thread = threading.Thread(
|
| 201 |
+
target=monitor_resources,
|
| 202 |
+
args=(logger, 30), # Monitor every 30 seconds
|
| 203 |
+
daemon=True
|
| 204 |
+
)
|
| 205 |
+
monitoring_thread.start()
|
| 206 |
+
|
| 207 |
+
# Start training
|
| 208 |
+
logger.info("🚀 Starting LoRA fine-tuning...")
|
| 209 |
+
try:
|
| 210 |
+
finetune_main()
|
| 211 |
+
logger.info("✅ Training completed successfully!")
|
| 212 |
+
except Exception as e:
|
| 213 |
+
logger.error(f"❌ Training failed: {e}")
|
| 214 |
+
raise
|
| 215 |
+
finally:
|
| 216 |
+
logger.info("📊 Training session ended")
|
| 217 |
+
|
| 218 |
+
# Save final monitoring data
|
| 219 |
+
monitoring_file = Path("logs") / f"final_monitoring_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 220 |
+
# Note: In a real implementation, you'd want to capture the monitoring data
|
| 221 |
+
logger.info(f"💾 Final monitoring data saved: {monitoring_file}")
|
| 222 |
+
|
| 223 |
+
if __name__ == "__main__":
|
| 224 |
+
main()
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
|
test_deployment.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script for Textilindo AI Assistant deployment
|
| 4 |
+
Run this to verify your setup before deploying to Hugging Face Spaces
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import json
|
| 10 |
+
import requests
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
def test_file_structure():
|
| 14 |
+
"""Test if all required files exist"""
|
| 15 |
+
print("🔍 Testing file structure...")
|
| 16 |
+
|
| 17 |
+
required_files = [
|
| 18 |
+
"app.py",
|
| 19 |
+
"Dockerfile",
|
| 20 |
+
"requirements.txt",
|
| 21 |
+
"configs/system_prompt.md",
|
| 22 |
+
"configs/training_config.yaml",
|
| 23 |
+
"templates/chat.html"
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
required_dirs = [
|
| 27 |
+
"data",
|
| 28 |
+
"configs",
|
| 29 |
+
"templates"
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
missing_files = []
|
| 33 |
+
missing_dirs = []
|
| 34 |
+
|
| 35 |
+
for file in required_files:
|
| 36 |
+
if not Path(file).exists():
|
| 37 |
+
missing_files.append(file)
|
| 38 |
+
|
| 39 |
+
for dir in required_dirs:
|
| 40 |
+
if not Path(dir).exists():
|
| 41 |
+
missing_dirs.append(dir)
|
| 42 |
+
|
| 43 |
+
if missing_files:
|
| 44 |
+
print(f"❌ Missing files: {missing_files}")
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
if missing_dirs:
|
| 48 |
+
print(f"❌ Missing directories: {missing_dirs}")
|
| 49 |
+
return False
|
| 50 |
+
|
| 51 |
+
print("✅ All required files and directories exist")
|
| 52 |
+
return True
|
| 53 |
+
|
| 54 |
+
def test_data_files():
|
| 55 |
+
"""Test if data files exist and are valid"""
|
| 56 |
+
print("🔍 Testing data files...")
|
| 57 |
+
|
| 58 |
+
data_dir = Path("data")
|
| 59 |
+
if not data_dir.exists():
|
| 60 |
+
print("❌ Data directory not found")
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
jsonl_files = list(data_dir.glob("*.jsonl"))
|
| 64 |
+
if not jsonl_files:
|
| 65 |
+
print("❌ No JSONL files found in data directory")
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
print(f"✅ Found {len(jsonl_files)} JSONL files:")
|
| 69 |
+
for file in jsonl_files:
|
| 70 |
+
print(f" - {file.name}")
|
| 71 |
+
|
| 72 |
+
# Test one JSONL file
|
| 73 |
+
test_file = jsonl_files[0]
|
| 74 |
+
try:
|
| 75 |
+
with open(test_file, 'r', encoding='utf-8') as f:
|
| 76 |
+
lines = f.readlines()
|
| 77 |
+
|
| 78 |
+
if not lines:
|
| 79 |
+
print(f"❌ {test_file.name} is empty")
|
| 80 |
+
return False
|
| 81 |
+
|
| 82 |
+
# Test first line
|
| 83 |
+
first_line = lines[0].strip()
|
| 84 |
+
if first_line:
|
| 85 |
+
json.loads(first_line)
|
| 86 |
+
print(f"✅ {test_file.name} contains valid JSON")
|
| 87 |
+
|
| 88 |
+
print(f"✅ {test_file.name} has {len(lines)} lines")
|
| 89 |
+
return True
|
| 90 |
+
|
| 91 |
+
except json.JSONDecodeError as e:
|
| 92 |
+
print(f"❌ {test_file.name} contains invalid JSON: {e}")
|
| 93 |
+
return False
|
| 94 |
+
except Exception as e:
|
| 95 |
+
print(f"❌ Error reading {test_file.name}: {e}")
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
def test_config_files():
|
| 99 |
+
"""Test if configuration files are valid"""
|
| 100 |
+
print("🔍 Testing configuration files...")
|
| 101 |
+
|
| 102 |
+
# Test system prompt
|
| 103 |
+
prompt_file = Path("configs/system_prompt.md")
|
| 104 |
+
if not prompt_file.exists():
|
| 105 |
+
print("❌ System prompt file not found")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
with open(prompt_file, 'r', encoding='utf-8') as f:
|
| 110 |
+
content = f.read()
|
| 111 |
+
|
| 112 |
+
if 'SYSTEM_PROMPT' not in content:
|
| 113 |
+
print("⚠️ SYSTEM_PROMPT not found in system_prompt.md")
|
| 114 |
+
else:
|
| 115 |
+
print("✅ System prompt file is valid")
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
print(f"❌ Error reading system prompt: {e}")
|
| 119 |
+
return False
|
| 120 |
+
|
| 121 |
+
# Test training config
|
| 122 |
+
config_file = Path("configs/training_config.yaml")
|
| 123 |
+
if not config_file.exists():
|
| 124 |
+
print("❌ Training config file not found")
|
| 125 |
+
return False
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
import yaml
|
| 129 |
+
with open(config_file, 'r') as f:
|
| 130 |
+
config = yaml.safe_load(f)
|
| 131 |
+
|
| 132 |
+
required_fields = ['model_name', 'model_path', 'dataset_path']
|
| 133 |
+
for field in required_fields:
|
| 134 |
+
if field not in config:
|
| 135 |
+
print(f"❌ Missing field in config: {field}")
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
print("✅ Training configuration is valid")
|
| 139 |
+
return True
|
| 140 |
+
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"❌ Error reading training config: {e}")
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
def test_app_import():
|
| 146 |
+
"""Test if the app can be imported"""
|
| 147 |
+
print("🔍 Testing app import...")
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
# Add current directory to path
|
| 151 |
+
sys.path.insert(0, '.')
|
| 152 |
+
|
| 153 |
+
# Try to import the app
|
| 154 |
+
import app
|
| 155 |
+
print("✅ App module imported successfully")
|
| 156 |
+
|
| 157 |
+
# Test if FastAPI app exists
|
| 158 |
+
if hasattr(app, 'app'):
|
| 159 |
+
print("✅ FastAPI app found")
|
| 160 |
+
else:
|
| 161 |
+
print("❌ FastAPI app not found")
|
| 162 |
+
return False
|
| 163 |
+
|
| 164 |
+
return True
|
| 165 |
+
|
| 166 |
+
except ImportError as e:
|
| 167 |
+
print(f"❌ Error importing app: {e}")
|
| 168 |
+
return False
|
| 169 |
+
except Exception as e:
|
| 170 |
+
print(f"❌ Unexpected error: {e}")
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
def test_environment():
|
| 174 |
+
"""Test environment variables"""
|
| 175 |
+
print("🔍 Testing environment...")
|
| 176 |
+
|
| 177 |
+
# Check if HUGGINGFACE_API_KEY is set
|
| 178 |
+
api_key = os.getenv('HUGGINGFACE_API_KEY')
|
| 179 |
+
if api_key:
|
| 180 |
+
print("✅ HUGGINGFACE_API_KEY is set")
|
| 181 |
+
else:
|
| 182 |
+
print("⚠️ HUGGINGFACE_API_KEY not set (will use mock responses)")
|
| 183 |
+
|
| 184 |
+
# Check Python version
|
| 185 |
+
python_version = sys.version_info
|
| 186 |
+
if python_version >= (3, 8):
|
| 187 |
+
print(f"✅ Python version {python_version.major}.{python_version.minor} is compatible")
|
| 188 |
+
else:
|
| 189 |
+
print(f"❌ Python version {python_version.major}.{python_version.minor} is too old (need 3.8+)")
|
| 190 |
+
return False
|
| 191 |
+
|
| 192 |
+
return True
|
| 193 |
+
|
| 194 |
+
def test_dependencies():
|
| 195 |
+
"""Test if required dependencies can be imported"""
|
| 196 |
+
print("🔍 Testing dependencies...")
|
| 197 |
+
|
| 198 |
+
required_modules = [
|
| 199 |
+
'fastapi',
|
| 200 |
+
'uvicorn',
|
| 201 |
+
'pydantic',
|
| 202 |
+
'requests',
|
| 203 |
+
'huggingface_hub'
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
missing_modules = []
|
| 207 |
+
|
| 208 |
+
for module in required_modules:
|
| 209 |
+
try:
|
| 210 |
+
__import__(module)
|
| 211 |
+
print(f"✅ {module}")
|
| 212 |
+
except ImportError:
|
| 213 |
+
missing_modules.append(module)
|
| 214 |
+
print(f"❌ {module}")
|
| 215 |
+
|
| 216 |
+
if missing_modules:
|
| 217 |
+
print(f"❌ Missing modules: {missing_modules}")
|
| 218 |
+
print("Install with: pip install " + " ".join(missing_modules))
|
| 219 |
+
return False
|
| 220 |
+
|
| 221 |
+
return True
|
| 222 |
+
|
| 223 |
+
def main():
|
| 224 |
+
"""Run all tests"""
|
| 225 |
+
print("🧪 Textilindo AI Assistant - Deployment Test")
|
| 226 |
+
print("=" * 50)
|
| 227 |
+
|
| 228 |
+
tests = [
|
| 229 |
+
test_file_structure,
|
| 230 |
+
test_data_files,
|
| 231 |
+
test_config_files,
|
| 232 |
+
test_environment,
|
| 233 |
+
test_dependencies,
|
| 234 |
+
test_app_import
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
passed = 0
|
| 238 |
+
total = len(tests)
|
| 239 |
+
|
| 240 |
+
for test in tests:
|
| 241 |
+
try:
|
| 242 |
+
if test():
|
| 243 |
+
passed += 1
|
| 244 |
+
print()
|
| 245 |
+
except Exception as e:
|
| 246 |
+
print(f"❌ Test failed with error: {e}")
|
| 247 |
+
print()
|
| 248 |
+
|
| 249 |
+
print("=" * 50)
|
| 250 |
+
print(f"📊 Test Results: {passed}/{total} tests passed")
|
| 251 |
+
|
| 252 |
+
if passed == total:
|
| 253 |
+
print("🎉 All tests passed! Your setup is ready for deployment.")
|
| 254 |
+
print("\n📋 Next steps:")
|
| 255 |
+
print("1. Run: ./deploy_to_hf_space.sh")
|
| 256 |
+
print("2. Or manually deploy to Hugging Face Spaces")
|
| 257 |
+
print("3. Set environment variables in your space settings")
|
| 258 |
+
print("4. Test your deployed application")
|
| 259 |
+
else:
|
| 260 |
+
print("❌ Some tests failed. Please fix the issues above before deploying.")
|
| 261 |
+
return 1
|
| 262 |
+
|
| 263 |
+
return 0
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
sys.exit(main())
|