1"""Source-adapter catalog + module config + app-config endpoints. 2 3* ``GET /api/sources`` — every registered adapter, with module + usage. 4* ``POST /api/sources/test`` — instantiate an adapter and call ``.fetch()``. 5* ``GET /api/modules`` — every loaded DataModule + its adapters. 6* ``PUT /api/modules/{name}`` — save module config back to default.yaml. 7* ``GET /api/config`` — read the live app config (with secrets masked). 8* ``PUT /api/config/llm`` — save the llm section back to default.yaml. 9""" 10 11from __future__ import annotations 12 13import logging 14from pathlib import Path 15from typing import Any 16 17import yaml 18from fastapi import APIRouter, HTTPException 19from pydantic import BaseModel 20 21from maf.dashboard import state 22 23logger = logging.getLogger(__name__) 24 25router = APIRouter() 26 27 28class SourceTestRequest(BaseModel): 29 adapter: str 30 params: dict[str, Any] = {} 31 32 33class UpdateModuleRequest(BaseModel): 34 enabled: bool = True 35 config: dict[str, Any] = {} 36 37 38class UpdateConfigRequest(BaseModel): 39 config: dict[str, Any] 40 41 42@router.get("/api/sources") 43async def list_sources() -> list[dict[str, Any]]: 44 """List all available source adapters grouped by module.""" 45 from maf.sources.registry import _ADAPTER_REGISTRY 46 47 maf_app = state.get_maf_app() 48 module_map: dict[str, str] = {} 49 if maf_app: 50 for module in maf_app.modules: 51 for adapter_name in module.get_adapters(): 52 module_map[adapter_name] = module.module_name 53 54 arena_usage: dict[str, list[str]] = {} 55 if maf_app: 56 for arena_name in maf_app.list_arenas(): 57 arena = maf_app.get_arena(arena_name) 58 if arena: 59 for src in arena.config.sources: 60 arena_usage.setdefault(src.adapter, []).append(arena_name) 61 62 return [ 63 { 64 "name": name, 65 "class": cls.__name__, 66 "module": module_map.get(name, "built-in"), 67 "used_in_arenas": arena_usage.get(name, []), 68 } 69 for name, cls in _ADAPTER_REGISTRY.items() 70 ] 71 72 73@router.post("/api/sources/test") 74async def test_source(req: SourceTestRequest) -> dict[str, Any]: 75 """Test-fetch from a source adapter with given params.""" 76 from maf.sources.registry import _ADAPTER_REGISTRY 77 78 adapter_cls = _ADAPTER_REGISTRY.get(req.adapter) 79 if not adapter_cls: 80 raise HTTPException(404, f"Adapter {req.adapter!r} not found") 81 82 try: 83 instance = adapter_cls(req.params) 84 result = await instance.fetch(req.params) 85 result_str = str(result) 86 return { 87 "adapter": req.adapter, 88 "status": "error" if result.get("error") else "ok", 89 "result": result, 90 "result_keys": list(result.keys()), 91 "truncated": len(result_str) > 5000, 92 } 93 except Exception as exc: 94 return { 95 "adapter": req.adapter, 96 "status": "error", 97 "error": str(exc), 98 "result": {}, 99 "result_keys": [], 100 } 101 102 103@router.get("/api/modules") 104async def list_modules() -> list[dict[str, Any]]: 105 """List all loaded data modules and their adapters.""" 106 maf_app = state.get_maf_app() 107 if not maf_app: 108 return [] 109 110 result = [] 111 for module in maf_app.modules: 112 adapters = module.get_adapters() 113 result.append({ 114 "name": module.module_name, 115 "class": module.__class__.__name__, 116 "initialized": module._initialized, 117 "config": module.config, 118 "adapters": list(adapters.keys()), 119 "adapter_count": len(adapters), 120 }) 121 return result 122 123 124@router.put("/api/modules/{name}") 125async def update_module_config(name: str, req: UpdateModuleRequest) -> dict[str, Any]: 126 """Update module configuration and write to default.yaml.""" 127 state.require_maf_app() 128 129 config_path = Path("config/default.yaml") 130 if not config_path.exists(): 131 raise HTTPException(404, "Default config not found") 132 133 try: 134 with open(config_path) as f: 135 raw = yaml.safe_load(f) 136 137 modules = raw.get("modules", []) 138 found = False 139 for m in modules: 140 if m.get("name") == name: 141 m["enabled"] = req.enabled 142 m["config"] = req.config 143 found = True 144 break 145 if not found: 146 modules.append({"name": name, "enabled": req.enabled, "config": req.config}) 147 raw["modules"] = modules 148 149 with open(config_path, "w") as f: 150 yaml.dump(raw, f, default_flow_style=False, sort_keys=False) 151 152 return {"status": "ok", "message": f"Module {name} config saved. Restart to apply."} 153 except Exception as exc: 154 raise HTTPException(500, f"Failed to save module config: {exc}") 155 156 157@router.get("/api/config") 158async def get_app_config() -> dict[str, Any]: 159 """Get the full app config (with secrets masked).""" 160 maf_app = state.get_maf_app() 161 if not maf_app: 162 return {} 163 164 config = maf_app.config.model_dump(exclude_none=True) 165 for provider_cfg in config.get("llm", {}).get("providers", {}).values(): 166 if "api_key" in provider_cfg and provider_cfg["api_key"]: 167 provider_cfg["api_key"] = provider_cfg["api_key"][:8] + "..." 168 return config 169 170 171@router.put("/api/config/llm") 172async def update_llm_config(req: UpdateConfigRequest) -> dict[str, Any]: 173 """Update LLM configuration in default.yaml.""" 174 config_path = Path("config/default.yaml") 175 if not config_path.exists(): 176 raise HTTPException(404, "Default config not found") 177 178 try: 179 with open(config_path) as f: 180 raw = yaml.safe_load(f) 181 182 raw["llm"] = req.config 183 184 with open(config_path, "w") as f: 185 yaml.dump(raw, f, default_flow_style=False, sort_keys=False) 186 187 return {"status": "ok", "message": "LLM config saved. Restart to apply."} 188 except Exception as exc: 189 raise HTTPException(500, f"Failed to save LLM config: {exc}")