mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 07:24:34 +01:00
524 lines
19 KiB
Python
524 lines
19 KiB
Python
"""
|
|
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 with status field
|
|
name = module_info["name"]
|
|
is_loaded = module_info["loaded"] # Module is actually loaded in memory
|
|
is_enabled = module_info["enabled"] # Module is enabled in config
|
|
|
|
# Determine status based on enabled + loaded state
|
|
if is_enabled and is_loaded:
|
|
status = "running"
|
|
elif is_enabled and not is_loaded:
|
|
status = "error" # Enabled but failed to load
|
|
else: # not is_enabled (regardless of loaded state)
|
|
status = "standby" # Disabled
|
|
|
|
api_module = {
|
|
"name": name,
|
|
"version": module_info["version"],
|
|
"description": module_info["description"],
|
|
"initialized": is_loaded,
|
|
"enabled": is_enabled,
|
|
"status": status # Add status field for frontend compatibility
|
|
}
|
|
|
|
# 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 comprehensive module status - CONSOLIDATED endpoint"""
|
|
log_api_request("get_modules_status", {})
|
|
|
|
# Get all discovered modules including disabled ones
|
|
all_modules = module_manager.list_all_modules()
|
|
|
|
modules_with_status = []
|
|
running_count = 0
|
|
standby_count = 0
|
|
failed_count = 0
|
|
|
|
for module_info in all_modules:
|
|
name = module_info["name"]
|
|
is_loaded = module_info["loaded"] # Module is actually loaded in memory
|
|
is_enabled = module_info["enabled"] # Module is enabled in config
|
|
|
|
# Determine status based on enabled + loaded state
|
|
if is_enabled and is_loaded:
|
|
status = "running"
|
|
running_count += 1
|
|
elif is_enabled and not is_loaded:
|
|
status = "failed" # Enabled but failed to load
|
|
failed_count += 1
|
|
else: # not is_enabled (regardless of loaded state)
|
|
status = "standby" # Disabled
|
|
standby_count += 1
|
|
|
|
# Get module statistics if available and loaded
|
|
stats = {}
|
|
if is_loaded and name in module_manager.modules:
|
|
module_instance = module_manager.modules[name]
|
|
if hasattr(module_instance, "get_stats"):
|
|
try:
|
|
import asyncio
|
|
if asyncio.iscoroutinefunction(module_instance.get_stats):
|
|
stats_result = await module_instance.get_stats()
|
|
else:
|
|
stats_result = module_instance.get_stats()
|
|
stats = stats_result.__dict__ if hasattr(stats_result, "__dict__") else stats_result
|
|
except:
|
|
stats = {}
|
|
|
|
modules_with_status.append({
|
|
"name": name,
|
|
"version": module_info["version"],
|
|
"description": module_info["description"],
|
|
"status": status,
|
|
"enabled": is_enabled,
|
|
"loaded": is_loaded,
|
|
"stats": stats
|
|
})
|
|
|
|
return {
|
|
"modules": modules_with_status,
|
|
"total": len(modules_with_status),
|
|
"running": running_count,
|
|
"standby": standby_count,
|
|
"failed": failed_count,
|
|
"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.llm.service import llm_service
|
|
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 LLM service
|
|
models_data = await llm_service.get_models()
|
|
model_ids = [model.id for model in models_data]
|
|
|
|
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)) |