refactor: split tool declarations into domain files + role config UI
tools/__init__.py shrinks from 1,137 → 250 lines. Each domain file now owns both its callables and its FunctionDeclarations (DECLARATIONS list), so adding a new tool only touches one file. New TOOL_CATEGORIES dict exported from __init__ — used by the UI for grouped tool checkboxes. Role config UI (Settings → Model Registry → Role Assignments): - ⚙ button per role expands an inline configure panel - Textarea for system_append (injected into system prompt for this role) - Grouped checkboxes for tool allow-list (all checked = no restriction) - POST /api/models/role-config saves both fields; updates ROLE_CONFIG_DATA in-page so re-open reflects current state without a page reload Backend: - model_registry.set_role_config() writes system_append + tools to registry - TOOL_CATEGORIES exported from tools/__init__ for UI rendering - TOOLS.md header updated: 30 → 39 tools (ae_journal_* and cortex_* additions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from auth_utils import COOKIE_NAME, decode_token
|
||||
from config import settings as app_settings
|
||||
import model_registry as reg
|
||||
from tools import TOOL_CATEGORIES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -285,13 +286,42 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
||||
f'data-slot="{slot}" title="{slot_label}">\n{model_opts}\n</select>'
|
||||
)
|
||||
role_rows += f'<div class="role-slot"><span class="slot-label">{slot_label}</span>{sel}</div>'
|
||||
role_rows += '</div></div>'
|
||||
role_rows += (
|
||||
f'</div>'
|
||||
f'<button class="role-cfg-btn" data-role="{role}" title="Configure persona and tools">⚙</button>'
|
||||
f'</div>'
|
||||
f'<div class="role-config-panel" id="rcp-{role}">'
|
||||
f'<div class="rcp-field">'
|
||||
f'<label class="rcp-label">System prompt addition</label>'
|
||||
f'<textarea class="rcp-textarea" data-role="{role}" rows="3" '
|
||||
f'placeholder="Extra instructions injected into the system prompt when this role is active…"></textarea>'
|
||||
f'</div>'
|
||||
f'<div class="rcp-field">'
|
||||
f'<label class="rcp-label">Tool allow-list '
|
||||
f'<span class="rcp-hint">— all checked means no restriction (use all accessible tools)</span></label>'
|
||||
f'<div class="rcp-tools" id="rcp-tools-{role}"></div>'
|
||||
f'</div>'
|
||||
f'<div class="rcp-actions">'
|
||||
f'<button class="btn btn-primary btn-sm rcp-save" data-role="{role}">Save</button>'
|
||||
f'<button class="btn btn-secondary btn-sm rcp-cancel" data-role="{role}">Cancel</button>'
|
||||
f'</div>'
|
||||
f'</div>'
|
||||
)
|
||||
|
||||
role_data_js = _json.dumps({
|
||||
role: {slot: (roles.get(role, {}).get(slot) or "") for slot in reg.PRIORITY_KEYS[:3]}
|
||||
for role in app_settings.get_defined_roles()
|
||||
})
|
||||
|
||||
role_config_data_js = _json.dumps({
|
||||
role: {
|
||||
"system_append": roles.get(role, {}).get("system_append", ""),
|
||||
"tools": roles.get(role, {}).get("tools") or None,
|
||||
}
|
||||
for role in app_settings.get_defined_roles()
|
||||
})
|
||||
tool_categories_js = _json.dumps(TOOL_CATEGORIES)
|
||||
|
||||
# ── Catalog data + Google accounts for JS ─────────────────────────────────
|
||||
google_accounts_js = _json.dumps(reg.get_google_accounts(username))
|
||||
google_catalog_js = _json.dumps(reg.get_catalog("google"))
|
||||
@@ -305,8 +335,10 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
||||
"{{ host_rows }}": host_rows,
|
||||
"{{ model_rows }}": model_rows,
|
||||
"{{ host_options }}": host_options,
|
||||
"{{ role_rows }}": role_rows,
|
||||
"{{ role_data_js }}": role_data_js,
|
||||
"{{ role_rows }}": role_rows,
|
||||
"{{ role_data_js }}": role_data_js,
|
||||
"{{ role_config_data_js }}": role_config_data_js,
|
||||
"{{ tool_categories_js }}": tool_categories_js,
|
||||
"{{ google_accounts_js }}": google_accounts_js,
|
||||
"{{ google_catalog_js }}": google_catalog_js,
|
||||
"{{ anthropic_catalog_js }}": anthropic_catalog_js,
|
||||
@@ -510,6 +542,36 @@ async def set_role(request: Request) -> JSONResponse:
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@router.post("/api/models/role-config")
|
||||
async def set_role_config(request: Request) -> JSONResponse:
|
||||
"""AJAX: save system_append and tool allow-list for a role.
|
||||
|
||||
Body: {"role": "coder", "system_append": "...", "tools": ["web_search", ...] | null}
|
||||
tools=null clears the allow-list (role uses all accessible tools).
|
||||
"""
|
||||
username = _get_user(request)
|
||||
if not username:
|
||||
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
||||
|
||||
role = body.get("role", "").strip()
|
||||
system_append = body.get("system_append", "")
|
||||
tools = body.get("tools") # list[str] or None
|
||||
|
||||
if not role:
|
||||
return JSONResponse({"error": "role is required"}, status_code=400)
|
||||
if tools is not None and not isinstance(tools, list):
|
||||
return JSONResponse({"error": "tools must be a list or null"}, status_code=400)
|
||||
|
||||
reg.set_role_config(username, role, system_append, tools)
|
||||
logger.info("role config saved: %s %s (tools=%s)", username, role,
|
||||
len(tools) if tools is not None else "all")
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
@router.get("/api/local-llm/fetch-models")
|
||||
async def fetch_models(request: Request, host_id: str = "") -> JSONResponse:
|
||||
"""Proxy to the host's models endpoint. host_id selects which host."""
|
||||
|
||||
Reference in New Issue
Block a user