packet-tracer-skill 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,18 @@ CAPABILITY_PATTERNS = {
38
38
  "ospf": [r"\bospf\b"],
39
39
  "eigrp": [r"\beigrp\b"],
40
40
  "rip": [r"\brip\b"],
41
+ "ospfv2": [r"\bospfv2\b", r"\bospf v2\b", r"\bsingle-area ospf\b", r"\brouter ospf\b"],
42
+ "eigrp_ipv4": [r"\beigrp ipv4\b", r"\bipv4 eigrp\b", r"\brouter eigrp\b"],
43
+ "ripv2": [r"\bripv2\b", r"\brip v2\b", r"\brip version 2\b", r"\brouter rip\b"],
44
+ "static_route": [r"\bstatic route\b", r"\bstatic-route\b", r"\bip route\b"],
45
+ "default_route": [r"\bdefault route\b", r"\bdefault-route\b", r"\b0\.0\.0\.0/0\b"],
46
+ "dhcp_relay": [r"\bdhcp relay\b", r"\bdhcp-relay\b", r"\bhelper-address\b", r"\bip helper\b"],
47
+ "nat_static": [r"\bstatic nat\b", r"\bnat static\b"],
48
+ "nat_dynamic": [r"\bdynamic nat\b", r"\bnat dynamic\b"],
49
+ "pat": [r"\bpat\b", r"\boverload\b"],
50
+ "ssh_ios": [r"\bssh\b", r"\bip ssh\b", r"\bcrypto key\b"],
51
+ "ntp_ios": [r"\bntp\b", r"\bntp server\b"],
52
+ "syslog_ios": [r"\bsyslog\b", r"\blogging host\b"],
41
53
  "ipv6_slaac": [r"\bslaac\b", r"\bipv6\s+slaac\b"],
42
54
  "dhcpv6_stateful": [r"\bdhcpv6\b", r"\bstateful dhcpv6\b", r"\bipv6 stateful\b"],
43
55
  "dhcpv6_stateless": [r"\bstateless dhcpv6\b", r"\bipv6 stateless\b"],
@@ -58,9 +70,17 @@ CAPABILITY_PATTERNS = {
58
70
  "rep": [r"\brep\b"],
59
71
  "snmp": [r"\bsnmp\b"],
60
72
  "netflow": [r"\bnetflow\b"],
61
- "span": [r"\bspan\b", r"\brspan\b"],
73
+ "span": [r"\bmonitor\s+session\b", r"\bspan\b", r"\brspan\b"],
62
74
  "qos": [r"\bqos policy\b", r"\bqos class\b", r"\bclass-map\b", r"\bpolicy-map\b", r"\bquality of service\b"],
63
75
  "port_security": [r"\bport security\b", r"\bport-security\b"],
76
+ "bgp": [r"\bbgp\b", r"\brouter bgp\b", r"\bremote-as\b"],
77
+ "stp": [r"\bstp\b", r"\bspanning tree\b", r"\bspanning-tree\b", r"\bpvst\b"],
78
+ "rstp": [r"\brstp\b", r"\brapid-pvst\b", r"\brapid pvst\b", r"\brapid spanning\b"],
79
+ "etherchannel": [r"\betherchannel\b", r"\bport-channel\b", r"\bchannel-group\b"],
80
+ "lacp": [r"\blacp\b"],
81
+ "pagp": [r"\bpagp\b"],
82
+ "vtp": [r"\bvtp\b"],
83
+ "dtp": [r"\bdtp\b", r"\bdynamic desirable\b", r"\bdynamic auto\b"],
64
84
  "vpn": [r"\bvpn\b", r"\bsite-to-site\b", r"\bipsec\b", r"\btunnel\b"],
65
85
  "ipsec": [r"\bipsec\b", r"\bike\b", r"\bphase 1\b", r"\bphase 2\b"],
66
86
  "gre": [r"\bgre\b", r"\bgre tunnel\b"],
@@ -160,7 +180,9 @@ NETWORK_STYLE_PATTERNS = {
160
180
  "campus": [r"\bcampus\b", r"\bkampus\b", r"\bsobeli\b", r"\bdepart", r"\bdepartment\b"],
161
181
  "branch": [r"\bbranch\b", r"\bfilial\b"],
162
182
  "ipv6_routing": [r"\bipv6\b", r"\bslaac\b", r"\bdhcpv6\b", r"\bospfv3\b", r"\bhsrp\b"],
183
+ "ipv4_routing_management": [r"\bospfv2\b", r"\bospf v2\b", r"\beigrp ipv4\b", r"\bripv2\b", r"\brip version 2\b", r"\bstatic route\b", r"\bdefault route\b", r"\bdhcp relay\b", r"\bnat\b", r"\bpat\b", r"\bip ssh\b", r"\bntp server\b", r"\blogging host\b"],
163
184
  "l2_security_monitoring": [r"\bdhcp snooping\b", r"\bdai\b", r"\bdot1x\b", r"\b802\.1x\b", r"\bsnmp\b", r"\bnetflow\b", r"\bspan\b", r"\brspan\b", r"\bquality of service\b", r"\blldp\b", r"\brep\b", r"\bport security\b", r"\bport-security\b"],
185
+ "l2_resiliency_routing": [r"\bbgp\b", r"\bstp\b", r"\brstp\b", r"\bspanning tree\b", r"\bspanning-tree\b", r"\betherchannel\b", r"\bport-channel\b", r"\bchannel-group\b", r"\blacp\b", r"\bpagp\b", r"\bvtp\b", r"\bdtp\b"],
164
186
  "wireless_advanced": [r"\bwlc\b", r"\bwireless lan controller\b", r"\bmeraki\b", r"\bbluetooth\b", r"\b5g\b", r"\bcellular\b", r"\bwpa enterprise\b", r"\bwpa2 enterprise\b", r"\bwep\b", r"\bguest wifi\b", r"\bguest wlan\b", r"\bbeamforming\b"],
165
187
  "automation_controller": [r"\bnetwork controller\b", r"\bnetcon\b", r"\bblockly\b", r"\bjavascript\b", r"\bpython programming\b", r"\biox\b"],
166
188
  "voice_collaboration": [r"\bvoip\b", r"\bip phone\b", r"\bcall manager\b", r"\btelephony\b"],
@@ -234,7 +256,11 @@ TRANSLITERATION_TABLE = str.maketrans(
234
256
 
235
257
 
236
258
  def _command_segments(prompt: str) -> list[str]:
237
- parts = re.split(r"(?=\b(?:device|connect|link|set|enable|associate|change|update|rename|apply)\b)", prompt, flags=re.IGNORECASE)
259
+ parts = re.split(
260
+ r"(?=(?<!-)\b(?:device|connect|link|set|enable|associate|change|update|rename|apply)\b)",
261
+ prompt,
262
+ flags=re.IGNORECASE,
263
+ )
238
264
  return [part.strip() for part in parts if part.strip()]
239
265
 
240
266
 
@@ -306,6 +332,7 @@ class IntentPlan:
306
332
  management_ops: list[dict[str, object]] = field(default_factory=list)
307
333
  verification_ops: list[dict[str, object]] = field(default_factory=list)
308
334
  iot_ops: list[dict[str, object]] = field(default_factory=list)
335
+ programming_ops: list[dict[str, object]] = field(default_factory=list)
309
336
 
310
337
  def all_operations(self) -> list[dict[str, object]]:
311
338
  merged: list[dict[str, object]] = []
@@ -319,6 +346,7 @@ class IntentPlan:
319
346
  self.management_ops,
320
347
  self.verification_ops,
321
348
  self.iot_ops,
349
+ self.programming_ops,
322
350
  ]:
323
351
  merged.extend(bucket)
324
352
  return merged
@@ -586,6 +614,32 @@ def _extract_switch_ops(prompt: str) -> list[dict[str, object]]:
586
614
  r"set\s+([A-Za-z0-9_-]+)\s+(?:r?span)\s+(\d+)\s+source\s+([A-Za-z0-9/._-]+)\s+destination\s+([A-Za-z0-9/._-]+)",
587
615
  flags=re.IGNORECASE,
588
616
  )
617
+ dot1x_pattern = re.compile(
618
+ r"set\s+([A-Za-z0-9_-]+)\s+dot1x\s+interface\s+([A-Za-z0-9/._-]+)\s+mode\s+(auto|force-authorized|force-unauthorized)"
619
+ r"(?:\s+radius\s+(\d+\.\d+\.\d+\.\d+)\s+key\s+([A-Za-z0-9._-]+))?",
620
+ flags=re.IGNORECASE,
621
+ )
622
+ qos_pattern = re.compile(
623
+ r"set\s+([A-Za-z0-9_-]+)\s+qos\s+class-map\s+([A-Za-z0-9_-]+)\s+match\s+(.+?)\s+policy-map\s+([A-Za-z0-9_-]+)\s+class\s+\2\s+(priority|bandwidth\s+\d+|police\s+\d+)"
624
+ r"\s+service-policy\s+(input|output)\s+([A-Za-z0-9/._-]+)(?=\s+set\s+|$)",
625
+ flags=re.IGNORECASE,
626
+ )
627
+ stp_pattern = re.compile(
628
+ r"set\s+([A-Za-z0-9_-]+)\s+stp\s+mode\s+([A-Za-z0-9_-]+)(?:\s+vlan\s+(\d+)\s+root\s+(primary|secondary))?",
629
+ flags=re.IGNORECASE,
630
+ )
631
+ etherchannel_pattern = re.compile(
632
+ r"set\s+([A-Za-z0-9_-]+)\s+etherchannel\s+(\d+)\s+mode\s+(active|passive|desirable|auto|on)\s+interfaces\s+(.+?)(?=\s+set\s+|$)",
633
+ flags=re.IGNORECASE,
634
+ )
635
+ vtp_pattern = re.compile(
636
+ r"set\s+([A-Za-z0-9_-]+)\s+vtp\s+domain\s+([A-Za-z0-9_.-]+)\s+mode\s+(server|client|transparent)(?:\s+version\s+(\d+))?",
637
+ flags=re.IGNORECASE,
638
+ )
639
+ dtp_pattern = re.compile(
640
+ r"set\s+([A-Za-z0-9_-]+)\s+dtp\s+interface\s+([A-Za-z0-9/._-]+)\s+mode\s+(dynamic\s+desirable|dynamic\s+auto|trunk|access)",
641
+ flags=re.IGNORECASE,
642
+ )
589
643
  for segment in _command_segments(prompt):
590
644
  for device, vlan_id, trust_port in dhcp_snooping_pattern.findall(segment):
591
645
  ops.append({"op": "set_dhcp_snooping", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
@@ -607,6 +661,63 @@ def _extract_switch_ops(prompt: str) -> list[dict[str, object]]:
607
661
  ops.append({"op": "set_rep", "device": device, "segment": int(segment_id), "interface": interface_name})
608
662
  for device, session, source, destination in span_pattern.findall(segment):
609
663
  ops.append({"op": "set_span", "device": device, "session": int(session), "source": source, "destination": destination})
664
+ for device, interface_name, mode, radius_host, radius_key in dot1x_pattern.findall(segment):
665
+ ops.append(
666
+ {
667
+ "op": "set_dot1x",
668
+ "device": device,
669
+ "interface": interface_name,
670
+ "mode": mode.lower(),
671
+ "radius_host": radius_host or None,
672
+ "radius_key": radius_key or None,
673
+ }
674
+ )
675
+ for device, class_map, match_expr, policy_map, action, direction, interface_name in qos_pattern.findall(segment):
676
+ ops.append(
677
+ {
678
+ "op": "set_qos_policy",
679
+ "device": device,
680
+ "class_map": class_map,
681
+ "match": match_expr.strip(),
682
+ "policy_map": policy_map,
683
+ "action": action.lower(),
684
+ "direction": direction.lower(),
685
+ "interface": interface_name,
686
+ }
687
+ )
688
+ for device, mode, vlan_id, root_priority in stp_pattern.findall(segment):
689
+ ops.append(
690
+ {
691
+ "op": "set_stp",
692
+ "device": device,
693
+ "mode": mode.lower(),
694
+ "vlan": int(vlan_id) if vlan_id else None,
695
+ "root": root_priority.lower() if root_priority else None,
696
+ }
697
+ )
698
+ for device, channel, mode, interfaces_text in etherchannel_pattern.findall(segment):
699
+ interfaces = [value for value in re.findall(r"[A-Za-z]+[A-Za-z]*[0-9/._-]+", interfaces_text) if value.lower() != "set"]
700
+ ops.append(
701
+ {
702
+ "op": "set_etherchannel",
703
+ "device": device,
704
+ "channel": int(channel),
705
+ "mode": mode.lower(),
706
+ "interfaces": interfaces,
707
+ }
708
+ )
709
+ for device, domain, mode, version in vtp_pattern.findall(segment):
710
+ ops.append(
711
+ {
712
+ "op": "set_vtp",
713
+ "device": device,
714
+ "domain": domain,
715
+ "mode": mode.lower(),
716
+ "version": int(version) if version else None,
717
+ }
718
+ )
719
+ for device, interface_name, mode in dtp_pattern.findall(segment):
720
+ ops.append({"op": "set_dtp", "device": device, "interface": interface_name, "mode": mode.lower()})
610
721
  return ops
611
722
 
612
723
 
@@ -695,11 +806,101 @@ def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
695
806
  r"set\s+([A-Za-z0-9_-]+)\s+hsrp\s+(\d+)\s+ipv6\s+([0-9A-Fa-f:]+)\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+priority\s+(\d+))?",
696
807
  flags=re.IGNORECASE,
697
808
  )
809
+ ospfv2_pattern = re.compile(
810
+ r"set\s+([A-Za-z0-9_-]+)\s+ospfv2\s+(\d+)\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+wildcard\s+(\d+\.\d+\.\d+\.\d+)\s+area\s+(\d+)",
811
+ flags=re.IGNORECASE,
812
+ )
813
+ eigrp_ipv4_pattern = re.compile(
814
+ r"set\s+([A-Za-z0-9_-]+)\s+eigrp\s+ipv4\s+(\d+)\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+wildcard\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(no-auto-summary))?",
815
+ flags=re.IGNORECASE,
816
+ )
817
+ ripv2_pattern = re.compile(
818
+ r"set\s+([A-Za-z0-9_-]+)\s+rip\s+version\s+2\s+network\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(no-auto-summary))?",
819
+ flags=re.IGNORECASE,
820
+ )
821
+ static_route_pattern = re.compile(
822
+ r"set\s+([A-Za-z0-9_-]+)\s+static-route\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+via\s+(\d+\.\d+\.\d+\.\d+)",
823
+ flags=re.IGNORECASE,
824
+ )
825
+ dhcp_relay_pattern = re.compile(
826
+ r"set\s+([A-Za-z0-9_-]+)\s+dhcp-relay\s+interface\s+([A-Za-z0-9/._-]+)\s+helper\s+(\d+\.\d+\.\d+\.\d+)",
827
+ flags=re.IGNORECASE,
828
+ )
829
+ nat_inside_outside_pattern = re.compile(
830
+ r"set\s+([A-Za-z0-9_-]+)\s+nat\s+(inside|outside)\s+interface\s+([A-Za-z0-9/._-]+)",
831
+ flags=re.IGNORECASE,
832
+ )
833
+ nat_static_pattern = re.compile(
834
+ r"set\s+([A-Za-z0-9_-]+)\s+nat\s+static\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)",
835
+ flags=re.IGNORECASE,
836
+ )
837
+ pat_pattern = re.compile(
838
+ r"set\s+([A-Za-z0-9_-]+)\s+pat\s+acl\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+(overload))?",
839
+ flags=re.IGNORECASE,
840
+ )
841
+ ssh_ios_pattern = re.compile(
842
+ r"set\s+([A-Za-z0-9_-]+)\s+ssh\s+domain\s+([A-Za-z0-9._-]+)\s+username\s+([A-Za-z0-9._-]+)\s+password\s+([A-Za-z0-9._-]+)(?:\s+modulus\s+(\d+))?",
843
+ flags=re.IGNORECASE,
844
+ )
845
+ ntp_ios_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ntp\s+server\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
846
+ syslog_ios_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+syslog\s+server\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
847
+ bgp_pattern = re.compile(
848
+ r"set\s+([A-Za-z0-9_-]+)\s+bgp\s+(\d+)\s+neighbor\s+(\d+\.\d+\.\d+\.\d+)\s+remote-as\s+(\d+)"
849
+ r"(?:\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+mask\s+(\d+\.\d+\.\d+\.\d+))?",
850
+ flags=re.IGNORECASE,
851
+ )
698
852
  snmp_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+snmp\s+community\s+([A-Za-z0-9_-]+)\s+(ro|rw)", flags=re.IGNORECASE)
699
853
  netflow_pattern = re.compile(
700
854
  r"set\s+([A-Za-z0-9_-]+)\s+netflow\s+destination\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)(?:\s+version\s+(\d+))?(?:\s+interface\s+([A-Za-z0-9/._-]+)\s+(ingress|egress))?",
701
855
  flags=re.IGNORECASE,
702
856
  )
857
+ gre_tunnel_pattern = re.compile(
858
+ r"set\s+([A-Za-z0-9_-]+)\s+gre\s+tunnel\s+([A-Za-z0-9/._-]+)\s+source\s+([A-Za-z0-9/._:-]+)\s+destination\s+([A-Za-z0-9/._:-]+)"
859
+ r"(?:\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+))?",
860
+ flags=re.IGNORECASE,
861
+ )
862
+ ppp_pattern = re.compile(
863
+ r"set\s+([A-Za-z0-9_-]+)\s+ppp\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+authentication\s+(chap|pap))?",
864
+ flags=re.IGNORECASE,
865
+ )
866
+ ipsec_transform_pattern = re.compile(
867
+ r"set\s+([A-Za-z0-9_-]+)\s+ipsec\s+transform-set\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)",
868
+ flags=re.IGNORECASE,
869
+ )
870
+ crypto_map_pattern = re.compile(
871
+ r"set\s+([A-Za-z0-9_-]+)\s+crypto\s+map\s+([A-Za-z0-9_-]+)\s+(\d+)\s+peer\s+(\d+\.\d+\.\d+\.\d+)\s+transform-set\s+([A-Za-z0-9_-]+)\s+match\s+([A-Za-z0-9_-]+)(?:\s+interface\s+([A-Za-z0-9/._-]+))?",
872
+ flags=re.IGNORECASE,
873
+ )
874
+ cbac_pattern = re.compile(
875
+ r"set\s+([A-Za-z0-9_-]+)\s+cbac\s+inspect\s+([A-Za-z0-9_-]+)\s+protocol\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)\s+direction\s+(in|out)",
876
+ flags=re.IGNORECASE,
877
+ )
878
+ zfw_zone_interface_pattern = re.compile(
879
+ r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+zone\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)",
880
+ flags=re.IGNORECASE,
881
+ )
882
+ zfw_zone_pair_pattern = re.compile(
883
+ r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+zone-pair\s+([A-Za-z0-9_-]+)\s+source\s+([A-Za-z0-9_-]+)\s+destination\s+([A-Za-z0-9_-]+)\s+policy\s+([A-Za-z0-9_-]+)",
884
+ flags=re.IGNORECASE,
885
+ )
886
+ zfw_policy_pattern = re.compile(
887
+ r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+class-map\s+([A-Za-z0-9_-]+)\s+match\s+protocol\s+([A-Za-z0-9_-]+)\s+policy-map\s+([A-Za-z0-9_-]+)\s+action\s+(inspect|pass|drop)",
888
+ flags=re.IGNORECASE,
889
+ )
890
+ telephony_service_pattern = re.compile(
891
+ r'set\s+"?([^"]+?)"?\s+telephony\s+service\s+source-address\s+(\d+\.\d+\.\d+\.\d+)\s+port\s+(\d+)'
892
+ r"(?:\s+max-ephones\s+(\d+))?(?:\s+max-dn\s+(\d+))?",
893
+ flags=re.IGNORECASE,
894
+ )
895
+ ephone_dn_pattern = re.compile(r'set\s+"?([^"]+?)"?\s+ephone-dn\s+(\d+)\s+number\s+([A-Za-z0-9*+#.-]+)', flags=re.IGNORECASE)
896
+ ephone_pattern = re.compile(
897
+ r'set\s+"?([^"]+?)"?\s+ephone\s+(\d+)\s+mac\s+([0-9A-Fa-f.:-]+)\s+button\s+([0-9:]+)',
898
+ flags=re.IGNORECASE,
899
+ )
900
+ dial_peer_pattern = re.compile(
901
+ r'set\s+"?([^"]+?)"?\s+dial-peer\s+voice\s+(\d+)\s+destination-pattern\s+(\S+)\s+session-target\s+ipv4:(\d+\.\d+\.\d+\.\d+)',
902
+ flags=re.IGNORECASE,
903
+ )
703
904
  for segment in _command_segments(prompt):
704
905
  for device in ipv6_unicast_pattern.findall(segment):
705
906
  ops.append({"op": "enable_ipv6_unicast_routing", "device": device})
@@ -745,6 +946,40 @@ def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
745
946
  "priority": int(priority) if priority else None,
746
947
  }
747
948
  )
