Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
134
backend/app/agent/playbooks.py
Normal file
134
backend/app/agent/playbooks.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Per-device-type connection and read-only diagnostic playbooks.
|
||||
|
||||
Each entry maps a DeviceType to:
|
||||
- mcp_server: default MCP server that drives the device
|
||||
- connect: (tool_name, arg_builder) to establish a session (optional)
|
||||
- diagnostics: list of (tool_name, arg_builder) read-only calls to gather state
|
||||
|
||||
arg_builder is a callable(device_ctx) -> dict, where device_ctx contains
|
||||
address, port, username, secret.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from app.agent.asterisk_profiles import ASTERISK_PLAYBOOKS
|
||||
from app.agent.device_connect import pfsense_connect_args, ssh_connect_args
|
||||
from app.models.enums import DeviceType
|
||||
|
||||
ArgBuilder = Callable[[dict], dict]
|
||||
|
||||
|
||||
def _ip(ctx: dict) -> dict:
|
||||
return {"ip": ctx["address"]}
|
||||
|
||||
|
||||
def _pfsense_connect(ctx: dict) -> dict:
|
||||
return pfsense_connect_args(ctx)
|
||||
|
||||
|
||||
def _ssh(ctx: dict) -> dict:
|
||||
return ssh_connect_args(ctx)
|
||||
|
||||
|
||||
PLAYBOOKS: dict[DeviceType, dict] = {
|
||||
DeviceType.pfsense: {
|
||||
"mcp_server": "pfsense-mcp",
|
||||
"connect": ("pfsense_connect", _pfsense_connect),
|
||||
"diagnostics": [
|
||||
("pfsense_get_system_status", lambda c: {}),
|
||||
("pfsense_list_gateways", lambda c: {}),
|
||||
("pfsense_get_gateway_status", lambda c: {}),
|
||||
("pfsense_list_interfaces", lambda c: {}),
|
||||
],
|
||||
},
|
||||
DeviceType.proxmox: {
|
||||
# Connect + diagnostics handled by proxmox_profiles.run_proxmox_diagnostics (API → SSH).
|
||||
"mcp_server": "proxmox-mcp",
|
||||
"connect": None,
|
||||
"diagnostics": [],
|
||||
},
|
||||
DeviceType.asterisk: {
|
||||
# Legacy key — prefer asterisk_geneseasx catalog slot (see ASTERISK_PLAYBOOKS).
|
||||
"mcp_server": "ssh-generic-mcp",
|
||||
"connect": ("ssh_connect", _ssh),
|
||||
"diagnostics": [],
|
||||
},
|
||||
# GeneseasX / satbox variants are resolved by catalog_key in playbook_for().
|
||||
DeviceType.debian: {
|
||||
"mcp_server": "ssh-generic-mcp",
|
||||
"connect": (
|
||||
"ssh_connect",
|
||||
lambda c: ssh_connect_args(c),
|
||||
),
|
||||
"diagnostics": [
|
||||
("ssh_run", lambda c: {"command": "uptime && uname -a"}),
|
||||
("ssh_run", lambda c: {"command": "df -h"}),
|
||||
("ssh_run", lambda c: {"command": "free -m"}),
|
||||
("ssh_run", lambda c: {"command": "systemctl --failed --no-pager"}),
|
||||
("ssh_run", lambda c: {"command": "journalctl -p err -n 100 --no-pager"}),
|
||||
],
|
||||
},
|
||||
DeviceType.geneseasx: {
|
||||
"mcp_server": "ssh-generic-mcp",
|
||||
"connect": (
|
||||
"ssh_connect",
|
||||
lambda c: ssh_connect_args(c),
|
||||
),
|
||||
"diagnostics": [
|
||||
("ssh_run", lambda c: {"command": "uptime && uname -a"}),
|
||||
("ssh_run", lambda c: {"command": "docker ps --format '{{.Names}}: {{.Status}}'"}),
|
||||
("ssh_run", lambda c: {"command": "systemctl --failed --no-pager"}),
|
||||
],
|
||||
},
|
||||
DeviceType.fortigate: {
|
||||
"mcp_server": "fortigate-mcp",
|
||||
"connect": (
|
||||
"fortigate_connect",
|
||||
lambda c: {
|
||||
"host": c["address"],
|
||||
"api_key": c.get("secret"),
|
||||
"username": c.get("username"),
|
||||
},
|
||||
),
|
||||
"diagnostics": [
|
||||
("fortigate_get_system_status", lambda c: {}),
|
||||
("fortigate_list_interfaces", lambda c: {}),
|
||||
("fortigate_list_policies", lambda c: {}),
|
||||
("fortigate_get_logs", lambda c: {"lines": 100}),
|
||||
],
|
||||
},
|
||||
DeviceType.fortiswitch: {
|
||||
"mcp_server": "fortiswitch-mcp",
|
||||
"connect": (
|
||||
"fortiswitch_connect",
|
||||
lambda c: {
|
||||
"host": c["address"],
|
||||
"username": c.get("username"),
|
||||
"password": c.get("secret"),
|
||||
},
|
||||
),
|
||||
"diagnostics": [
|
||||
("fortiswitch_get_system_status", lambda c: {}),
|
||||
("fortiswitch_list_ports", lambda c: {}),
|
||||
("fortiswitch_get_port_stats", lambda c: {}),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def playbook_for(
|
||||
device_type: DeviceType,
|
||||
mcp_override: str | None,
|
||||
catalog_key: str | None = None,
|
||||
) -> dict | None:
|
||||
if catalog_key and catalog_key in ASTERISK_PLAYBOOKS:
|
||||
pb = ASTERISK_PLAYBOOKS[catalog_key]
|
||||
else:
|
||||
pb = PLAYBOOKS.get(device_type)
|
||||
if pb is None:
|
||||
return None
|
||||
# Catalog playbooks define the MCP server; ignore stale device-row overrides.
|
||||
if mcp_override and catalog_key not in ASTERISK_PLAYBOOKS and device_type != DeviceType.proxmox:
|
||||
pb = {**pb, "mcp_server": mcp_override}
|
||||
return pb
|
||||
Reference in New Issue
Block a user