mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 07:24:34 +01:00
clean commit
This commit is contained in:
478
backend/app/api/v1/modules.py
Normal file
478
backend/app/api/v1/modules.py
Normal file
@@ -0,0 +1,478 @@
|
||||
"""
|
||||
Modules API endpoints
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from app.services.module_manager import module_manager, ModuleConfig
|
||||
from app.core.logging import log_api_request
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_modules():
|
||||
"""Get list of all discovered modules with their status (enabled and disabled)"""
|
||||
log_api_request("list_modules", {})
|
||||
|
||||
# Get all discovered modules including disabled ones
|
||||
all_modules = module_manager.list_all_modules()
|
||||
|
||||
modules = []
|
||||
for module_info in all_modules:
|
||||
# Convert module_info to API format
|
||||
api_module = {
|
||||
"name": module_info["name"],
|
||||
"version": module_info["version"],
|
||||
"description": module_info["description"],
|
||||
"initialized": module_info["loaded"],
|
||||
"enabled": module_info["enabled"]
|
||||
}
|
||||
|
||||
# Get module statistics if available and module is loaded
|
||||
if module_info["loaded"] and module_info["name"] in module_manager.modules:
|
||||
module_instance = module_manager.modules[module_info["name"]]
|
||||
if hasattr(module_instance, "get_stats"):
|
||||
try:
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(module_instance.get_stats):
|
||||
stats = await module_instance.get_stats()
|
||||
else:
|
||||
stats = module_instance.get_stats()
|
||||
api_module["stats"] = stats.__dict__ if hasattr(stats, "__dict__") else stats
|
||||
except:
|
||||
api_module["stats"] = {}
|
||||
|
||||
modules.append(api_module)
|
||||
|
||||
# Calculate stats
|
||||
loaded_count = sum(1 for m in modules if m["initialized"] and m["enabled"])
|
||||
|
||||
return {
|
||||
"total": len(modules),
|
||||
"modules": modules,
|
||||
"module_count": loaded_count,
|
||||
"initialized": module_manager.initialized
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_modules_status():
|
||||
"""Get summary status of all modules"""
|
||||
log_api_request("get_modules_status", {})
|
||||
|
||||
total_modules = len(module_manager.modules)
|
||||
running_modules = 0
|
||||
standby_modules = 0
|
||||
failed_modules = 0
|
||||
|
||||
for name, module in module_manager.modules.items():
|
||||
config = module_manager.module_configs.get(name)
|
||||
is_initialized = getattr(module, "initialized", False)
|
||||
is_enabled = config.enabled if config else True
|
||||
|
||||
if is_initialized and is_enabled:
|
||||
running_modules += 1
|
||||
elif not is_initialized:
|
||||
failed_modules += 1
|
||||
else:
|
||||
standby_modules += 1
|
||||
|
||||
return {
|
||||
"total": total_modules,
|
||||
"running": running_modules,
|
||||
"standby": standby_modules,
|
||||
"failed": failed_modules,
|
||||
"system_initialized": module_manager.initialized
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{module_name}")
|
||||
async def get_module_info(module_name: str):
|
||||
"""Get detailed information about a specific module"""
|
||||
log_api_request("get_module_info", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.modules:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
module = module_manager.modules[module_name]
|
||||
module_info = {
|
||||
"name": module_name,
|
||||
"version": getattr(module, "version", "1.0.0"),
|
||||
"description": getattr(module, "description", ""),
|
||||
"initialized": getattr(module, "initialized", False),
|
||||
"enabled": module_manager.module_configs.get(module_name, ModuleConfig(module_name)).enabled,
|
||||
"capabilities": []
|
||||
}
|
||||
|
||||
# Get module capabilities
|
||||
if hasattr(module, "get_module_info"):
|
||||
try:
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(module.get_module_info):
|
||||
info = await module.get_module_info()
|
||||
else:
|
||||
info = module.get_module_info()
|
||||
module_info.update(info)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Get module statistics
|
||||
if hasattr(module, "get_stats"):
|
||||
try:
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(module.get_stats):
|
||||
stats = await module.get_stats()
|
||||
else:
|
||||
stats = module.get_stats()
|
||||
module_info["stats"] = stats.__dict__ if hasattr(stats, "__dict__") else stats
|
||||
except:
|
||||
module_info["stats"] = {}
|
||||
|
||||
# List available methods
|
||||
methods = []
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if callable(attr) and not attr_name.startswith("_"):
|
||||
methods.append(attr_name)
|
||||
module_info["methods"] = methods
|
||||
|
||||
return module_info
|
||||
|
||||
|
||||
@router.post("/{module_name}/enable")
|
||||
async def enable_module(module_name: str):
|
||||
"""Enable a module"""
|
||||
log_api_request("enable_module", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.module_configs:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
# Enable the module in config
|
||||
config = module_manager.module_configs[module_name]
|
||||
config.enabled = True
|
||||
|
||||
# Load the module if not already loaded
|
||||
if module_name not in module_manager.modules:
|
||||
try:
|
||||
await module_manager._load_module(module_name, config)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to enable module '{module_name}': {str(e)}")
|
||||
|
||||
return {
|
||||
"message": f"Module '{module_name}' enabled successfully",
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{module_name}/disable")
|
||||
async def disable_module(module_name: str):
|
||||
"""Disable a module"""
|
||||
log_api_request("disable_module", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.module_configs:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
# Disable the module in config
|
||||
config = module_manager.module_configs[module_name]
|
||||
config.enabled = False
|
||||
|
||||
# Unload the module if loaded
|
||||
if module_name in module_manager.modules:
|
||||
try:
|
||||
await module_manager.unload_module(module_name)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to disable module '{module_name}': {str(e)}")
|
||||
|
||||
return {
|
||||
"message": f"Module '{module_name}' disabled successfully",
|
||||
"enabled": False
|
||||
}
|
||||
|
||||
|
||||
@router.post("/all/reload")
|
||||
async def reload_all_modules():
|
||||
"""Reload all modules"""
|
||||
log_api_request("reload_all_modules", {})
|
||||
|
||||
results = {}
|
||||
failed_modules = []
|
||||
|
||||
for module_name in list(module_manager.modules.keys()):
|
||||
try:
|
||||
success = await module_manager.reload_module(module_name)
|
||||
results[module_name] = {"success": success, "error": None}
|
||||
if not success:
|
||||
failed_modules.append(module_name)
|
||||
except Exception as e:
|
||||
results[module_name] = {"success": False, "error": str(e)}
|
||||
failed_modules.append(module_name)
|
||||
|
||||
if failed_modules:
|
||||
return {
|
||||
"message": f"Reloaded {len(results) - len(failed_modules)}/{len(results)} modules successfully",
|
||||
"success": False,
|
||||
"results": results,
|
||||
"failed_modules": failed_modules
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": f"All {len(results)} modules reloaded successfully",
|
||||
"success": True,
|
||||
"results": results,
|
||||
"failed_modules": []
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{module_name}/reload")
|
||||
async def reload_module(module_name: str):
|
||||
"""Reload a specific module"""
|
||||
log_api_request("reload_module", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.modules:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
success = await module_manager.reload_module(module_name)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to reload module '{module_name}'")
|
||||
|
||||
return {
|
||||
"message": f"Module '{module_name}' reloaded successfully",
|
||||
"reloaded": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{module_name}/restart")
|
||||
async def restart_module(module_name: str):
|
||||
"""Restart a specific module (alias for reload)"""
|
||||
log_api_request("restart_module", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.modules:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
success = await module_manager.reload_module(module_name)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to restart module '{module_name}'")
|
||||
|
||||
return {
|
||||
"message": f"Module '{module_name}' restarted successfully",
|
||||
"restarted": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{module_name}/start")
|
||||
async def start_module(module_name: str):
|
||||
"""Start a specific module (enable and load)"""
|
||||
log_api_request("start_module", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.module_configs:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
# Enable the module
|
||||
config = module_manager.module_configs[module_name]
|
||||
config.enabled = True
|
||||
|
||||
# Load the module if not already loaded
|
||||
if module_name not in module_manager.modules:
|
||||
await module_manager._load_module(module_name, config)
|
||||
|
||||
return {
|
||||
"message": f"Module '{module_name}' started successfully",
|
||||
"started": True
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{module_name}/stop")
|
||||
async def stop_module(module_name: str):
|
||||
"""Stop a specific module (disable and unload)"""
|
||||
log_api_request("stop_module", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.module_configs:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
# Disable the module
|
||||
config = module_manager.module_configs[module_name]
|
||||
config.enabled = False
|
||||
|
||||
# Unload the module if loaded
|
||||
if module_name in module_manager.modules:
|
||||
await module_manager.unload_module(module_name)
|
||||
|
||||
return {
|
||||
"message": f"Module '{module_name}' stopped successfully",
|
||||
"stopped": True
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{module_name}/stats")
|
||||
async def get_module_stats(module_name: str):
|
||||
"""Get module statistics"""
|
||||
log_api_request("get_module_stats", {"module_name": module_name})
|
||||
|
||||
if module_name not in module_manager.modules:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
module = module_manager.modules[module_name]
|
||||
|
||||
if not hasattr(module, "get_stats"):
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' does not provide statistics")
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(module.get_stats):
|
||||
stats = await module.get_stats()
|
||||
else:
|
||||
stats = module.get_stats()
|
||||
return {
|
||||
"module": module_name,
|
||||
"stats": stats.__dict__ if hasattr(stats, "__dict__") else stats
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get statistics: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{module_name}/execute")
|
||||
async def execute_module_action(module_name: str, request_data: Dict[str, Any]):
|
||||
"""Execute a module action through the interceptor pattern"""
|
||||
log_api_request("execute_module_action", {"module_name": module_name, "action": request_data.get("action")})
|
||||
|
||||
if module_name not in module_manager.modules:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
module = module_manager.modules[module_name]
|
||||
|
||||
# Check if module supports the new interceptor pattern
|
||||
if hasattr(module, 'execute_with_interceptors'):
|
||||
try:
|
||||
# Prepare context (would normally come from authentication middleware)
|
||||
context = {
|
||||
"user_id": "test_user", # Would come from authentication
|
||||
"api_key_id": "test_api_key", # Would come from API key auth
|
||||
"ip_address": "127.0.0.1", # Would come from request
|
||||
"user_permissions": [f"modules:{module_name}:*"] # Would come from user/API key permissions
|
||||
}
|
||||
|
||||
# Execute through interceptor chain
|
||||
response = await module.execute_with_interceptors(request_data, context)
|
||||
|
||||
return {
|
||||
"module": module_name,
|
||||
"success": True,
|
||||
"response": response,
|
||||
"interceptor_pattern": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Module execution failed: {str(e)}")
|
||||
|
||||
# Fallback for legacy modules
|
||||
else:
|
||||
action = request_data.get("action", "execute")
|
||||
|
||||
if hasattr(module, action):
|
||||
try:
|
||||
method = getattr(module, action)
|
||||
if callable(method):
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(method):
|
||||
response = await method(request_data)
|
||||
else:
|
||||
response = method(request_data)
|
||||
|
||||
return {
|
||||
"module": module_name,
|
||||
"success": True,
|
||||
"response": response,
|
||||
"interceptor_pattern": False
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"'{action}' is not callable")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Module execution failed: {str(e)}")
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Action '{action}' not supported by module '{module_name}'")
|
||||
|
||||
|
||||
@router.get("/{module_name}/config")
|
||||
async def get_module_config(module_name: str):
|
||||
"""Get module configuration schema and current values"""
|
||||
log_api_request("get_module_config", {"module_name": module_name})
|
||||
|
||||
from app.services.module_config_manager import module_config_manager
|
||||
from app.services.litellm_client import litellm_client
|
||||
import copy
|
||||
|
||||
# Get module manifest and schema
|
||||
manifest = module_config_manager.get_module_manifest(module_name)
|
||||
if not manifest:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
schema = module_config_manager.get_module_schema(module_name)
|
||||
current_config = module_config_manager.get_module_config(module_name)
|
||||
|
||||
# For Signal module, populate model options dynamically
|
||||
if module_name == "signal" and schema:
|
||||
try:
|
||||
# Get available models from LiteLLM
|
||||
models_data = await litellm_client.get_models()
|
||||
model_ids = [model.get("id", model.get("model", "")) for model in models_data if model.get("id") or model.get("model")]
|
||||
|
||||
if model_ids:
|
||||
# Create a copy of the schema to avoid modifying the original
|
||||
dynamic_schema = copy.deepcopy(schema)
|
||||
|
||||
# Add enum options for the model field
|
||||
if "properties" in dynamic_schema and "model" in dynamic_schema["properties"]:
|
||||
dynamic_schema["properties"]["model"]["enum"] = model_ids
|
||||
# Set a sensible default if the current default isn't in the list
|
||||
current_default = dynamic_schema["properties"]["model"].get("default", "gpt-3.5-turbo")
|
||||
if current_default not in model_ids and model_ids:
|
||||
dynamic_schema["properties"]["model"]["default"] = model_ids[0]
|
||||
|
||||
schema = dynamic_schema
|
||||
|
||||
except Exception as e:
|
||||
# If we can't get models, log warning but continue with original schema
|
||||
logger.warning(f"Failed to get dynamic models for Signal config: {e}")
|
||||
|
||||
return {
|
||||
"module": module_name,
|
||||
"description": manifest.description,
|
||||
"schema": schema,
|
||||
"current_config": current_config,
|
||||
"has_schema": schema is not None
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{module_name}/config")
|
||||
async def update_module_config(module_name: str, config: dict):
|
||||
"""Update module configuration"""
|
||||
log_api_request("update_module_config", {"module_name": module_name})
|
||||
|
||||
from app.services.module_config_manager import module_config_manager
|
||||
|
||||
# Validate module exists
|
||||
manifest = module_config_manager.get_module_manifest(module_name)
|
||||
if not manifest:
|
||||
raise HTTPException(status_code=404, detail=f"Module '{module_name}' not found")
|
||||
|
||||
try:
|
||||
# Save configuration
|
||||
success = await module_config_manager.save_module_config(module_name, config)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to save configuration")
|
||||
|
||||
# Update module manager with new config
|
||||
success = await module_manager.update_module_config(module_name, config)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to apply configuration")
|
||||
|
||||
return {
|
||||
"message": f"Configuration updated for module '{module_name}'",
|
||||
"config": config
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
Reference in New Issue
Block a user