Activity
Mon
Wed
Fri
Sun
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
Jan
Feb
Mar
What is this?
Less
More

Owned by Eduard

Autonomous MSP

15 members β€’ Free

AI-powered NOC, SOC and compliance for MSPs and IT consultancies. Built by a 25-year enterprise network practitioner.

Memberships

SalesBooster

14 members β€’ Free

Skoolers

192k members β€’ Free

The Enterprise AI

655 members β€’ Free

AI Automation (A-Z)

151k members β€’ Free

15 contributions to Autonomous MSP
Build Your First AI Agent from Scratch
Hey everyone πŸ‘‹ I just published the full course we've been building toward β€” Build Your First AI Agent from Scratch. 8 modules. 8 Colab notebooks. Completely free. This is not a theory course. Every module builds one piece of a real system: - Module 1 β€” why your Ansible scripts break and agents don't - Module 2 β€” your first agent in plain Python, no API key needed - Module 3 β€” SSH tools, REST calls, database queries β€” the agent's hands - Module 4 β€” ChromaDB memory so your agent remembers past incidents - Module 5 β€” LangGraph state machine (think OSPF FSM, but for reasoning) - Module 6 β€” MCP, RAG, topology context - Module 7 β€” production safety, audit logs, observability - Module 8 β€” capstone: full OSPF troubleshooting agent, 25-minute diagnosis in 90 seconds The whole thing is designed for network engineers, not data scientists. Every example uses OSPF, BGP, Netmiko and NOC scenarios you already know. πŸ‘‰ https://netsec-academy.vexpertai.com/ No login. No paywall. Just go. If you finish Module 8 and run the capstone notebook β€” post your run summary here. I want to see your confidence scores.
1
0
Module 03 Β· Lesson 3 β€” The Safety Gate
All Tool Calls Go Through One Function. No Exceptions. You do not call tool.execute() directly from the agent loop. You call execute_with_approval(). Always. This function is the single chokepoint where the three access levels are enforced and every action is logged. The agent loop does not need to know whether a tool is READ, WRITE, or ADMIN β€” the gate handles that. --- The Audit Log First, the logging function. Every action β€” approved or rejected β€” gets a record. import json import datetime AUDIT_LOG_PATH = "audit.jsonl" def _log_action(tool_name: str, params: dict, approved: bool, operator: str): entry = { "timestamp": datetime.datetime.utcnow().isoformat() + "Z", "tool": tool_name, "params": params, "approved": approved, "operator": operator, } with open(AUDIT_LOG_PATH, "a") as f: f.write(json.dumps(entry) + "\n") JSONL format β€” one JSON object per line. Easy to ship to a SIEM. Easy to search with grep or jq. Each line is self-contained. Think of this as your debug ip ospf events equivalent β€” a timestamped trail of everything the agent did or tried to do. --- The Full Safety Gate def execute_with_approval(tool: BaseTool, params: dict, operator: str = "unknown") -> ToolResult: """The safety gate. Every tool call goes through here.""" if tool.category == READ: result = tool.execute(**params) _log_action(tool.name, params, approved=True, operator=operator) return result if tool.category == WRITE: print(f"\n{'='*55}") print(" WRITE OPERATION REQUESTED") print(f"{'='*55}") print(f" Tool : {tool.name}") print(f" Params: {json.dumps(params, indent=4)}") if "diff" in params: print(f"\n Config diff:\n{params['diff']}") print(f"{'='*55}") answer = input(" Approve? (y/n): ").strip().lower() approved = (answer == "y") _log_action(tool.name, params, approved=approved, operator=operator) if not approved: return ToolResult(success=False, data={}, error="Operator rejected.")
0
0
Module 3 Β· Lesson 1 β€” Tool Design Philosophy
The LLM Does Not Touch Your Network Before anything else, get this right in your head: the language model cannot execute code. It cannot SSH into a router. It cannot run a Python function. What it can do is produce text β€” and that text, if you structure it correctly, looks like a tool call request. Your Python code reads that request. Your Python code decides whether to honor it. Your Python code runs the actual SSH session. That separation is not a design choice you can skip. It is where every safety control lives. --- Three Access Levels. No Exceptions. Think of these like privilege levels on a Cisco router. Every tool you build belongs to exactly one of three levels: - READ β€” show commands, lookups, pings. Runs automatically. Cannot change anything. - WRITE β€” config changes, interface bounces. Agent proposes, you type y to approve. - ADMIN β€” high-impact operations. You must type YES I CONFIRM exactly. You are running this agent against client infrastructure. The access level is not a suggestion. Here is how you define them in Python β€” plain string constants, nothing fancy: READ = "read" # privilege 1 β€” show commands only, auto-execute WRITE = "write" # privilege 7 β€” config changes, needs y/n approval ADMIN = "admin" # privilege 15 β€” destructive ops, needs YES I CONFIRM --- ToolResult: What Every Tool Returns Every tool returns exactly the same shape. This is important β€” the agent loop always knows what it is getting back. class ToolResult: """ Every tool returns this. Always the same three fields: success β€” True or False data β€” what came back, e.g. {"raw_output": "..."} error β€” error message if success is False, empty string otherwise """ def __init__(self, success: bool, data: dict, error: str = ""): self.success = success self.data = data self.error = error Usage: result = ToolResult(success=True, data={"output": "neighbor FULL"}) print(result.success) # True print(result.data) # {"output": "neighbor FULL"}
0
0
Module 2 Β· Lesson 3 β€” Run It: Your First Diagnosis
Read time: ~6 min | Hands-on Here is the complete agent. No framework. No abstraction. Pure Python and the Anthropic SDK. Every line does exactly one thing. You will understand all of it. --- simple_agent.py import re import json import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env # ─ Demo tools ─ def show_ospf_neighbors(device: str) -> str: data = { "R1": "Gi0/1: neighbor 10.0.0.2 State INIT Dead 34\n" "Gi0/2: neighbor 10.0.0.6 State FULL Dead 38", "R2": "Gi0/0: neighbor 10.0.0.1 State INIT Dead 31", } return data.get(device, f"Device {device} not found") def show_ospf_interface(device: str, interface: str) -> str: data = { ("R1", "Gi0/1"): "Area 0, Hello 10, Dead 40, MTU 1500, auth: none", ("R2", "Gi0/0"): "Area 1, Hello 10, Dead 40, MTU 1500, auth: none", } return data.get((device, interface), f"Interface {interface} not found on {device}") def ping_device(device: str, target: str) -> str: return f"{device} -> {target}: Success rate 0 percent (0/5)" TOOLS = { "show_ospf_neighbors": { "function": show_ospf_neighbors, "description": "Get OSPF neighbor table from a router", "params": {"device": "Device hostname (string)"}, }, "show_ospf_interface": { "function": show_ospf_interface, "description": "Get OSPF config for a specific interface", "params": {"device": "Device hostname", "interface": "Interface name"}, }, "ping_device": { "function": ping_device, "description": "Ping a target IP from a source device", "params": {"device": "Source device hostname", "target": "Target IP address"}, }, } # ── System prompt ─── SYSTEM_PROMPT = """You are a network troubleshooting agent for an MSP. For every problem, follow this exact format: Thought: [your reasoning about what to check next] Action: [tool_name] Params: {"param": "value"} OR when you have a diagnosis: Final Answer: [root cause and recommended fix] Available tools: {tools_description} Rules: - Always start with a Thought.
0
0
Module 2 Β· Lesson 2 β€” Tools and the Observation Loop
Module 2 Β· Lesson 2 | Tools and the Observation Loop πŸ“Œ Read time: ~8 min | Module 2 of 8 Tools and the Observation Loop The agent thinks. Python acts. You connect them. In Lesson 1 you saw the ReAct loop: Thought β†’ Action β†’ Observation β†’ Thought. This lesson zooms in on the middle part β€” the Action. Specifically: what is a tool, how does the agent choose which one to call, and how does the result come back into the loop? By the end you will have a working tool registry and a parser you can drop straight into the agent. What is a tool? Forget the word 'tool' for a second. A tool is just a Python function. But there is one extra ingredient: a plain-English description that the LLM reads. That description is how the agent knows the function exists and what it does. Without it, the LLM cannot see the function at all. So a tool = Python function + description. Nothing else. No special SDK, no framework magic. Networking analogy: think of a tool like an SNMP OID. The OID is the address of a piece of data. The description is the MIB entry β€” it explains what that OID gives you. You register it once. The agent decides when to poll it. The description has exactly one job: help the agent self-select the right tool. Write it the way you would label a show command for a junior engineer β€” specific and unambiguous. Vague description β†’ agent calls the wrong tool. Precise description β†’ agent calls the right one. The TOOLS registry β€” your toolbox All your tools live in one Python dict called TOOLS. Each entry has three things: β€’ "function" β€” the actual Python function to run β€’ "description" β€” what the LLM reads to decide whether to pick this tool β€’ "params" β€” what arguments the function needs, explained in plain English Here is the registry with two tools: # ── The functions (demo data β€” replace with Netmiko in production) ── def show_ospf_neighbors(device: str) -> str: # In production: SSH to device, run 'show ip ospf neighbor' return f"{device} Gi0/1: neighbor 10.0.0.2 State INIT Dead 34"
0
0
1-10 of 15
Eduard Dulharu
1
2points to level up
@eduard-dulharu-8225
CTO and Founder at vExpertAI

Active 3h ago
Joined Mar 12, 2026