949
+ for device, process_id, network, wildcard, area in ospfv2_pattern.findall(segment):
950
+ ops.append({"op": "set_ospfv2_network", "device": device, "process_id": int(process_id), "network": network, "wildcard": wildcard, "area": int(area)})
951
+ for device, asn, network, wildcard, no_auto in eigrp_ipv4_pattern.findall(segment):
952
+ ops.append({"op": "set_eigrp_ipv4_network", "device": device, "asn": int(asn), "network": network, "wildcard": wildcard, "no_auto_summary": bool(no_auto)})
953
+ for device, network, no_auto in ripv2_pattern.findall(segment):
954
+ ops.append({"op": "set_ripv2_network", "device": device, "network": network, "no_auto_summary": bool(no_auto)})
955
+ for device, network, prefix, next_hop in static_route_pattern.findall(segment):
956
+ ops.append({"op": "set_static_route", "device": device, "network": network, "prefix": int(prefix), "next_hop": next_hop})
957
+ for device, interface_name, helper in dhcp_relay_pattern.findall(segment):
958
+ ops.append({"op": "set_dhcp_relay", "device": device, "interface": interface_name, "helper": helper})
959
+ for device, role, interface_name in nat_inside_outside_pattern.findall(segment):
960
+ ops.append({"op": "set_nat_interface", "device": device, "role": role.lower(), "interface": interface_name})
961
+ for device, inside_local, inside_global in nat_static_pattern.findall(segment):
962
+ ops.append({"op": "set_nat_static", "device": device, "inside_local": inside_local, "inside_global": inside_global})
963
+ for device, acl, interface_name, overload in pat_pattern.findall(segment):
964
+ ops.append({"op": "set_pat_overload", "device": device, "acl": acl, "interface": interface_name, "overload": bool(overload)})
965
+ for device, domain, username, password, modulus in ssh_ios_pattern.findall(segment):
966
+ ops.append({"op": "set_ssh_ios", "device": device, "domain": domain, "username": username, "password": password, "modulus": int(modulus) if modulus else 1024})
967
+ for device, server in ntp_ios_pattern.findall(segment):
968
+ ops.append({"op": "set_ntp_server", "device": device, "server": server})
969
+ for device, server in syslog_ios_pattern.findall(segment):
970
+ ops.append({"op": "set_syslog_server", "device": device, "server": server})
971
+ for device, asn, neighbor, remote_as, network, mask in bgp_pattern.findall(segment):
972
+ ops.append(
973
+ {
974
+ "op": "set_bgp_neighbor",
975
+ "device": device,
976
+ "asn": int(asn),
977
+ "neighbor": neighbor,
978
+ "remote_as": int(remote_as),
979
+ "network": network or None,
980
+ "mask": mask or None,
981
+ }
982
+ )
748
983
  for device, community, mode in snmp_pattern.findall(segment):
