|
|
|
|
|
"""
|
|
|
Dynamic Model Loader - Intelligent Model Detection & Registration
|
|
|
سیستم هوشمند بارگذاری و تشخیص مدلهای AI
|
|
|
|
|
|
Features:
|
|
|
- Auto-detect API type (HuggingFace, OpenAI, REST, GraphQL, etc.)
|
|
|
- Intelligent endpoint detection
|
|
|
- Automatic initialization
|
|
|
- Persistent storage in database
|
|
|
- Cross-page availability
|
|
|
"""
|
|
|
|
|
|
import httpx
|
|
|
import json
|
|
|
import re
|
|
|
import logging
|
|
|
from typing import Dict, Any, Optional, List
|
|
|
from datetime import datetime
|
|
|
import sqlite3
|
|
|
from pathlib import Path
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
class DynamicModelLoader:
|
|
|
"""
|
|
|
هوشمند: تشخیص خودکار نوع API و مدل
|
|
|
"""
|
|
|
|
|
|
def __init__(self, db_path: str = "data/dynamic_models.db"):
|
|
|
self.db_path = db_path
|
|
|
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
|
self.init_database()
|
|
|
|
|
|
|
|
|
self.api_patterns = {
|
|
|
'huggingface': [
|
|
|
r'huggingface\.co',
|
|
|
r'api-inference\.huggingface\.co',
|
|
|
r'hf\.co',
|
|
|
r'hf_[a-zA-Z0-9]+',
|
|
|
],
|
|
|
'openai': [
|
|
|
r'openai\.com',
|
|
|
r'api\.openai\.com',
|
|
|
r'sk-[a-zA-Z0-9]+',
|
|
|
],
|
|
|
'anthropic': [
|
|
|
r'anthropic\.com',
|
|
|
r'claude',
|
|
|
r'sk-ant-',
|
|
|
],
|
|
|
'rest': [
|
|
|
r'/api/v\d+/',
|
|
|
r'/rest/',
|
|
|
r'application/json',
|
|
|
],
|
|
|
'graphql': [
|
|
|
r'/graphql',
|
|
|
r'query.*\{',
|
|
|
r'mutation.*\{',
|
|
|
],
|
|
|
'websocket': [
|
|
|
r'ws://',
|
|
|
r'wss://',
|
|
|
]
|
|
|
}
|
|
|
|
|
|
def init_database(self):
|
|
|
"""ایجاد جداول دیتابیس"""
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
|
|
|
cursor.execute('''
|
|
|
CREATE TABLE IF NOT EXISTS dynamic_models (
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
model_id TEXT UNIQUE NOT NULL,
|
|
|
model_name TEXT,
|
|
|
api_type TEXT,
|
|
|
base_url TEXT,
|
|
|
api_key TEXT,
|
|
|
config JSON,
|
|
|
endpoints JSON,
|
|
|
is_active BOOLEAN DEFAULT 1,
|
|
|
auto_detected BOOLEAN DEFAULT 1,
|
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
last_used_at TIMESTAMP,
|
|
|
use_count INTEGER DEFAULT 0
|
|
|
)
|
|
|
''')
|
|
|
|
|
|
|
|
|
cursor.execute('''
|
|
|
CREATE TABLE IF NOT EXISTS model_usage_history (
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
model_id TEXT NOT NULL,
|
|
|
endpoint_used TEXT,
|
|
|
response_time_ms REAL,
|
|
|
success BOOLEAN,
|
|
|
error_message TEXT,
|
|
|
used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
FOREIGN KEY (model_id) REFERENCES dynamic_models(model_id)
|
|
|
)
|
|
|
''')
|
|
|
|
|
|
conn.commit()
|
|
|
conn.close()
|
|
|
logger.info(f"✅ Dynamic Models database initialized: {self.db_path}")
|
|
|
|
|
|
async def detect_api_type(self, config: Dict[str, Any]) -> str:
|
|
|
"""
|
|
|
تشخیص هوشمند نوع API
|
|
|
|
|
|
Args:
|
|
|
config: تنظیمات ورودی (url, key, headers, etc.)
|
|
|
|
|
|
Returns:
|
|
|
نوع API (huggingface, openai, rest, graphql, etc.)
|
|
|
"""
|
|
|
config_str = json.dumps(config).lower()
|
|
|
|
|
|
|
|
|
scores = {}
|
|
|
for api_type, patterns in self.api_patterns.items():
|
|
|
score = 0
|
|
|
for pattern in patterns:
|
|
|
if re.search(pattern, config_str, re.IGNORECASE):
|
|
|
score += 1
|
|
|
scores[api_type] = score
|
|
|
|
|
|
|
|
|
if max(scores.values()) > 0:
|
|
|
detected_type = max(scores, key=scores.get)
|
|
|
logger.info(f"🔍 Detected API type: {detected_type} (score: {scores[detected_type]})")
|
|
|
return detected_type
|
|
|
|
|
|
|
|
|
logger.info("🔍 No specific type detected, defaulting to REST")
|
|
|
return 'rest'
|
|
|
|
|
|
async def auto_discover_endpoints(self, base_url: str, api_key: Optional[str] = None) -> Dict[str, Any]:
|
|
|
"""
|
|
|
کشف خودکار endpoints
|
|
|
|
|
|
Args:
|
|
|
base_url: URL پایه
|
|
|
api_key: کلید API (اختیاری)
|
|
|
|
|
|
Returns:
|
|
|
لیست endpoints کشف شده
|
|
|
"""
|
|
|
discovered = {
|
|
|
'endpoints': [],
|
|
|
'methods': [],
|
|
|
'schemas': {}
|
|
|
}
|
|
|
|
|
|
|
|
|
common_paths = [
|
|
|
'',
|
|
|
'/docs',
|
|
|
'/openapi.json',
|
|
|
'/swagger.json',
|
|
|
'/api-docs',
|
|
|
'/health',
|
|
|
'/status',
|
|
|
'/models',
|
|
|
'/v1/models',
|
|
|
'/api/v1',
|
|
|
]
|
|
|
|
|
|
headers = {}
|
|
|
if api_key:
|
|
|
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
|
for path in common_paths:
|
|
|
try:
|
|
|
url = f"{base_url.rstrip('/')}{path}"
|
|
|
response = await client.get(url, headers=headers)
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
discovered['endpoints'].append({
|
|
|
'path': path,
|
|
|
'url': url,
|
|
|
'status': 200,
|
|
|
'content_type': response.headers.get('content-type', '')
|
|
|
})
|
|
|
|
|
|
|
|
|
if 'json' in response.headers.get('content-type', ''):
|
|
|
try:
|
|
|
data = response.json()
|
|
|
discovered['schemas'][path] = data
|
|
|
except:
|
|
|
pass
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.debug(f"Failed to discover {path}: {e}")
|
|
|
continue
|
|
|
|
|
|
logger.info(f"🔍 Discovered {len(discovered['endpoints'])} endpoints")
|
|
|
return discovered
|
|
|
|
|
|
async def test_model_connection(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
"""
|
|
|
تست اتصال به مدل
|
|
|
|
|
|
Args:
|
|
|
config: تنظیمات مدل
|
|
|
|
|
|
Returns:
|
|
|
نتیجه تست
|
|
|
"""
|
|
|
api_type = config.get('api_type', 'rest')
|
|
|
base_url = config.get('base_url', '')
|
|
|
api_key = config.get('api_key')
|
|
|
|
|
|
result = {
|
|
|
'success': False,
|
|
|
'api_type': api_type,
|
|
|
'response_time_ms': 0,
|
|
|
'error': None,
|
|
|
'detected_capabilities': []
|
|
|
}
|
|
|
|
|
|
start_time = datetime.now()
|
|
|
|
|
|
try:
|
|
|
|
|
|
if api_type == 'huggingface':
|
|
|
result = await self._test_huggingface(base_url, api_key)
|
|
|
elif api_type == 'openai':
|
|
|
result = await self._test_openai(base_url, api_key)
|
|
|
elif api_type == 'rest':
|
|
|
result = await self._test_rest(base_url, api_key)
|
|
|
elif api_type == 'graphql':
|
|
|
result = await self._test_graphql(base_url, api_key)
|
|
|
else:
|
|
|
result = await self._test_generic(base_url, api_key)
|
|
|
|
|
|
end_time = datetime.now()
|
|
|
result['response_time_ms'] = (end_time - start_time).total_seconds() * 1000
|
|
|
|
|
|
except Exception as e:
|
|
|
result['error'] = str(e)
|
|
|
logger.error(f"❌ Test failed: {e}")
|
|
|
|
|
|
return result
|
|
|
|
|
|
async def _test_huggingface(self, url: str, api_key: Optional[str]) -> Dict[str, Any]:
|
|
|
"""تست مدل HuggingFace"""
|
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
if api_key:
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
|
|
|
|
|
|
test_payload = {'inputs': 'Test'}
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
|
response = await client.post(url, headers=headers, json=test_payload)
|
|
|
|
|
|
return {
|
|
|
'success': response.status_code in [200, 503],
|
|
|
'status_code': response.status_code,
|
|
|
'detected_capabilities': ['text-classification', 'sentiment-analysis']
|
|
|
if response.status_code == 200 else ['loading']
|
|
|
}
|
|
|
|
|
|
async def _test_openai(self, url: str, api_key: Optional[str]) -> Dict[str, Any]:
|
|
|
"""تست API سازگار با OpenAI"""
|
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
if api_key:
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
|
|
|
|
|
|
test_payload = {
|
|
|
'model': 'gpt-3.5-turbo',
|
|
|
'messages': [{'role': 'user', 'content': 'Test'}],
|
|
|
'max_tokens': 5
|
|
|
}
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
|
response = await client.post(
|
|
|
f"{url.rstrip('/')}/v1/chat/completions",
|
|
|
headers=headers,
|
|
|
json=test_payload
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
'success': response.status_code == 200,
|
|
|
'status_code': response.status_code,
|
|
|
'detected_capabilities': ['chat', 'completion', 'embeddings']
|
|
|
}
|
|
|
|
|
|
async def _test_rest(self, url: str, api_key: Optional[str]) -> Dict[str, Any]:
|
|
|
"""تست REST API عمومی"""
|
|
|
headers = {}
|
|
|
if api_key:
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
|
response = await client.get(url, headers=headers)
|
|
|
|
|
|
return {
|
|
|
'success': response.status_code == 200,
|
|
|
'status_code': response.status_code,
|
|
|
'detected_capabilities': ['rest-api']
|
|
|
}
|
|
|
|
|
|
async def _test_graphql(self, url: str, api_key: Optional[str]) -> Dict[str, Any]:
|
|
|
"""تست GraphQL API"""
|
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
if api_key:
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
|
|
|
|
|
|
query = {'query': '{ __schema { types { name } } }'}
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
|
response = await client.post(url, headers=headers, json=query)
|
|
|
|
|
|
return {
|
|
|
'success': response.status_code == 200,
|
|
|
'status_code': response.status_code,
|
|
|
'detected_capabilities': ['graphql']
|
|
|
}
|
|
|
|
|
|
async def _test_generic(self, url: str, api_key: Optional[str]) -> Dict[str, Any]:
|
|
|
"""تست عمومی"""
|
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
|
response = await client.get(url)
|
|
|
|
|
|
return {
|
|
|
'success': response.status_code == 200,
|
|
|
'status_code': response.status_code,
|
|
|
'detected_capabilities': ['unknown']
|
|
|
}
|
|
|
|
|
|
async def register_model(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
"""
|
|
|
ثبت مدل جدید
|
|
|
|
|
|
Args:
|
|
|
config: {
|
|
|
'model_id': 'unique-id',
|
|
|
'model_name': 'My Model',
|
|
|
'base_url': 'https://...',
|
|
|
'api_key': 'xxx',
|
|
|
'api_type': 'huggingface' (optional, auto-detected),
|
|
|
'endpoints': {...} (optional, auto-discovered),
|
|
|
'custom_config': {...} (optional)
|
|
|
}
|
|
|
|
|
|
Returns:
|
|
|
نتیجه ثبت
|
|
|
"""
|
|
|
|
|
|
if 'api_type' not in config:
|
|
|
config['api_type'] = await self.detect_api_type(config)
|
|
|
|
|
|
|
|
|
if 'endpoints' not in config:
|
|
|
discovered = await self.auto_discover_endpoints(
|
|
|
config.get('base_url', ''),
|
|
|
config.get('api_key')
|
|
|
)
|
|
|
config['endpoints'] = discovered
|
|
|
|
|
|
|
|
|
test_result = await self.test_model_connection(config)
|
|
|
|
|
|
if not test_result['success']:
|
|
|
return {
|
|
|
'success': False,
|
|
|
'error': f"Connection test failed: {test_result.get('error', 'Unknown error')}",
|
|
|
'test_result': test_result
|
|
|
}
|
|
|
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
try:
|
|
|
cursor.execute('''
|
|
|
INSERT OR REPLACE INTO dynamic_models
|
|
|
(model_id, model_name, api_type, base_url, api_key, config, endpoints, auto_detected)
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
''', (
|
|
|
config.get('model_id'),
|
|
|
config.get('model_name'),
|
|
|
config.get('api_type'),
|
|
|
config.get('base_url'),
|
|
|
config.get('api_key'),
|
|
|
json.dumps(config.get('custom_config', {})),
|
|
|
json.dumps(config.get('endpoints', {})),
|
|
|
True
|
|
|
))
|
|
|
|
|
|
conn.commit()
|
|
|
|
|
|
logger.info(f"✅ Model registered: {config.get('model_id')}")
|
|
|
|
|
|
return {
|
|
|
'success': True,
|
|
|
'model_id': config.get('model_id'),
|
|
|
'api_type': config.get('api_type'),
|
|
|
'test_result': test_result,
|
|
|
'message': 'Model registered successfully'
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ Failed to register model: {e}")
|
|
|
return {
|
|
|
'success': False,
|
|
|
'error': str(e)
|
|
|
}
|
|
|
|
|
|
finally:
|
|
|
conn.close()
|
|
|
|
|
|
def get_all_models(self) -> List[Dict[str, Any]]:
|
|
|
"""دریافت همه مدلهای ثبت شده"""
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
conn.row_factory = sqlite3.Row
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
cursor.execute('''
|
|
|
SELECT * FROM dynamic_models
|
|
|
WHERE is_active = 1
|
|
|
ORDER BY use_count DESC, created_at DESC
|
|
|
''')
|
|
|
|
|
|
models = [dict(row) for row in cursor.fetchall()]
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
for model in models:
|
|
|
model['config'] = json.loads(model.get('config', '{}'))
|
|
|
model['endpoints'] = json.loads(model.get('endpoints', '{}'))
|
|
|
|
|
|
return models
|
|
|
|
|
|
def get_model(self, model_id: str) -> Optional[Dict[str, Any]]:
|
|
|
"""دریافت یک مدل خاص"""
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
conn.row_factory = sqlite3.Row
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
cursor.execute('''
|
|
|
SELECT * FROM dynamic_models
|
|
|
WHERE model_id = ? AND is_active = 1
|
|
|
''', (model_id,))
|
|
|
|
|
|
row = cursor.fetchone()
|
|
|
conn.close()
|
|
|
|
|
|
if row:
|
|
|
model = dict(row)
|
|
|
model['config'] = json.loads(model.get('config', '{}'))
|
|
|
model['endpoints'] = json.loads(model.get('endpoints', '{}'))
|
|
|
return model
|
|
|
|
|
|
return None
|
|
|
|
|
|
async def use_model(self, model_id: str, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
"""
|
|
|
استفاده از یک مدل ثبت شده
|
|
|
|
|
|
Args:
|
|
|
model_id: شناسه مدل
|
|
|
endpoint: endpoint مورد نظر
|
|
|
payload: دادههای ورودی
|
|
|
|
|
|
Returns:
|
|
|
خروجی مدل
|
|
|
"""
|
|
|
model = self.get_model(model_id)
|
|
|
|
|
|
if not model:
|
|
|
return {
|
|
|
'success': False,
|
|
|
'error': f'Model not found: {model_id}'
|
|
|
}
|
|
|
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
cursor = conn.cursor()
|
|
|
cursor.execute('''
|
|
|
UPDATE dynamic_models
|
|
|
SET use_count = use_count + 1, last_used_at = CURRENT_TIMESTAMP
|
|
|
WHERE model_id = ?
|
|
|
''', (model_id,))
|
|
|
conn.commit()
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
api_type = model['api_type']
|
|
|
base_url = model['base_url']
|
|
|
api_key = model['api_key']
|
|
|
|
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
if api_key:
|
|
|
if api_type == 'huggingface':
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
elif api_type == 'openai':
|
|
|
headers['Authorization'] = f'Bearer {api_key}'
|
|
|
else:
|
|
|
headers['Authorization'] = api_key
|
|
|
|
|
|
url = f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
|
|
|
|
|
start_time = datetime.now()
|
|
|
|
|
|
try:
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
|
response = await client.post(url, headers=headers, json=payload)
|
|
|
|
|
|
end_time = datetime.now()
|
|
|
response_time = (end_time - start_time).total_seconds() * 1000
|
|
|
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
cursor = conn.cursor()
|
|
|
cursor.execute('''
|
|
|
INSERT INTO model_usage_history
|
|
|
(model_id, endpoint_used, response_time_ms, success)
|
|
|
VALUES (?, ?, ?, ?)
|
|
|
''', (model_id, endpoint, response_time, response.status_code == 200))
|
|
|
conn.commit()
|
|
|
conn.close()
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
return {
|
|
|
'success': True,
|
|
|
'data': response.json(),
|
|
|
'response_time_ms': response_time
|
|
|
}
|
|
|
else:
|
|
|
return {
|
|
|
'success': False,
|
|
|
'error': f'HTTP {response.status_code}: {response.text[:200]}'
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ Model usage failed: {e}")
|
|
|
|
|
|
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
cursor = conn.cursor()
|
|
|
cursor.execute('''
|
|
|
INSERT INTO model_usage_history
|
|
|
(model_id, endpoint_used, success, error_message)
|
|
|
VALUES (?, ?, ?, ?)
|
|
|
''', (model_id, endpoint, False, str(e)))
|
|
|
conn.commit()
|
|
|
conn.close()
|
|
|
|
|
|
return {
|
|
|
'success': False,
|
|
|
'error': str(e)
|
|
|
}
|
|
|
|
|
|
def delete_model(self, model_id: str) -> bool:
|
|
|
"""حذف یک مدل"""
|
|
|
conn = sqlite3.connect(self.db_path)
|
|
|
cursor = conn.cursor()
|
|
|
|
|
|
cursor.execute('''
|
|
|
UPDATE dynamic_models
|
|
|
SET is_active = 0
|
|
|
WHERE model_id = ?
|
|
|
''', (model_id,))
|
|
|
|
|
|
conn.commit()
|
|
|
affected = cursor.rowcount
|
|
|
conn.close()
|
|
|
|
|
|
return affected > 0
|
|
|
|
|
|
|
|
|
|
|
|
dynamic_loader = DynamicModelLoader()
|
|
|
|
|
|
__all__ = ['DynamicModelLoader', 'dynamic_loader']
|
|
|
|
|
|
|