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.
- Never give a Final Answer before using at least one tool.
- Be specific and actionable.
"""
# ── Parser ───
def parse_response(text: str) -> dict:
if "Final Answer:" in text:
return {"type": "final", "answer": text.split("Final Answer:")[-1].strip()}
tool_match = re.search(r"Action:\s*(\w+)", text)
params_match = re.search(r"Params:\s*(\{.*?\})", text, re.DOTALL)
if not tool_match:
return {"type": "error", "message": "No action found"}
params = json.loads(params_match.group(1)) if params_match else {}
return {"type": "action", "tool": tool_match.group(1).strip(), "params": params}
# ── Main loop ───
def run_agent(problem: str, max_iterations: int = 8) -> str:
tools_desc = "\n".join(
f"- {n}: {i['description']} | params: {i['params']}"
for n, i in TOOLS.items()
)
messages = [{"role": "user", "content": problem}]
print(f"\n[Agent] Problem: {problem}\n{'─'*60}")
for iteration in range(max_iterations):
print(f"\n[Iteration {iteration + 1}]")
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=SYSTEM_PROMPT.format(tools_description=tools_desc),
messages=messages,
)
raw = response.content[0].text
print(raw)
messages.append({"role": "assistant", "content": raw})
parsed = parse_response(raw)
if parsed["type"] == "final":
print(f"\n{'─'*60}\n[Agent] Done.\n")
return parsed["answer"]
if parsed["type"] == "error" or parsed["tool"] not in TOOLS:
return f"Agent error: {parsed.get('message', 'unknown tool')}"
result = TOOLS[parsed["tool"]]["function"](**parsed["params"])
print(f"\n[Tool Result] {result}")
messages.append({"role": "user", "content": f"Observation: {result}"})
return "Max iterations reached."
# ── Entry point ────
if __name__ == "__main__":
answer = run_agent(
"R1 is showing an OSPF neighbor stuck in INIT state. "
"Diagnose the root cause and recommend a fix."
)
print(f"[Final Answer]\n{answer}")
---
How to run it
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..." # Windows: set ANTHROPIC_API_KEY=sk-ant-...
python simple_agent.py
That is the only dependency. The script runs the loop, prints each iteration, and exits with the final diagnosis.
---
What you will see
[Agent] Problem: R1 is showing an OSPF neighbor stuck in INIT state...
[Iteration 1]
Thought: INIT state means one-way Hello exchange. I'll check the
neighbor table on R1 first to confirm which interface.
Action: show_ospf_neighbors
Params: {"device": "R1"}
[Tool Result] Gi0/1: neighbor 10.0.0.2 State INIT Dead 34 ...
[Iteration 2]
Thought: INIT confirmed on Gi0/1. Check OSPF interface config.
Action: show_ospf_interface
Params: {"device": "R1", "interface": "Gi0/1"}
[Tool Result] Area 0, Hello 10, Dead 40, MTU 1500, auth: none
[Iteration 3]
Thought: R1 is Area 0. Check R2's side of the same segment.
Action: show_ospf_interface
Params: {"device": "R2", "interface": "Gi0/0"}
[Tool Result] Area 1, Hello 10, Dead 40, MTU 1500, auth: none
[Iteration 4]
Final Answer: Root cause is an OSPF area mismatch...
Each iteration is one LLM call. The LLM never executes code — it only reads observations and decides what to do next.
---
What frameworks add on top
You now have a working agent in ~80 lines. LangGraph, CrewAI, and similar tools add things you will eventually need: persistent state, parallel tool execution, retry logic, graph-based routing, built-in observability.
Module 5 cover LangGraph. By then you will know exactly what problem each abstraction solves — because you built the underlying thing yourself here.
Open In Colab
---
Module Challenge
Run simple_agent.py with the OSPF INIT query.
Post a screenshot of your terminal output in #agent-builds.
The first member to share gets a shoutout in the next live session.
---
Next: Module 3 — Build the Tool Layer (connect to real devices)
0
0 comments
Eduard Dulharu
1
Module 2 · Lesson 3 — Run It: Your First Diagnosis
powered by
Autonomous MSP
skool.com/autonomous-msp-2162
AI-powered NOC, SOC and compliance for MSPs and IT consultancies. Built by a 25-year enterprise network practitioner.
Build your own community
Bring people together around your passion and get paid.
Powered by