749
984
  ops.append({"op": "set_snmp_community", "device": device, "community": community, "mode": mode.lower()})
750
985
  for device, destination, port, version, interface_name, direction in netflow_pattern.findall(segment):
@@ -759,6 +994,110 @@ def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
759
994
  "direction": direction.lower() if direction else None,
760
995
  }
761
996
  )
997
+ for device, tunnel_interface, source, destination, ip, prefix in gre_tunnel_pattern.findall(segment):
998
+ ops.append(
999
+ {
1000
+ "op": "set_gre_tunnel",
1001
+ "device": device,
1002
+ "interface": tunnel_interface,
1003
+ "source": source,
1004
+ "destination": destination,
1005
+ "ip": ip or None,
1006
+ "prefix": int(prefix) if prefix else None,
1007
+ }
1008
+ )
1009
+ for device, interface_name, authentication in ppp_pattern.findall(segment):
1010
+ ops.append(
1011
+ {
1012
+ "op": "set_ppp_interface",
1013
+ "device": device,
1014
+ "interface": interface_name,
1015
+ "authentication": authentication.lower() if authentication else None,
1016
+ }
1017
+ )
1018
+ for device, name, encryption, integrity in ipsec_transform_pattern.findall(segment):
1019
+ ops.append(
1020
+ {
1021
+ "op": "set_ipsec_transform_set",
1022
+ "device": device,
1023
+ "name": name,
1024
+ "encryption": encryption,
1025
+ "integrity": integrity,
1026
+ }
1027
+ )
1028
+ for device, map_name, sequence, peer, transform_set, acl_name, interface_name in crypto_map_pattern.findall(segment):
1029
+ ops.append(
1030
+ {
1031
+ "op": "set_crypto_map",
1032
+ "device": device,
1033
+ "map_name": map_name,
1034
+ "sequence": int(sequence),
1035
+ "peer": peer,
1036
+ "transform_set": transform_set,
1037
+ "acl_name": acl_name,
1038
+ "interface": interface_name or None,
1039
+ }
1040
+ )
1041
+ for device, name, protocol, interface_name, direction in cbac_pattern.findall(segment):
1042
+ ops.append(
1043
+ {
1044
+ "op": "set_cbac_inspect",
1045
+ "device": device,
1046
+ "name": name,
1047
+ "protocol": protocol.lower(),
1048
+ "interface": interface_name,
1049
+ "direction": direction.lower(),
1050
+ }
1051
+ )
1052
+ for device, zone, interface_name in zfw_zone_interface_pattern.findall(segment):
1053
+ ops.append({"op": "set_zfw_zone_interface", "device": device, "zone": zone, "interface": interface_name})
1054
+ for device, pair_name, source, destination, policy in zfw_zone_pair_pattern.findall(segment):
1055
+ ops.append(
1056
+ {
1057
+ "op": "set_zfw_zone_pair",
1058
+ "device": device,
1059
+ "pair_name": pair_name,
1060
+ "source": source,
1061
+ "destination": destination,
1062
+ "policy": policy,
1063
+ }
1064
+ )
1065
+ for device, class_map, protocol, policy_map, action in zfw_policy_pattern.findall(segment):
1066
+ ops.append(
1067
+ {
1068
+ "op": "set_zfw_policy",
1069
+ "device": device,
1070
+ "class_map": class_map,
1071
+ "protocol": protocol.lower(),
1072
+ "policy_map": policy_map,
1073
+ "action": action.lower(),
1074
+ }
1075
+ )
1076
+ for device, source_address, port, max_ephones, max_dn in telephony_service_pattern.findall(segment):
1077
+ ops.append(
1078
+ {
1079
+ "op": "set_telephony_service",
1080
+ "device": device.strip(),
1081
+ "source_address": source_address,
1082
+ "port": int(port),
1083
+ "max_ephones": int(max_ephones) if max_ephones else None,
1084
+ "max_dn": int(max_dn) if max_dn else None,
1085
+ }
1086
+ )
1087
+ for device, dn_id, number in ephone_dn_pattern.findall(segment):
1088
+ ops.append({"op": "set_ephone_dn", "device": device.strip(), "dn_id": int(dn_id), "number": number})
1089
+ for device, ephone_id, mac, button in ephone_pattern.findall(segment):
1090
+ ops.append({"op": "set_ephone", "device": device.strip(), "ephone_id": int(ephone_id), "mac": mac.upper(), "button": button})
1091
+ for device, peer_id, destination_pattern, session_target in dial_peer_pattern.findall(segment):
1092
+ ops.append(
1093
+ {
1094
+ "op": "set_dial_peer_voice",
1095
+ "device": device.strip(),
1096
+ "peer_id": int(peer_id),
1097
+ "destination_pattern": destination_pattern,
1098
+ "session_target": session_target,
1099
+ }
1100
+ )
762
1101
  return ops
