From b752e56b944e0f2387a01cfb7658ab536a513864 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 18 Aug 2025 13:45:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20AI=20=EC=84=9C=EB=B2=84=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20Phase=202=20=EA=B3=A0?= =?UTF-8?q?=EA=B8=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– ๋ชจ๋ธ ๊ด€๋ฆฌ ๊ณ ๋„ํ™”: - ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ: ์ธ๊ธฐ ๋ชจ๋ธ๋“ค ์›ํด๋ฆญ ์„ค์น˜ (llama, qwen, gemma, codellama, mistral) - ๋ชจ๋ธ ์‚ญ์ œ: ํ™•์ธ ๋ชจ๋‹ฌ๊ณผ ํ•จ๊ป˜ ์•ˆ์ „ํ•œ ์‚ญ์ œ ๊ธฐ๋Šฅ - ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ ๋ชฉ๋ก: ํƒœ๊ทธ๋ณ„ ๋ถ„๋ฅ˜ (chat, code, lightweight ๋“ฑ) - ๋ชจ๋ธ ์ƒ์„ธ ์ •๋ณด: ์„ค๋ช…, ํฌ๊ธฐ, ์šฉ๋„๋ณ„ ํƒœ๊ทธ ํ‘œ์‹œ ๏ฟฝ๏ฟฝ ์‹ค์‹œ๊ฐ„ ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง: - CPU/๋ฉ”๋ชจ๋ฆฌ/๋””์Šคํฌ/GPU ์‚ฌ์šฉ๋ฅ  ์›ํ˜• ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ” - ์ƒ‰์ƒ ์ฝ”๋”ฉ: ์‚ฌ์šฉ๋ฅ ์— ๋”ฐ๋ฅธ ์‹œ๊ฐ์  ๊ตฌ๋ถ„ (๋…น์ƒ‰/์ฃผํ™ฉ/๋นจ๊ฐ•) - ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ: 30์ดˆ๋งˆ๋‹ค ์ž๋™ ์ƒˆ๋กœ๊ณ ์นจ - ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ์ƒ์„ธ ์ •๋ณด (์ฝ”์–ด ์ˆ˜, ์šฉ๋Ÿ‰, ์˜จ๋„ ๋“ฑ) ๐ŸŽจ ๊ณ ๊ธ‰ UI/UX: - ๋ชจ๋‹ฌ ์ฐฝ: ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ๋ธ”๋Ÿฌ ํšจ๊ณผ - ์›ํ˜• ํ”„๋กœ๊ทธ๋ ˆ์Šค๋ฐ”: CSS ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ - ๋ฐ˜์‘ํ˜• ๋””์ž์ธ: ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™” - ํƒœ๊ทธ ์‹œ์Šคํ…œ: ๋ชจ๋ธ ๋ถ„๋ฅ˜ ๋ฐ ์‹œ๊ฐํ™” ๐Ÿ”ง ์ƒˆ API ์—”๋“œํฌ์ธํŠธ: - POST /admin/models/download - ๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ - DELETE /admin/models/{model_name} - ๋ชจ๋ธ ์‚ญ์ œ - GET /admin/models/available - ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ ๋ชฉ๋ก - GET /admin/system/stats - ์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋ฅ  ์ˆ˜์ •๋œ ํŒŒ์ผ: - server/main.py: Phase 2 API ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ - test_admin.py: ํ…Œ์ŠคํŠธ ๋ชจ๋“œ Phase 2 ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - templates/admin.html: ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง ์„น์…˜, ๋ชจ๋‹ฌ ์ฐฝ ์ถ”๊ฐ€ - static/admin.css: ๋ชจ๋‹ˆํ„ฐ๋ง ์ฐจํŠธ, ๋ชจ๋‹ฌ ์Šคํƒ€์ผ ์ถ”๊ฐ€ - static/admin.js: Phase 2 ๊ธฐ๋Šฅ JavaScript ๊ตฌํ˜„ --- server/main.py | 151 ++++++++++++++++++++++++ static/admin.css | 274 +++++++++++++++++++++++++++++++++++++++++++ static/admin.js | 168 ++++++++++++++++++++++++++ templates/admin.html | 111 ++++++++++++++++++ test_admin.py | 110 +++++++++++++++++ 5 files changed, 814 insertions(+) diff --git a/server/main.py b/server/main.py index 046150f..f0a2a03 100644 --- a/server/main.py +++ b/server/main.py @@ -595,3 +595,154 @@ async def admin_delete_api_key(key_id: str, api_key: str = Depends(require_api_k else: raise HTTPException(status_code=404, detail="API key not found") + +# Phase 2: Advanced Model Management +@app.post("/admin/models/download") +async def admin_download_model(request: dict, api_key: str = Depends(require_api_key)): + """๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ""" + model_name = request.get("model") + if not model_name: + raise HTTPException(status_code=400, detail="Model name is required") + + try: + # Ollama pull ๋ช…๋ น ์‹คํ–‰ + result = await ollama.pull_model(model_name) + return { + "success": True, + "message": f"Model '{model_name}' download started", + "details": result + } + except Exception as e: + return { + "success": False, + "error": f"Failed to download model: {str(e)}" + } + + +@app.delete("/admin/models/{model_name}") +async def admin_delete_model(model_name: str, api_key: str = Depends(require_api_key)): + """๋ชจ๋ธ ์‚ญ์ œ""" + try: + # Ollama ๋ชจ๋ธ ์‚ญ์ œ + result = await ollama.delete_model(model_name) + return { + "success": True, + "message": f"Model '{model_name}' deleted successfully", + "details": result + } + except Exception as e: + return { + "success": False, + "error": f"Failed to delete model: {str(e)}" + } + + +@app.get("/admin/models/available") +async def admin_get_available_models(api_key: str = Depends(require_api_key)): + """๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ ๋ชฉ๋ก""" + # ์ธ๊ธฐ ์žˆ๋Š” ๋ชจ๋ธ๋“ค ๋ชฉ๋ก (์‹ค์ œ๋กœ๋Š” Ollama ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ ๊ฐ€์ ธ์™€์•ผ ํ•จ) + available_models = [ + { + "name": "llama3.2:1b", + "description": "Meta์˜ Llama 3.2 1B ๋ชจ๋ธ - ๊ฐ€๋ฒผ์šด ์ž‘์—…์šฉ", + "size": "1.3GB", + "tags": ["chat", "lightweight"] + }, + { + "name": "llama3.2:3b", + "description": "Meta์˜ Llama 3.2 3B ๋ชจ๋ธ - ๊ท ํ˜•์žกํžŒ ์„ฑ๋Šฅ", + "size": "2.0GB", + "tags": ["chat", "recommended"] + }, + { + "name": "qwen2.5:7b", + "description": "Alibaba์˜ Qwen 2.5 7B ๋ชจ๋ธ - ๋‹ค๊ตญ์–ด ์ง€์›", + "size": "4.1GB", + "tags": ["chat", "multilingual"] + }, + { + "name": "gemma2:2b", + "description": "Google์˜ Gemma 2 2B ๋ชจ๋ธ - ํšจ์œจ์ ์ธ ์ถ”๋ก ", + "size": "1.6GB", + "tags": ["chat", "efficient"] + }, + { + "name": "codellama:7b", + "description": "Meta์˜ Code Llama 7B - ์ฝ”๋“œ ์ƒ์„ฑ ํŠนํ™”", + "size": "3.8GB", + "tags": ["code", "programming"] + }, + { + "name": "mistral:7b", + "description": "Mistral AI์˜ 7B ๋ชจ๋ธ - ๊ณ ์„ฑ๋Šฅ ์ถ”๋ก ", + "size": "4.1GB", + "tags": ["chat", "performance"] + } + ] + + return {"available_models": available_models} + + +# Phase 2: System Monitoring +@app.get("/admin/system/stats") +async def admin_get_system_stats(api_key: str = Depends(require_api_key)): + """์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋ฅ  ์กฐํšŒ""" + import psutil + import GPUtil + + try: + # CPU ์‚ฌ์šฉ๋ฅ  + cpu_percent = psutil.cpu_percent(interval=1) + cpu_count = psutil.cpu_count() + + # ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ  + memory = psutil.virtual_memory() + memory_percent = memory.percent + memory_used = memory.used // (1024**3) # GB + memory_total = memory.total // (1024**3) # GB + + # ๋””์Šคํฌ ์‚ฌ์šฉ๋ฅ  + disk = psutil.disk_usage('/') + disk_percent = (disk.used / disk.total) * 100 + disk_used = disk.used // (1024**3) # GB + disk_total = disk.total // (1024**3) # GB + + # GPU ์‚ฌ์šฉ๋ฅ  (NVIDIA GPU๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) + gpu_stats = [] + try: + gpus = GPUtil.getGPUs() + for gpu in gpus: + gpu_stats.append({ + "name": gpu.name, + "load": gpu.load * 100, + "memory_used": gpu.memoryUsed, + "memory_total": gpu.memoryTotal, + "temperature": gpu.temperature + }) + except: + gpu_stats = [] + + return { + "cpu": { + "usage_percent": cpu_percent, + "core_count": cpu_count + }, + "memory": { + "usage_percent": memory_percent, + "used_gb": memory_used, + "total_gb": memory_total + }, + "disk": { + "usage_percent": disk_percent, + "used_gb": disk_used, + "total_gb": disk_total + }, + "gpu": gpu_stats, + "timestamp": datetime.now().isoformat() + } + except Exception as e: + return { + "error": f"Failed to get system stats: {str(e)}", + "timestamp": datetime.now().isoformat() + } + diff --git a/static/admin.css b/static/admin.css index bfb3840..9e71ee3 100644 --- a/static/admin.css +++ b/static/admin.css @@ -330,6 +330,255 @@ body { color: #f39c12; } +/* Phase 2: System Monitoring Styles */ +.monitoring-container { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 4px 6px rgba(0,0,0,0.07); + border: 1px solid #e1e8ed; +} + +.monitoring-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; +} + +.monitor-card { + background: #f8f9fa; + border-radius: 8px; + padding: 1.5rem; + text-align: center; + border: 1px solid #e9ecef; +} + +.monitor-card .card-header { + margin-bottom: 1rem; +} + +.progress-circle { + position: relative; + width: 80px; + height: 80px; + margin: 0 auto 1rem; + border-radius: 50%; + background: conic-gradient(#667eea 0deg, #e9ecef 0deg); + display: flex; + align-items: center; + justify-content: center; +} + +.progress-circle::before { + content: ''; + position: absolute; + width: 60px; + height: 60px; + border-radius: 50%; + background: white; +} + +.progress-text { + position: relative; + z-index: 1; + font-size: 0.9rem; + font-weight: 600; + color: #2c3e50; +} + +.monitor-details { + font-size: 0.8rem; + color: #7f8c8d; +} + +/* Progress circle colors */ +.progress-circle.low { + background: conic-gradient(#27ae60 var(--progress, 0deg), #e9ecef var(--progress, 0deg)); +} + +.progress-circle.medium { + background: conic-gradient(#f39c12 var(--progress, 0deg), #e9ecef var(--progress, 0deg)); +} + +.progress-circle.high { + background: conic-gradient(#e74c3c var(--progress, 0deg), #e9ecef var(--progress, 0deg)); +} + +/* Modal Styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + backdrop-filter: blur(4px); +} + +.modal-content { + background-color: white; + margin: 5% auto; + padding: 0; + border-radius: 12px; + width: 90%; + max-width: 600px; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); + animation: modalSlideIn 0.3s ease; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(-50px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1.5rem; + border-bottom: 1px solid #e1e8ed; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 12px 12px 0 0; +} + +.modal-header h3 { + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.close-btn { + background: none; + border: none; + font-size: 1.5rem; + color: white; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.close-btn:hover { + background-color: rgba(255,255,255,0.2); +} + +.modal-body { + padding: 1.5rem; +} + +.modal-actions { + display: flex; + gap: 1rem; + justify-content: flex-end; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid #e1e8ed; +} + +/* Available Models List */ +.available-model-item { + background: #f8f9fa; + border: 1px solid #e1e8ed; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s ease; +} + +.available-model-item:hover { + border-color: #667eea; +} + +.model-info { + flex: 1; +} + +.model-name { + font-weight: 600; + color: #2c3e50; + margin-bottom: 0.3rem; +} + +.model-description { + font-size: 0.9rem; + color: #7f8c8d; + margin-bottom: 0.5rem; +} + +.model-tags { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.model-tag { + background: #667eea; + color: white; + padding: 0.2rem 0.5rem; + border-radius: 12px; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.model-tag.code { + background: #e74c3c; +} + +.model-tag.lightweight { + background: #27ae60; +} + +.model-tag.recommended { + background: #f39c12; +} + +.model-size { + font-size: 0.9rem; + color: #95a5a6; + margin-top: 0.5rem; +} + +.model-delete-info { + background: #fadbd8; + border: 1px solid #f1948a; + border-radius: 8px; + padding: 1rem; + margin: 1rem 0; +} + +.model-delete-info strong { + color: #e74c3c; +} + +/* Enhanced Models Table */ +.models-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; +} + /* Responsive */ @media (max-width: 768px) { .admin-main { @@ -341,6 +590,11 @@ body { gap: 1rem; } + .monitoring-grid { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + } + .header-content { flex-direction: column; gap: 1rem; @@ -351,6 +605,11 @@ body { overflow-x: auto; } + .models-header { + flex-direction: column; + align-items: stretch; + } + .api-key-item { flex-direction: column; align-items: flex-start; @@ -361,4 +620,19 @@ body { width: 100%; justify-content: flex-end; } + + .available-model-item { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .modal-content { + width: 95%; + margin: 2% auto; + } + + .modal-actions { + flex-direction: column; + } } diff --git a/static/admin.js b/static/admin.js index fef9025..e345b1f 100644 --- a/static/admin.js +++ b/static/admin.js @@ -30,11 +30,13 @@ class AdminDashboard { await this.loadSystemStatus(); await this.loadModels(); await this.loadApiKeys(); + await this.loadSystemStats(); // Phase 2 // Auto-refresh every 30 seconds setInterval(() => { this.loadSystemStatus(); this.loadModels(); + this.loadSystemStats(); // Phase 2 }, 30000); } @@ -144,6 +146,9 @@ class AdminDashboard { + `).join(''); @@ -254,6 +259,161 @@ class AdminDashboard { alert(`Failed to delete API key: ${error.message}`); } } + + // Phase 2: System Monitoring + async loadSystemStats() { + try { + const response = await this.apiRequest('/admin/system/stats'); + + // Update CPU + this.updateProgressCircle('cpu-progress', response.cpu.usage_percent); + document.getElementById('cpu-text').textContent = `${response.cpu.usage_percent}%`; + document.getElementById('cpu-cores').textContent = `${response.cpu.core_count} cores`; + + // Update Memory + this.updateProgressCircle('memory-progress', response.memory.usage_percent); + document.getElementById('memory-text').textContent = `${response.memory.usage_percent}%`; + document.getElementById('memory-details').textContent = + `${response.memory.used_gb} / ${response.memory.total_gb} GB`; + + // Update Disk + this.updateProgressCircle('disk-progress', response.disk.usage_percent); + document.getElementById('disk-text').textContent = `${response.disk.usage_percent}%`; + document.getElementById('disk-details').textContent = + `${response.disk.used_gb} / ${response.disk.total_gb} GB`; + + // Update GPU + if (response.gpu && response.gpu.length > 0) { + const gpu = response.gpu[0]; + this.updateProgressCircle('gpu-progress', gpu.load); + document.getElementById('gpu-text').textContent = `${gpu.load}%`; + document.getElementById('gpu-details').textContent = + `${gpu.name} - ${gpu.temperature}ยฐC`; + } else { + document.getElementById('gpu-text').textContent = '--'; + document.getElementById('gpu-details').textContent = 'No GPU detected'; + } + + } catch (error) { + console.error('Failed to load system stats:', error); + } + } + + updateProgressCircle(elementId, percentage) { + const element = document.getElementById(elementId); + const degrees = (percentage / 100) * 360; + + // Remove existing color classes + element.classList.remove('low', 'medium', 'high'); + + // Add appropriate color class + if (percentage < 50) { + element.classList.add('low'); + } else if (percentage < 80) { + element.classList.add('medium'); + } else { + element.classList.add('high'); + } + + // Update CSS custom property for progress + element.style.setProperty('--progress', `${degrees}deg`); + } + + // Phase 2: Model Download + async openModelDownload() { + try { + const response = await this.apiRequest('/admin/models/available'); + const models = response.available_models || []; + + const container = document.getElementById('available-models-list'); + + if (models.length === 0) { + container.innerHTML = '
No models available
'; + } else { + container.innerHTML = models.map(model => ` +
+
+
${model.name}
+
${model.description}
+
+ ${model.tags.map(tag => `${tag}`).join('')} +
+
Size: ${model.size}
+
+ +
+ `).join(''); + } + + this.openModal('model-download-modal'); + + } catch (error) { + console.error('Failed to load available models:', error); + alert('Failed to load available models'); + } + } + + async downloadModel(modelName) { + try { + const response = await this.apiRequest('/admin/models/download', { + method: 'POST', + body: JSON.stringify({ model: modelName }) + }); + + if (response.success) { + alert(`Download started: ${response.message}`); + this.closeModal('model-download-modal'); + // Refresh models list after a short delay + setTimeout(() => this.loadModels(), 2000); + } else { + alert(`Download failed: ${response.error}`); + } + + } catch (error) { + alert(`Download failed: ${error.message}`); + } + } + + // Phase 2: Model Delete + confirmDeleteModel(modelName) { + document.getElementById('delete-model-name').textContent = modelName; + + // Set up delete confirmation + const confirmBtn = document.getElementById('confirm-delete-btn'); + confirmBtn.onclick = () => this.deleteModel(modelName); + + this.openModal('model-delete-modal'); + } + + async deleteModel(modelName) { + try { + const response = await this.apiRequest(`/admin/models/${modelName}`, { + method: 'DELETE' + }); + + if (response.success) { + alert(`Model deleted: ${response.message}`); + this.closeModal('model-delete-modal'); + await this.loadModels(); + } else { + alert(`Delete failed: ${response.error}`); + } + + } catch (error) { + alert(`Delete failed: ${error.message}`); + } + } + + // Modal management + openModal(modalId) { + document.getElementById(modalId).style.display = 'block'; + } + + closeModal(modalId) { + document.getElementById(modalId).style.display = 'none'; + } } // Global functions for HTML onclick handlers @@ -267,6 +427,14 @@ function generateApiKey() { admin.generateApiKey(); } +function openModelDownload() { + admin.openModelDownload(); +} + +function closeModal(modalId) { + admin.closeModal(modalId); +} + // Initialize dashboard when page loads document.addEventListener('DOMContentLoaded', () => { admin = new AdminDashboard(); diff --git a/templates/admin.html b/templates/admin.html index a06430b..066d855 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -82,6 +82,9 @@ +
@@ -104,6 +107,74 @@ + +
+

System Monitoring

+
+
+
+
+ +

CPU Usage

+
+
+
+ -- +
+
+ -- cores +
+
+
+ +
+
+ +

Memory Usage

+
+
+
+ -- +
+
+ -- / -- GB +
+
+
+ +
+
+ +

Disk Usage

+
+
+
+ -- +
+
+ -- / -- GB +
+
+
+ +
+
+ +

GPU Status

+
+
+
+ -- +
+
+ No GPU detected +
+
+
+
+
+
+

API Key Management

@@ -121,6 +192,46 @@ + + + + + + diff --git a/test_admin.py b/test_admin.py index 4fe1df4..26d0436 100644 --- a/test_admin.py +++ b/test_admin.py @@ -191,6 +191,116 @@ async def admin_delete_api_key(key_id: str, api_key: str = Depends(require_api_k raise HTTPException(status_code=404, detail="API key not found") +# Phase 2: Advanced Model Management (Test Mode) +@app.post("/admin/models/download") +async def admin_download_model(request: dict, api_key: str = Depends(require_api_key)): + """๋ชจ๋ธ ๋‹ค์šด๋กœ๋“œ (ํ…Œ์ŠคํŠธ ๋ชจ๋“œ)""" + model_name = request.get("model") + if not model_name: + raise HTTPException(status_code=400, detail="Model name is required") + + # ํ…Œ์ŠคํŠธ ๋ชจ๋“œ์—์„œ๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + return { + "success": True, + "message": f"Test mode: Model '{model_name}' download simulation started", + "details": f"In real mode, this would download {model_name} from Ollama registry" + } + + +@app.delete("/admin/models/{model_name}") +async def admin_delete_model(model_name: str, api_key: str = Depends(require_api_key)): + """๋ชจ๋ธ ์‚ญ์ œ (ํ…Œ์ŠคํŠธ ๋ชจ๋“œ)""" + # ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ์—์„œ ๋ชจ๋ธ ์ œ๊ฑฐ + global test_models + test_models = [m for m in test_models if m["name"] != model_name] + + return { + "success": True, + "message": f"Test mode: Model '{model_name}' deleted from test data", + "details": f"In real mode, this would delete {model_name} from Ollama" + } + + +@app.get("/admin/models/available") +async def admin_get_available_models(api_key: str = Depends(require_api_key)): + """๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅํ•œ ๋ชจ๋ธ ๋ชฉ๋ก""" + available_models = [ + { + "name": "llama3.2:1b", + "description": "Meta์˜ Llama 3.2 1B ๋ชจ๋ธ - ๊ฐ€๋ฒผ์šด ์ž‘์—…์šฉ", + "size": "1.3GB", + "tags": ["chat", "lightweight"] + }, + { + "name": "llama3.2:3b", + "description": "Meta์˜ Llama 3.2 3B ๋ชจ๋ธ - ๊ท ํ˜•์žกํžŒ ์„ฑ๋Šฅ", + "size": "2.0GB", + "tags": ["chat", "recommended"] + }, + { + "name": "qwen2.5:7b", + "description": "Alibaba์˜ Qwen 2.5 7B ๋ชจ๋ธ - ๋‹ค๊ตญ์–ด ์ง€์›", + "size": "4.1GB", + "tags": ["chat", "multilingual"] + }, + { + "name": "gemma2:2b", + "description": "Google์˜ Gemma 2 2B ๋ชจ๋ธ - ํšจ์œจ์ ์ธ ์ถ”๋ก ", + "size": "1.6GB", + "tags": ["chat", "efficient"] + }, + { + "name": "codellama:7b", + "description": "Meta์˜ Code Llama 7B - ์ฝ”๋“œ ์ƒ์„ฑ ํŠนํ™”", + "size": "3.8GB", + "tags": ["code", "programming"] + }, + { + "name": "mistral:7b", + "description": "Mistral AI์˜ 7B ๋ชจ๋ธ - ๊ณ ์„ฑ๋Šฅ ์ถ”๋ก ", + "size": "4.1GB", + "tags": ["chat", "performance"] + } + ] + + return {"available_models": available_models} + + +# Phase 2: System Monitoring (Test Mode) +@app.get("/admin/system/stats") +async def admin_get_system_stats(api_key: str = Depends(require_api_key)): + """์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋ฅ  ์กฐํšŒ (ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ)""" + import random + + # ํ…Œ์ŠคํŠธ์šฉ ๋žœ๋ค ๋ฐ์ดํ„ฐ ์ƒ์„ฑ + return { + "cpu": { + "usage_percent": round(random.uniform(10, 80), 1), + "core_count": 8 + }, + "memory": { + "usage_percent": round(random.uniform(30, 90), 1), + "used_gb": round(random.uniform(4, 12), 1), + "total_gb": 16 + }, + "disk": { + "usage_percent": round(random.uniform(20, 70), 1), + "used_gb": round(random.uniform(50, 200), 1), + "total_gb": 500 + }, + "gpu": [ + { + "name": "Test GPU (Simulated)", + "load": round(random.uniform(0, 100), 1), + "memory_used": round(random.uniform(1000, 8000)), + "memory_total": 8192, + "temperature": round(random.uniform(45, 75)) + } + ], + "timestamp": datetime.now().isoformat() + } + + if __name__ == "__main__": print("๐Ÿš€ AI Server Admin Dashboard (Test Mode)") print(f"๐Ÿ“ Server: http://localhost:{TEST_SERVER_PORT}")