763
1102
 
764
1103
 
@@ -930,6 +1269,36 @@ def _extract_iot_ops(prompt: str) -> list[dict[str, object]]:
930
1269
  return ops
931
1270
 
932
1271
 
1272
+ def _decode_prompt_string(value: str) -> str:
1273
+ return (
1274
+ value.replace(r"\\", "\0")
1275
+ .replace(r"\"", '"')
1276
+ .replace(r"\n", "\n")
1277
+ .replace(r"\r", "\r")
1278
+ .replace(r"\t", "\t")
1279
+ .replace("\0", "\\")
1280
+ )
1281
+
1282
+
1283
+ def _extract_programming_ops(prompt: str) -> list[dict[str, object]]:
1284
+ ops: list[dict[str, object]] = []
1285
+ script_pattern = re.compile(
1286
+ r'set\s+"((?:\\.|[^"\\])*)"\s+script\s+app\s+"((?:\\.|[^"\\])*)"\s+file\s+"((?:\\.|[^"\\])*)"\s+content\s+"((?:\\.|[^"\\])*)"',
1287
+ flags=re.IGNORECASE | re.DOTALL,
1288
+ )
1289
+ for device, app_name, file_name, content in script_pattern.findall(prompt):
1290
+ ops.append(
1291
+ {
1292
+ "op": "set_script_file_content",
1293
+ "device": _decode_prompt_string(device).strip(),
1294
+ "app_name": _decode_prompt_string(app_name).strip(),
1295
+ "file_name": _decode_prompt_string(file_name).strip(),
1296
+ "content": _decode_prompt_string(content),
1297
+ }
1298
+ )
1299
+ return ops
1300
+
1301
+
933
1302
  def parse_intent(prompt: str) -> IntentPlan:
934
1303
  pkt_path_match = re.search(r"([A-Za-z]:\\[^\"\n]+?\.pkt)\b", prompt, flags=re.IGNORECASE)
935
1304
  pkt_path = pkt_path_match.group(1) if pkt_path_match else None
@@ -957,6 +1326,48 @@ def parse_intent(prompt: str) -> IntentPlan:
957
1326
  or network_style == "l2_security_monitoring"
958
1327
  ):
959
1328
  capability_set.add("qos")
1329
+ if network_style == "automation_controller" or re.search(r"\b(?:network controller|netcon|blockly|iox)\b", lowered):
1330
+ if re.search(r"\bpython\b", lowered):
1331
+ capability_set.add("python_programming")
1332
+ if re.search(r"\b(?:tcp|udp)\b", lowered) and re.search(r"\b(?:app|application|test app)\b", lowered):
1333
+ capability_set.add("tcp_udp_app")
1334
+ if "ospf" in capability_set and not re.search(r"\b(?:ipv6|ospfv3)\b", lowered):
1335
+ capability_set.add("ospfv2")
1336
+ if "eigrp" in capability_set and not re.search(r"\bipv6\b", lowered):
1337
+ capability_set.add("eigrp_ipv4")
1338
+ if "rip" in capability_set and not re.search(r"\b(?:ipv6|ripng)\b", lowered):
1339
+ capability_set.add("ripv2")
1340
+ if "nat" in capability_set:
1341
+ if re.search(r"\bpat\b|\boverload\b", lowered):
1342
+ capability_set.add("pat")
1343
+ if re.search(r"\bstatic\s+nat\b|\bnat\s+static\b", lowered):
1344
+ capability_set.add("nat_static")
1345
+ if re.search(r"\bdynamic\s+nat\b|\bnat\s+dynamic\b", lowered):
1346
+ capability_set.add("nat_dynamic")
1347
+ if network_style == "ipv4_routing_management" and not {"nat_static", "nat_dynamic"} & capability_set:
1348
+ capability_set.update({"nat_static", "nat_dynamic"})
1349
+ if "ssh" in capability_set:
1350
+ capability_set.add("ssh_ios")
1351
+ if "ntp" in capability_set:
1352
+ capability_set.add("ntp_ios")
1353
+ if (
1354
+ "server_syslog" in capability_set
1355
+ and re.search(r"\b(?:server|ftp|email|aaa|dns)\b", lowered)
1356
+ and not re.search(r"\b(?:logging\s+host|set\s+\S+\s+syslog\s+server)\b", lowered)
1357
+ ):
1358
+ capability_set.discard("syslog_ios")
1359
+ if re.search(r"\blogging\s+host\b", lowered):
1360
+ capability_set.add("syslog_ios")
1361
+ if re.search(r"\bdefault\s+route\b|\b0\.0\.0\.0/0\b", lowered):
1362
+ capability_set.update({"static_route", "default_route"})
1363
+ elif re.search(r"\bstatic\s+route\b|\bip\s+route\b", lowered):
1364
+ capability_set.add("static_route")
1365
+ if re.search(r"\bdhcp\s+relay\b|\bdhcp-relay\b|\bhelper-address\b|\bip\s+helper\b", lowered):
1366
+ capability_set.add("dhcp_relay")
1367
+ if not re.search(r"\b(?:dhcp\s+pool|server\s+dhcp|dhcp\s+server|default-router)\b", lowered):
1368
+ capability_set.discard("dhcp_pool")
1369
+ capability_set.discard("router_dhcp")
1370
+ capability_set.discard("server_dhcp")
960
1371
  if "dhcp_snooping" in capability_set and not re.search(r"\b(?:dhcp\s+pool|server\s+dhcp|dhcp\s+server|default-router)\b", lowered):
961
1372
  capability_set.discard("dhcp_pool")
962
1373
  capability_set.discard("router_dhcp")
@@ -1010,6 +1421,7 @@ def parse_intent(prompt: str) -> IntentPlan:
1010
1421
  management_ops = _extract_management_ops(prompt)
1011
1422
  verification_ops = _extract_verification_ops(prompt)
1012
1423
  iot_ops = _extract_iot_ops(prompt)
1424
+ programming_ops = _extract_programming_ops(prompt)
1013
1425
 
1014
1426
  if any(op["op"] == "associate_wireless_client" for op in wireless_ops):
1015
1427
  capability_set.update({"wireless_client", "wireless_client_association"})
@@ -1030,6 +1442,28 @@ def parse_intent(prompt: str) -> IntentPlan:
1030
1442
  capability_set.add("end_device_mutation")
1031
1443
  if iot_ops:
1032
1444
  capability_set.update({"iot", "iot_registration"})
1445
+ if any(op.get("op") == "set_iot_rule_state" for op in iot_ops):
1446
+ capability_set.add("iot_control")
1447
+ for operation in programming_ops:
1448
+ file_name = str(operation.get("file_name") or "").lower()
1449
+ app_name = str(operation.get("app_name") or "").lower()
1450
+ content = str(operation.get("content") or "").lower()
1451
+ if file_name.endswith(".py") or "python" in app_name:
1452
+ capability_set.add("python_programming")
1453
+ if file_name.endswith(".js") or "javascript" in app_name:
1454
+ capability_set.add("javascript_programming")
1455
+ if file_name.endswith(".visual") or "visual" in app_name or "<xml" in content:
1456
+ capability_set.add("blockly_programming")
1457
+ if "tcp" in " ".join([file_name, app_name, content]) or "udp" in " ".join([file_name, app_name, content]):
1458
+ capability_set.add("tcp_udp_app")
1459
+ if "realhttp" in content or "real http" in app_name:
1460
+ capability_set.add("real_http")
1461
+ if "realws" in content or "websocket" in content or "websocket" in app_name:
1462
+ capability_set.add("real_websocket")
1463
+ if "mqtt" in content or "mqtt" in app_name:
1464
+ capability_set.add("mqtt")
1465
+ if programming_ops and capability_set & {"python_programming", "javascript_programming", "blockly_programming"} and not capability_set & {"real_http", "real_websocket", "mqtt"}:
1466
+ network_style = network_style or "automation_controller"
1033
1467
  switch_op_names = {str(op.get("op")) for op in switch_ops}
1034
1468
  if "set_dhcp_snooping" in switch_op_names:
1035
1469
  capability_set.add("dhcp_snooping")
@@ -1043,11 +1477,50 @@ def parse_intent(prompt: str) -> IntentPlan:
1043
1477
  capability_set.add("span")
1044
1478
  if "set_port_security" in switch_op_names:
1045
1479
  capability_set.add("port_security")
1480
+ if "set_dot1x" in switch_op_names:
1481
+ capability_set.add("dot1x")
1482
+ if "set_qos_policy" in switch_op_names:
1483
+ capability_set.add("qos")
1484
+ if "set_stp" in switch_op_names:
1485
+ capability_set.add("stp")
1486
+ for op in switch_ops:
1487
+ if op.get("op") == "set_stp" and re.search(r"(?:rapid|rstp)", str(op.get("mode") or ""), flags=re.IGNORECASE):
1488
+ capability_set.add("rstp")
1489
+ if "set_etherchannel" in switch_op_names:
1490
+ capability_set.add("etherchannel")
1491
+ for op in switch_ops:
1492
+ if op.get("op") != "set_etherchannel":
1493
+ continue
1494
+ mode = str(op.get("mode") or "").lower()
1495
+ if mode in {"active", "passive"}:
1496
+ capability_set.add("lacp")
1497
+ if mode in {"desirable", "auto"}:
1498
+ capability_set.add("pagp")
1499
+ if "set_vtp" in switch_op_names:
1500
+ capability_set.add("vtp")
1501
+ if "set_dtp" in switch_op_names:
1502
+ capability_set.add("dtp")
1046
1503
  router_op_names = {str(op.get("op")) for op in router_ops}
1504
+ if "set_bgp_neighbor" in router_op_names:
1505
+ capability_set.add("bgp")
1047
1506
  if "set_snmp_community" in router_op_names:
1048
1507
  capability_set.add("snmp")
1049
1508
  if "set_netflow" in router_op_names:
1050
1509
  capability_set.add("netflow")
1510
+ if "set_gre_tunnel" in router_op_names:
1511
+ capability_set.add("gre")
1512
+ if "set_ppp_interface" in router_op_names:
1513
+ capability_set.add("ppp")
1514
+ if router_op_names & {"set_ipsec_transform_set", "set_crypto_map"}:
1515
+ capability_set.update({"ipsec", "vpn"})
1516
+ if "set_cbac_inspect" in router_op_names:
1517
+ capability_set.update({"cbac", "security_edge"})
1518
+ if router_op_names & {"set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy"}:
1519
+ capability_set.update({"zfw", "security_edge"})
1520
+ if "set_qos_policy" not in switch_op_names:
1521
+ capability_set.discard("qos")
1522
+ capability_set.discard("server_http")
1523
+ capability_set.discard("server_https")
1051
1524
  if router_op_names & {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"}:
1052
1525
  capability_set.add("ipv6_slaac")
1053
1526
  if "set_dhcpv6_pool" in router_op_names:
@@ -1060,16 +1533,61 @@ def parse_intent(prompt: str) -> IntentPlan:
1060
1533
  capability_set.add("ripng")
1061
1534
  if "set_hsrp_ipv6" in router_op_names:
1062
1535
  capability_set.add("hsrp")
1536
+ if "set_ospfv2_network" in router_op_names:
1537
+ capability_set.add("ospfv2")
1538
+ if "set_eigrp_ipv4_network" in router_op_names:
1539
+ capability_set.add("eigrp_ipv4")
1540
+ if "set_ripv2_network" in router_op_names:
1541
+ capability_set.add("ripv2")
1542
+ if "set_static_route" in router_op_names:
1543
+ capability_set.add("static_route")
1544
+ if any(op.get("op") == "set_static_route" and op.get("network") == "0.0.0.0" and op.get("prefix") == 0 for op in router_ops):
1545
+ capability_set.add("default_route")
1546
+ if "set_dhcp_relay" in router_op_names:
1547
+ capability_set.add("dhcp_relay")
1548
+ if "set_nat_interface" in router_op_names:
1549
+ capability_set.add("nat")
1550
+ if "set_nat_static" in router_op_names:
1551
+ capability_set.update({"nat", "nat_static"})
1552
+ if "set_pat_overload" in router_op_names:
1553
+ capability_set.update({"nat", "pat"})
1554
+ if "set_ssh_ios" in router_op_names:
1555
+ capability_set.update({"ssh", "ssh_ios"})
1556
+ if "set_ntp_server" in router_op_names:
1557
+ capability_set.update({"ntp", "ntp_ios"})
1558
+ if "set_syslog_server" in router_op_names:
1559
+ capability_set.update({"syslog_ios"})
1560
+ if router_op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone", "set_dial_peer_voice"}:
1561
+ capability_set.update({"voip", "call_manager"})
1562
+ if router_op_names & {"set_ephone_dn", "set_ephone"}:
1563
+ capability_set.add("ip_phone")
1063
1564
  if capability_set & {"vpn", "ipsec", "gre", "ppp", "security_edge"}:
1064
1565
  network_style = network_style or "wan_security"
1065
1566
  if capability_set & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
1066
1567
  network_style = network_style or "ipv6_routing"
1568
+ if capability_set & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
1569
+ network_style = network_style or "ipv4_routing_management"
1067
1570
  if capability_set & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
1068
1571
  network_style = network_style or "l2_security_monitoring"
1572
+ if capability_set & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
1573
+ network_style = "l2_resiliency_routing"
1069
1574
  if capability_set & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
1070
1575
  network_style = "wireless_advanced"
1576
+ if capability_set & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}:
1577
+ network_style = "physical_media_device"
1071
1578
  if capability_set & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
1072
1579
  network_style = "industrial_iot"
1580
+ if capability_set & {"real_http", "real_websocket"}:
1581
+ capability_set.discard("server_http")
1582
+ capability_set.discard("server_https")
1583
+ if not (
1584
+ iot_ops
1585
+ or re.search(r"\b(?:home\s+gateway|smart\s+home|iot\s+registration|iot\s+control|iot\s+rule)\b", lowered)
1586
+ or re.search(r"\bregister\s+.+\s+to\b", lowered)
1587
+ ):
1588
+ capability_set.discard("iot")
1589
+ if capability_set & {"voip", "ip_phone", "call_manager", "linksys_voice"}:
1590
+ network_style = "voice_collaboration"
1073
1591
 
1074
1592
  capabilities = sorted(capability_set)
1075
1593
 
@@ -1135,4 +1653,5 @@ def parse_intent(prompt: str) -> IntentPlan:
1135
1653
  management_ops=management_ops,
1136
1654
  verification_ops=verification_ops,
1137
1655
  iot_ops=iot_ops,
1656
+ programming_ops=programming_ops,
1138
1657
  )