packet-tracer-skill 0.1.0

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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/LICENSE.Twofish-BSD-3-Clause.txt +29 -0
  3. package/README.md +687 -0
  4. package/SKILL.md +221 -0
  5. package/bin/packet-tracer-skill.js +635 -0
  6. package/examples/blueprint_minimal.json +46 -0
  7. package/package.json +42 -0
  8. package/references/packettracer-sample-catalog.json +21410 -0
  9. package/references/packettracer-sample-catalog.md +1124 -0
  10. package/references/pkt-format.md +57 -0
  11. package/references/xml-skeleton-notes.md +44 -0
  12. package/requirements-dev.txt +1 -0
  13. package/requirements.txt +6 -0
  14. package/scripts/build_sample_catalog.py +65 -0
  15. package/scripts/donor_diagnostics.py +35 -0
  16. package/scripts/generate_pkt.py +1264 -0
  17. package/scripts/install_skill.py +71 -0
  18. package/scripts/intent_parser.py +712 -0
  19. package/scripts/packet_tracer_env.py +278 -0
  20. package/scripts/pkt_builder.py +15 -0
  21. package/scripts/pkt_codec.py +181 -0
  22. package/scripts/pkt_editor.py +752 -0
  23. package/scripts/pkt_transformer.py +541 -0
  24. package/scripts/sample_catalog.py +385 -0
  25. package/scripts/sample_selector.py +156 -0
  26. package/scripts/setup.ps1 +26 -0
  27. package/scripts/twofish_diagnostics.py +91 -0
  28. package/scripts/vendor/README.md +46 -0
  29. package/scripts/vendor/twofish.py +81 -0
  30. package/scripts/workspace_repair.py +441 -0
  31. package/templates/pt900/base_empty.xml +21 -0
  32. package/templates/pt900/device_library/pc.xml +20 -0
  33. package/templates/pt900/device_library/printer.xml +432 -0
  34. package/templates/pt900/device_library/router.xml +16 -0
  35. package/templates/pt900/device_library/switch.xml +38 -0
@@ -0,0 +1,712 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import unicodedata
5
+ from dataclasses import asdict, dataclass, field
6
+
7
+
8
+ CAPABILITY_PATTERNS = {
9
+ "vlan": [r"\bvlan(?:larda|lar|da|de|a|e)?\b", r"\btrunk\b", r"\baccess port\b", r"\brouter-on-a-stick\b", r"\bdot1q\b"],
10
+ "trunk": [r"\btrunk\b", r"\ballowed vlan\b", r"\bnative vlan\b"],
11
+ "access_port": [r"\baccess port\b", r"\baccess-port\b"],
12
+ "router_on_a_stick": [r"\brouter-on-a-stick\b", r"\bsubinterface\b", r"\bdot1q\b"],
13
+ "dhcp_pool": [r"\bdhcp\b", r"\bdhcp pool\b"],
14
+ "router_dhcp": [r"\bdhcp\b", r"\bdhcp pool\b", r"\bdefault-router\b"],
15
+ "server_dhcp": [r"\bserver dhcp\b", r"\bdhcp server\b"],
16
+ "dns": [r"\bdns\b", r"\ba record\b", r"\bcname\b"],
17
+ "server_dns": [r"\bdns\b", r"\ba record\b", r"\bcname\b"],
18
+ "server_http": [r"\bhttp\b", r"\bweb\b"],
19
+ "server_ftp": [r"\bftp\b", r"\btftp\b"],
20
+ "management_vlan": [r"\bmanagement vlan\b", r"\bvlan 99\b", r"\bdefault-gateway\b"],
21
+ "telnet": [r"\btelnet\b", r"\bvty\b", r"\btransport input telnet\b"],
22
+ "wireless_ap": [r"\bssid\b", r"\bwifi\b", r"\bwpa\b", r"\bwpa2\b", r"\bwireless\b", r"\baccess point\b", r"\bap\b"],
23
+ "wireless_client": [r"\btablet\b", r"\blaptop\b", r"\bsmartphone\b", r"\bassociate\b", r"\bjoin\b"],
24
+ "tablet": [r"\btablet\b"],
25
+ "printer": [r"\bprinter\b"],
26
+ "verification": [r"\bshow vlan brief\b", r"\bshow interfaces trunk\b", r"\bshow ip interface brief\b", r"\bshow ip dhcp binding\b", r"\bping\b", r"\btelnet\b"],
27
+ "ospf": [r"\bospf\b"],
28
+ "eigrp": [r"\beigrp\b"],
29
+ "rip": [r"\brip\b"],
30
+ "nat": [r"\bnat\b"],
31
+ "acl": [r"\bacl\b", r"\baccess-list\b"],
32
+ }
33
+
34
+ DEVICE_SYNONYMS = {
35
+ "router": "Router",
36
+ "switch": "Switch",
37
+ "pc": "PC",
38
+ "server": "Server",
39
+ "access-point": "LightWeightAccessPoint",
40
+ "accesspoint": "LightWeightAccessPoint",
41
+ "ap": "LightWeightAccessPoint",
42
+ "wireless-router": "WirelessRouter",
43
+ "wirelessrouter": "WirelessRouter",
44
+ "tablet": "Tablet",
45
+ "laptop": "Laptop",
46
+ "smartphone": "Smartphone",
47
+ "printer": "Printer",
48
+ }
49
+
50
+ NATURAL_DEVICE_ALIASES = {
51
+ "Router": ["router", "routerler", "routerlerin"],
52
+ "Switch": ["switch", "switchler", "switchlerin"],
53
+ "PC": ["pc", "pcs", "computer", "computers", "komputer", "komputerler", "kompyuter", "kompyuterler"],
54
+ "Server": ["server", "serverler"],
55
+ "Tablet": ["tablet", "tabletler"],
56
+ "Laptop": ["laptop", "laptoplar"],
57
+ "Printer": ["printer", "printerler"],
58
+ "LightWeightAccessPoint": ["ap", "aps", "accesspoint", "access-point", "access point", "apler"],
59
+ }
60
+
61
+ NETWORK_STYLE_PATTERNS = {
62
+ "campus": [r"\bcampus\b", r"\bkampus\b", r"\bsobeli\b", r"\bdepart", r"\bdepartment\b"],
63
+ "branch": [r"\bbranch\b", r"\bfilial\b"],
64
+ "small_office": [r"\bsmall office\b", r"\bhome\b", r"\bofis\b"],
65
+ "wireless_branch": [r"\bwireless\b", r"\bwifi\b", r"\bssid\b"],
66
+ }
67
+
68
+ PER_DEPARTMENT_DEVICE_ALIASES = {
69
+ "PC": ["pc", "komputer", "computer"],
70
+ "Printer": ["printer"],
71
+ "LightWeightAccessPoint": ["ap", "access point", "accesspoint"],
72
+ "Tablet": ["tablet"],
73
+ "Laptop": ["laptop"],
74
+ "Server": ["server"],
75
+ }
76
+
77
+ TOPOLOGY_HINT_WORDS = [
78
+ "switch",
79
+ "switchler",
80
+ "router",
81
+ "komputer",
82
+ "pc",
83
+ "server",
84
+ "printer",
85
+ "ap",
86
+ "tablet",
87
+ "qosul",
88
+ "qo",
89
+ "gig",
90
+ "fastethernet",
91
+ "vlan",
92
+ ]
93
+
94
+ SECURITY_TO_AUTH = {
95
+ "open": ("0", "0"),
96
+ "wep": ("1", "1"),
97
+ "wpa-psk": ("3", "3"),
98
+ "wpa2-psk": ("4", "4"),
99
+ "wpa": ("2", "2"),
100
+ "wpa2": ("4", "4"),
101
+ "802.1x": ("5", "5"),
102
+ }
103
+
104
+ TRANSLITERATION_TABLE = str.maketrans(
105
+ {
106
+ "ə": "e",
107
+ "Ə": "e",
108
+ "ş": "s",
109
+ "Ş": "s",
110
+ "ı": "i",
111
+ "İ": "i",
112
+ "ç": "c",
113
+ "Ç": "c",
114
+ "ö": "o",
115
+ "Ö": "o",
116
+ "ü": "u",
117
+ "Ü": "u",
118
+ "ğ": "g",
119
+ "Ğ": "g",
120
+ "â": "a",
121
+ "Â": "a",
122
+ "ê": "e",
123
+ "Ê": "e",
124
+ }
125
+ )
126
+
127
+
128
+ def _command_segments(prompt: str) -> list[str]:
129
+ parts = re.split(r"(?=\b(?:device|connect|link|set|enable|associate|change|update|rename|apply)\b)", prompt, flags=re.IGNORECASE)
130
+ return [part.strip() for part in parts if part.strip()]
131
+
132
+
133
+ def _normalize_prompt(prompt: str) -> str:
134
+ text = prompt.translate(TRANSLITERATION_TABLE)
135
+ for source, target in {
136
+ "É™": "e",
137
+ "Ə": "e",
138
+ "ÅŸ": "s",
139
+ "Ş": "s",
140
+ "ı": "i",
141
+ "İ": "i",
142
+ "ç": "c",
143
+ "Ç": "c",
144
+ "ö": "o",
145
+ "Ö": "o",
146
+ "ü": "u",
147
+ "Ü": "u",
148
+ "ÄŸ": "g",
149
+ "Ğ": "g",
150
+ }.items():
151
+ text = text.replace(source, target)
152
+ text = unicodedata.normalize("NFKD", text)
153
+ text = text.encode("ascii", "ignore").decode("ascii")
154
+ text = text.lower()
155
+ text = re.sub(r"[^a-z0-9./\\,:_-]+", " ", text)
156
+ text = re.sub(r"\s+", " ", text).strip()
157
+ return text
158
+
159
+
160
+ @dataclass
161
+ class IntentPlan:
162
+ goal: str
163
+ prompt: str
164
+ pkt_path: str | None = None
165
+ capabilities: list[str] = field(default_factory=list)
166
+ network_style: str | None = None
167
+ device_requirements: dict[str, int] = field(default_factory=dict)
168
+ device_counts: dict[str, int] = field(default_factory=dict)
169
+ department_groups: list[dict[str, object]] = field(default_factory=list)
170
+ service_requirements: dict[str, object] = field(default_factory=dict)
171
+ topology_requirements: dict[str, object] = field(default_factory=dict)
172
+ vlan_ids: list[int] = field(default_factory=list)
173
+ uplink_intent: str | None = None
174
+ host_link_intent: str | None = None
175
+ host_vlan_assignment: dict[int, int] = field(default_factory=dict)
176
+ assumptions_used: list[str] = field(default_factory=list)
177
+ confidence_score: float = 0.0
178
+ parse_warnings: list[str] = field(default_factory=list)
179
+ blocking_gaps: list[str] = field(default_factory=list)
180
+ edit_operations: list[dict[str, object]] = field(default_factory=list)
181
+ devices: list[dict[str, object]] = field(default_factory=list)
182
+ links: list[dict[str, object]] = field(default_factory=list)
183
+ switch_ops: list[dict[str, object]] = field(default_factory=list)
184
+ router_ops: list[dict[str, object]] = field(default_factory=list)
185
+ server_ops: list[dict[str, object]] = field(default_factory=list)
186
+ wireless_ops: list[dict[str, object]] = field(default_factory=list)
187
+ end_device_ops: list[dict[str, object]] = field(default_factory=list)
188
+ management_ops: list[dict[str, object]] = field(default_factory=list)
189
+ verification_ops: list[dict[str, object]] = field(default_factory=list)
190
+
191
+ def all_operations(self) -> list[dict[str, object]]:
192
+ merged: list[dict[str, object]] = []
193
+ for bucket in [
194
+ self.edit_operations,
195
+ self.switch_ops,
196
+ self.router_ops,
197
+ self.server_ops,
198
+ self.wireless_ops,
199
+ self.end_device_ops,
200
+ self.management_ops,
201
+ self.verification_ops,
202
+ ]:
203
+ merged.extend(bucket)
204
+ return merged
205
+
206
+ def to_dict(self) -> dict[str, object]:
207
+ data = asdict(self)
208
+ data["all_operations"] = self.all_operations()
209
+ return data
210
+
211
+
212
+ def _normalize_device_type(raw_type: str) -> str:
213
+ return DEVICE_SYNONYMS.get(raw_type.strip().lower(), raw_type.strip())
214
+
215
+
216
+ def _extract_natural_device_counts(normalized_prompt: str) -> dict[str, int]:
217
+ counts: dict[str, int] = {}
218
+ for device_type, aliases in NATURAL_DEVICE_ALIASES.items():
219
+ alias_pattern = "|".join(re.escape(alias) for alias in aliases)
220
+ patterns = [
221
+ re.compile(rf"(?<![,/])\b(\d+)\s*(?:dene|dene?|eded|eded|tane)?\s*(?:{alias_pattern})\b"),
222
+ re.compile(rf"\b(?:{alias_pattern})\s+(\d+)\b"),
223
+ ]
224
+ values: list[int] = []
225
+ for pattern in patterns:
226
+ values.extend(int(value) for value in pattern.findall(normalized_prompt))
227
+ if values:
228
+ counts[device_type] = max(values)
229
+ return counts
230
+
231
+
232
+ def _extract_vlan_ids(normalized_prompt: str) -> list[int]:
233
+ vlan_ids: list[int] = []
234
+ for match in re.findall(r"\bvlan(?:larda|lar|da|de|a|e)?\s+([0-9,\s/veand]+)", normalized_prompt):
235
+ vlan_ids.extend(int(value) for value in re.findall(r"\d+", match))
236
+ return sorted(dict.fromkeys(vlan_ids))
237
+
238
+
239
+ def _extract_host_vlan_assignment(normalized_prompt: str) -> dict[int, int]:
240
+ assignments: dict[int, int] = {}
241
+ patterns = [
242
+ re.compile(r"\bvlan\s+(\d+)\s*(?:da|de|a|e)?\s+(\d+)\s+(?:pc|komputer|computer)\b"),
243
+ re.compile(r"\b(\d+)\s+(?:pc|komputer|computer)\s+vlan(?:da|de|a|e)?\s+(\d+)\b"),
244
+ ]
245
+ for pattern_index, pattern in enumerate(patterns):
246
+ for first, second in pattern.findall(normalized_prompt):
247
+ if pattern_index == 0:
248
+ vlan_id, count = int(first), int(second)
249
+ else:
250
+ count, vlan_id = int(first), int(second)
251
+ assignments[vlan_id] = assignments.get(vlan_id, 0) + count
252
+ return assignments
253
+
254
+
255
+ def _extract_uplink_intent(normalized_prompt: str) -> str | None:
256
+ if any(token in normalized_prompt for token in ["gig port", "gigabit", " gig ", "gi0", "routerle aralarinda gig", "switchlerin oz aralarinda"]):
257
+ return "gigabit"
258
+ return None
259
+
260
+
261
+ def _extract_host_link_intent(normalized_prompt: str) -> str | None:
262
+ if any(token in normalized_prompt for token in ["fa port", "fastethernet", " fa ", "pc ler ise fa", "komputerler ise fa"]):
263
+ return "fastethernet"
264
+ return None
265
+
266
+
267
+ def _extract_network_style(normalized_prompt: str) -> str | None:
268
+ for style, patterns in NETWORK_STYLE_PATTERNS.items():
269
+ if any(re.search(pattern, normalized_prompt) for pattern in patterns):
270
+ return style
271
+ return None
272
+
273
+
274
+ def _extract_department_count(normalized_prompt: str) -> int:
275
+ patterns = [
276
+ re.compile(r"\b(\d+)\s+(?:sobeli|sobe|department|departamentli)\b"),
277
+ re.compile(r"\b(\d+)\s+(?:depart(?:ment)?|bolme)\b"),
278
+ ]
279
+ for pattern in patterns:
280
+ match = pattern.search(normalized_prompt)
281
+ if match:
282
+ return int(match.group(1))
283
+ return 0
284
+
285
+
286
+ def _extract_per_department_devices(normalized_prompt: str) -> dict[str, int]:
287
+ counts: dict[str, int] = {}
288
+ group_segment = ""
289
+ segment_match = re.search(
290
+ r"\bher\s+(?:sobede|bolmede|departmentda)\s+(.+?)(?=\b(?:router|dhcp|vlan|ssid|acl|telnet|ospf|eigrp|rip|nat)\b|$)",
291
+ normalized_prompt,
292
+ )
293
+ if segment_match:
294
+ group_segment = segment_match.group(1)
295
+ for device_type, aliases in PER_DEPARTMENT_DEVICE_ALIASES.items():
296
+ alias_pattern = "|".join(re.escape(alias) for alias in aliases)
297
+ patterns = [
298
+ re.compile(rf"\bher\s+(?:sobede|bolmede|departmentda)\s+(\d+)\s+(?:dene\s+)?(?:{alias_pattern})\b"),
299
+ re.compile(rf"\beach\s+(?:department|group)\s+(\d+)\s+(?:{alias_pattern})\b"),
300
+ re.compile(rf"\b(\d+)\s+(?:dene\s+)?(?:{alias_pattern})\b"),
301
+ ]
302
+ values: list[int] = []
303
+ for pattern in patterns:
304
+ target_text = group_segment if pattern.pattern.startswith(r"\b(\d+)") and group_segment else normalized_prompt
305
+ values.extend(int(value) for value in pattern.findall(target_text))
306
+ if values:
307
+ counts[device_type] = max(values)
308
+ return counts
309
+
310
+
311
+ def _build_department_groups(
312
+ normalized_prompt: str,
313
+ department_count: int,
314
+ per_department_devices: dict[str, int],
315
+ vlan_ids: list[int],
316
+ ) -> list[dict[str, object]]:
317
+ if department_count <= 0:
318
+ return []
319
+ groups: list[dict[str, object]] = []
320
+ for index in range(department_count):
321
+ group_name = f"DEPT{index + 1}"
322
+ groups.append(
323
+ {
324
+ "name": group_name,
325
+ "switch_name": f"{group_name}-SW",
326
+ "vlan_id": vlan_ids[index] if index < len(vlan_ids) else None,
327
+ "devices": dict(per_department_devices),
328
+ }
329
+ )
330
+ return groups
331
+
332
+
333
+ def _extract_service_requirements(capabilities: list[str], prompt: str) -> dict[str, object]:
334
+ lowered = prompt.lower()
335
+ requirements: dict[str, object] = {
336
+ "routing": next((cap for cap in ["ospf", "eigrp", "rip"] if cap in capabilities), None),
337
+ "services": [],
338
+ "security": [],
339
+ }
340
+ for service in ["dhcp", "dns", "http", "https", "ftp", "tftp", "ntp"]:
341
+ if service in lowered:
342
+ requirements["services"].append(service)
343
+ for security in ["acl", "telnet", "wpa2", "wpa", "wep", "nat"]:
344
+ if security in lowered:
345
+ requirements["security"].append(security)
346
+ return requirements
347
+
348
+
349
+ def _estimate_confidence(
350
+ device_requirements: dict[str, int],
351
+ capabilities: list[str],
352
+ parse_warnings: list[str],
353
+ blocking_gaps: list[str],
354
+ explicit_devices: list[dict[str, object]],
355
+ links: list[dict[str, object]],
356
+ ) -> float:
357
+ score = 0.2
358
+ if device_requirements or explicit_devices:
359
+ score += 0.25
360
+ if capabilities:
361
+ score += 0.2
362
+ if links:
363
+ score += 0.15
364
+ score -= min(len(parse_warnings) * 0.08, 0.16)
365
+ score -= min(len(blocking_gaps) * 0.2, 0.4)
366
+ return max(0.0, min(1.0, round(score, 2)))
367
+
368
+
369
+ def _extract_explicit_devices(prompt: str) -> list[dict[str, object]]:
370
+ devices: list[dict[str, object]] = []
371
+ pattern = re.compile(
372
+ r"(?:device\s+)?([A-Za-z][A-Za-z0-9_-]*)\s+type\s+([A-Za-z-]+)(?:\s+model\s+([A-Za-z0-9._-]+))?",
373
+ flags=re.IGNORECASE,
374
+ )
375
+ for segment in _command_segments(prompt):
376
+ for name, raw_type, model in pattern.findall(segment):
377
+ devices.append({"name": name, "type": _normalize_device_type(raw_type), "model": model or None})
378
+ return devices
379
+
380
+
381
+ def _extract_explicit_links(prompt: str) -> list[dict[str, object]]:
382
+ links: list[dict[str, object]] = []
383
+ pattern = re.compile(
384
+ r"(?:connect|link)\s+([A-Za-z][A-Za-z0-9_-]*):([A-Za-z0-9/._-]+)\s+(?:to|->)\s+([A-Za-z][A-Za-z0-9_-]*):([A-Za-z0-9/._-]+)"
385
+ r"(?:\s+with\s+([A-Za-z0-9._-]+))?",
386
+ flags=re.IGNORECASE,
387
+ )
388
+ for segment in _command_segments(prompt):
389
+ for left_dev, left_port, right_dev, right_port, cable in pattern.findall(segment):
390
+ links.append(
391
+ {
392
+ "a": {"dev": left_dev, "port": left_port},
393
+ "b": {"dev": right_dev, "port": right_port},
394
+ "media": (cable or "copper").lower(),
395
+ }
396
+ )
397
+ deduped: list[dict[str, object]] = []
398
+ seen: set[tuple[str, str, str, str, str]] = set()
399
+ for link in links:
400
+ key = (
401
+ str(link["a"]["dev"]),
402
+ str(link["a"]["port"]),
403
+ str(link["b"]["dev"]),
404
+ str(link["b"]["port"]),
405
+ str(link["media"]),
406
+ )
407
+ if key not in seen:
408
+ seen.add(key)
409
+ deduped.append(link)
410
+ return deduped
411
+
412
+
413
+ def _extract_link_edit_operations(links: list[dict[str, object]]) -> list[dict[str, object]]:
414
+ return [{"op": "set_link", **link} for link in links]
415
+
416
+
417
+ def _extract_switch_ops(prompt: str) -> list[dict[str, object]]:
418
+ ops: list[dict[str, object]] = []
419
+ for segment in _command_segments(prompt):
420
+ for device, vlan_id, name in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+vlan\s+(\d+)\s+name\s+([A-Za-z0-9_-]+)", segment, flags=re.IGNORECASE):
421
+ ops.append({"op": "set_vlan", "device": device, "vlan": int(vlan_id), "name": name})
422
+ for device, port, vlan_id in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+access-port\s+([A-Za-z0-9/._-]+)\s+vlan\s+(\d+)", segment, flags=re.IGNORECASE):
423
+ ops.append({"op": "set_access_port", "device": device, "port": port, "vlan": int(vlan_id)})
424
+ trunk_pattern = re.compile(
425
+ r"set\s+([A-Za-z0-9_-]+)\s+trunk-port\s+([A-Za-z0-9/._-]+)\s+allowed\s+([0-9,\s]+)(?:\s+native\s+(\d+))?",
426
+ flags=re.IGNORECASE,
427
+ )
428
+ for segment in _command_segments(prompt):
429
+ for device, port, allowed, native in trunk_pattern.findall(segment):
430
+ ops.append(
431
+ {
432
+ "op": "set_trunk_port",
433
+ "device": device,
434
+ "port": port,
435
+ "allowed": [int(value) for value in re.findall(r"\d+", allowed)],
436
+ "native": int(native) if native else None,
437
+ }
438
+ )
439
+ return ops
440
+
441
+
442
+ def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
443
+ ops: list[dict[str, object]] = []
444
+ subif_pattern = re.compile(
445
+ r"set\s+([A-Za-z0-9_-]+)\s+subinterface\s+([A-Za-z0-9/._-]+)\s+encapsulation\s+dot1q\s+(\d+)\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+)",
446
+ flags=re.IGNORECASE,
447
+ )
448
+ for segment in _command_segments(prompt):
449
+ for device, subinterface, vlan_id, ip, prefix in subif_pattern.findall(segment):
450
+ ops.append({"op": "set_subinterface", "device": device, "subinterface": subinterface, "vlan": int(vlan_id), "ip": ip, "prefix": int(prefix)})
451
+ dhcp_pattern = re.compile(
452
+ r"set\s+([A-Za-z0-9_-]+)\s+dhcp\s+pool\s+([A-Za-z0-9_-]+)\s+network\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+gateway\s+(\d+\.\d+\.\d+\.\d+)"
453
+ r"(?:\s+dns\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+start\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+max\s+(\d+))?",
454
+ flags=re.IGNORECASE,
455
+ )
456
+ for segment in _command_segments(prompt):
457
+ for device, name, network, prefix, gateway, dns, start, max_users in dhcp_pattern.findall(segment):
458
+ ops.append(
459
+ {
460
+ "op": "set_router_dhcp_pool",
461
+ "device": device,
462
+ "name": name,
463
+ "network": network,
464
+ "prefix": int(prefix),
465
+ "gateway": gateway,
466
+ "dns": dns or None,
467
+ "start": start or None,
468
+ "max_users": int(max_users) if max_users else None,
469
+ }
470
+ )
471
+ acl_create_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+acl\s+(standard|extended)\s+([A-Za-z0-9_-]+)", flags=re.IGNORECASE)
472
+ for segment in _command_segments(prompt):
473
+ for device, acl_kind, acl_name in acl_create_pattern.findall(segment):
474
+ ops.append({"op": "set_acl", "device": device, "acl_kind": acl_kind.lower(), "acl_name": acl_name})
475
+ acl_rule_pattern = re.compile(
476
+ r"acl\s+([A-Za-z0-9_-]+)\s+(permit|deny)\s+(host\s+\d+\.\d+\.\d+\.\d+|\d+\.\d+\.\d+\.\d+\s+\d+\.\d+\.\d+\.\d+|any)"
477
+ r"(?:\s+(host\s+\d+\.\d+\.\d+\.\d+|\d+\.\d+\.\d+\.\d+\s+\d+\.\d+\.\d+\.\d+|any))?",
478
+ flags=re.IGNORECASE,
479
+ )
480
+ for segment in _command_segments(prompt):
481
+ for acl_name, action, source, destination in acl_rule_pattern.findall(segment):
482
+ ops.append({"op": "add_acl_rule", "acl_name": acl_name, "action": action.lower(), "source": source.strip(), "destination": destination.strip() if destination else None})
483
+ acl_apply_pattern = re.compile(r"apply\s+acl\s+([A-Za-z0-9_-]+)\s+(in|out)\s+on\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
484
+ for segment in _command_segments(prompt):
485
+ for acl_name, direction, device, interface_name in acl_apply_pattern.findall(segment):
486
+ ops.append({"op": "apply_acl", "device": device, "acl_name": acl_name, "direction": direction.lower(), "interface": interface_name})
487
+ return ops
488
+
489
+
490
+ def _extract_server_ops(prompt: str) -> list[dict[str, object]]:
491
+ ops: list[dict[str, object]] = []
492
+ dns_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+dns\s+(A|CNAME)\s+([A-Za-z0-9._-]+)\s+([A-Za-z0-9._-]+)", flags=re.IGNORECASE)
493
+ for segment in _command_segments(prompt):
494
+ for device, record_type, name, value in dns_pattern.findall(segment):
495
+ ops.append({"op": "set_server_dns_record", "device": device, "record_type": record_type.upper(), "name": name, "value": value})
496
+ dhcp_pattern = re.compile(
497
+ r"set\s+([A-Za-z0-9_-]+)\s+server-dhcp\s+pool\s+([A-Za-z0-9_-]+)\s+network\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+gateway\s+(\d+\.\d+\.\d+\.\d+)"
498
+ r"(?:\s+dns\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+start\s+(\d+\.\d+\.\d+\.\d+))?(?:\s+max\s+(\d+))?",
499
+ flags=re.IGNORECASE,
500
+ )
501
+ for segment in _command_segments(prompt):
502
+ for device, name, network, prefix, gateway, dns, start, max_users in dhcp_pattern.findall(segment):
503
+ ops.append(
504
+ {
505
+ "op": "set_server_dhcp_pool",
506
+ "device": device,
507
+ "name": name,
508
+ "network": network,
509
+ "prefix": int(prefix),
510
+ "gateway": gateway,
511
+ "dns": dns or None,
512
+ "start": start or None,
513
+ "max_users": int(max_users) if max_users else 0,
514
+ }
515
+ )
516
+ for service, device in re.findall(r"enable\s+(dns|http|https|ftp|tftp|ntp)\s+on\s+([A-Za-z0-9_ -]+)", segment, flags=re.IGNORECASE):
517
+ ops.append({"op": "enable_server_service", "device": device.strip(), "service": service.lower()})
518
+ return ops
519
+
520
+
521
+ def _extract_management_ops(prompt: str) -> list[dict[str, object]]:
522
+ ops: list[dict[str, object]] = []
523
+ mgmt_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+management\s+vlan\s+(\d+)\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+gateway\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
524
+ for segment in _command_segments(prompt):
525
+ for device, vlan_id, ip, prefix, gateway in mgmt_pattern.findall(segment):
526
+ ops.append({"op": "set_management_vlan", "device": device, "vlan": int(vlan_id), "ip": ip, "prefix": int(prefix), "gateway": gateway})
527
+ telnet_pattern = re.compile(r"enable\s+telnet\s+on\s+([A-Za-z0-9_-]+)\s+username\s+([A-Za-z0-9._-]+)\s+password\s+([A-Za-z0-9._-]+)", flags=re.IGNORECASE)
528
+ for segment in _command_segments(prompt):
529
+ for device, username, password in telnet_pattern.findall(segment):
530
+ ops.append({"op": "enable_telnet", "device": device, "username": username, "password": password})
531
+ return ops
532
+
533
+
534
+ def _extract_wireless_ops(prompt: str) -> list[dict[str, object]]:
535
+ ops: list[dict[str, object]] = []
536
+ ssid_pattern = re.compile(
537
+ r"set\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)(?:\s+security\s+([A-Za-z0-9.-]+))?(?:\s+passphrase\s+([A-Za-z0-9._-]+))?(?:\s+channel\s+(\d+))?",
538
+ flags=re.IGNORECASE,
539
+ )
540
+ for segment in _command_segments(prompt):
541
+ for device, ssid, security, passphrase, channel in ssid_pattern.findall(segment):
542
+ security_key = (security or "open").lower()
543
+ auth_type, encrypt_type = SECURITY_TO_AUTH.get(security_key, ("0", "0"))
544
+ ops.append({"op": "set_wireless_ssid", "device": device.strip(), "ssid": ssid, "security": security_key, "auth_type": auth_type, "encrypt_type": encrypt_type, "passphrase": passphrase or "", "channel": int(channel) if channel else 1})
545
+ assoc_pattern = re.compile(r"associate\s+([A-Za-z0-9_ -]+?)\s+to\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)(?:\s+(dhcp|static))?", flags=re.IGNORECASE)
546
+ for segment in _command_segments(prompt):
547
+ for client, ap, ssid, ip_mode in assoc_pattern.findall(segment):
548
+ ops.append({"op": "associate_wireless_client", "device": client.strip(), "ap": ap.strip(), "ssid": ssid, "ip_mode": (ip_mode or "dhcp").lower()})
549
+ return ops
550
+
551
+
552
+ def _extract_end_device_ops(prompt: str) -> list[dict[str, object]]:
553
+ ops: list[dict[str, object]] = []
554
+ ip_pattern = re.compile(
555
+ r"(?:set|change|update)\s+([A-Za-z0-9_-]+)\s+ip\s+(\d+\.\d+\.\d+\.\d+)"
556
+ r"(?:\s+mask\s+(\d+\.\d+\.\d+\.\d+))?"
557
+ r"(?:\s+gw\s+(\d+\.\d+\.\d+\.\d+))?"
558
+ r"(?:\s+dns\s+(\d+\.\d+\.\d+\.\d+))?",
559
+ flags=re.IGNORECASE,
560
+ )
561
+ for segment in _command_segments(prompt):
562
+ for device, ip, mask, gw, dns in ip_pattern.findall(segment):
563
+ operation: dict[str, object] = {"op": "set_host_ip", "device": device, "ip": ip}
564
+ if mask:
565
+ operation["mask"] = mask
566
+ if gw:
567
+ operation["gw"] = gw
568
+ if dns:
569
+ operation["dns"] = dns
570
+ ops.append(operation)
571
+ for device in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+ipv4\s+dhcp", segment, flags=re.IGNORECASE):
572
+ ops.append({"op": "set_host_dhcp", "device": device})
573
+ for device, dns in re.findall(r"set\s+([A-Za-z0-9_-]+)\s+dns\s+(\d+\.\d+\.\d+\.\d+)", segment, flags=re.IGNORECASE):
574
+ ops.append({"op": "set_host_dns", "device": device, "dns": dns})
575
+ return ops
576
+
577
+
578
+ def _extract_verification_ops(prompt: str) -> list[dict[str, object]]:
579
+ ops: list[dict[str, object]] = []
580
+ lowered = prompt.lower()
581
+ for command in ["show vlan brief", "show interfaces trunk", "show ip interface brief", "show ip dhcp binding"]:
582
+ if command in lowered:
583
+ ops.append({"op": "verify_command", "command": command})
584
+ if "ping" in lowered:
585
+ ops.append({"op": "verify_ping"})
586
+ if "telnet" in lowered:
587
+ ops.append({"op": "verify_telnet"})
588
+ return ops
589
+
590
+
591
+ def parse_intent(prompt: str) -> IntentPlan:
592
+ pkt_path_match = re.search(r"([A-Za-z]:\\[^\"\n]+?\.pkt)\b", prompt, flags=re.IGNORECASE)
593
+ pkt_path = pkt_path_match.group(1) if pkt_path_match else None
594
+ lowered = prompt.lower()
595
+ normalized_prompt = _normalize_prompt(prompt)
596
+
597
+ capabilities = sorted(capability for capability, patterns in CAPABILITY_PATTERNS.items() if any(re.search(pattern, lowered) for pattern in patterns))
598
+ devices = _extract_explicit_devices(prompt)
599
+ links = _extract_explicit_links(prompt)
600
+ device_counts = _extract_natural_device_counts(normalized_prompt)
601
+ vlan_ids = _extract_vlan_ids(normalized_prompt)
602
+ uplink_intent = _extract_uplink_intent(normalized_prompt)
603
+ host_link_intent = _extract_host_link_intent(normalized_prompt)
604
+ host_vlan_assignment = _extract_host_vlan_assignment(normalized_prompt)
605
+ network_style = _extract_network_style(normalized_prompt)
606
+ department_count = _extract_department_count(normalized_prompt)
607
+ per_department_devices = _extract_per_department_devices(normalized_prompt)
608
+
609
+ device_requirements = dict(device_counts)
610
+ for device in devices:
611
+ device_type = str(device["type"])
612
+ device_requirements[device_type] = device_requirements.get(device_type, 0) + 1
613
+
614
+ department_groups = _build_department_groups(normalized_prompt, department_count, per_department_devices, vlan_ids)
615
+ if department_groups:
616
+ device_requirements["Switch"] = max(device_requirements.get("Switch", 0), department_count)
617
+ for device_type, per_group_count in per_department_devices.items():
618
+ device_requirements[device_type] = max(device_requirements.get(device_type, 0), department_count * per_group_count)
619
+
620
+ topology_requirements: dict[str, object] = {}
621
+ if vlan_ids:
622
+ topology_requirements["vlan_ids"] = vlan_ids
623
+ if uplink_intent:
624
+ topology_requirements["uplink_intent"] = uplink_intent
625
+ if host_link_intent:
626
+ topology_requirements["host_link_intent"] = host_link_intent
627
+ if host_vlan_assignment:
628
+ topology_requirements["host_vlan_assignment"] = host_vlan_assignment
629
+ if device_requirements.get("Switch", 0) > 1:
630
+ topology_requirements["uplink_topology"] = "core_switch"
631
+ if department_groups:
632
+ topology_requirements["uplink_topology"] = "chain"
633
+ topology_requirements["department_count"] = department_count
634
+ if "router_dhcp" in capabilities or "dhcp_pool" in capabilities:
635
+ topology_requirements["needs_dhcp_pool"] = True
636
+ routing = next((cap for cap in ["ospf", "eigrp", "rip"] if cap in capabilities), None)
637
+ if routing:
638
+ topology_requirements["routing_protocol"] = routing
639
+ service_requirements = _extract_service_requirements(capabilities, prompt)
640
+
641
+ edit_operations: list[dict[str, object]] = []
642
+ for old_name, new_name in re.findall(r"rename\s+([A-Za-z0-9_-]+)\s+to\s+([A-Za-z0-9_-]+)", prompt, flags=re.IGNORECASE):
643
+ edit_operations.append({"op": "rename_device", "device": old_name, "new_name": new_name})
644
+ edit_operations.extend(_extract_link_edit_operations(links))
645
+
646
+ switch_ops = _extract_switch_ops(prompt)
647
+ router_ops = _extract_router_ops(prompt)
648
+ server_ops = _extract_server_ops(prompt)
649
+ wireless_ops = _extract_wireless_ops(prompt)
650
+ end_device_ops = _extract_end_device_ops(prompt)
651
+ management_ops = _extract_management_ops(prompt)
652
+ verification_ops = _extract_verification_ops(prompt)
653
+
654
+ parse_warnings: list[str] = []
655
+ blocking_gaps: list[str] = []
656
+ assumptions_used: list[str] = []
657
+ if any(word in normalized_prompt for word in TOPOLOGY_HINT_WORDS) and not device_requirements and not devices:
658
+ parse_warnings.append("Prompt includes topology words but no stable device counts were parsed.")
659
+ if any(op["op"] == "set_management_vlan" for op in management_ops):
660
+ topology_requirements["management_vlan"] = True
661
+
662
+ pc_count = device_requirements.get("PC", 0)
663
+ if department_groups and vlan_ids and len(vlan_ids) >= len(department_groups) and not host_vlan_assignment:
664
+ for index, group in enumerate(department_groups):
665
+ vlan_id = vlan_ids[index]
666
+ host_vlan_assignment[vlan_id] = host_vlan_assignment.get(vlan_id, 0) + int(group["devices"].get("PC", 0))
667
+ assumptions_used.append("Assigned each department's PCs to the matching VLAN order.")
668
+ if vlan_ids and pc_count and not host_vlan_assignment and not any(op["op"] == "set_access_port" for op in switch_ops):
669
+ blocking_gaps.append("Host-to-VLAN assignment is missing. Specify how many PCs belong to each VLAN.")
670
+ if vlan_ids and not device_requirements.get("Switch", 0) and not any(device.get("type") == "Switch" for device in devices):
671
+ blocking_gaps.append("VLAN planning requires at least one switch.")
672
+ if department_groups and vlan_ids and len(vlan_ids) < len(department_groups):
673
+ blocking_gaps.append("Department count is larger than provided VLAN IDs.")
674
+
675
+ if department_groups and "Switch" not in device_counts:
676
+ assumptions_used.append("Added one switch per department group.")
677
+ if department_groups and not network_style:
678
+ network_style = "campus"
679
+ assumptions_used.append("Interpreted department-based prompt as campus style.")
680
+
681
+ goal = "edit" if pkt_path or any(word in normalized_prompt for word in ["deyis", "edit", "modify", "change", "rename", "update"]) else "generate"
682
+ confidence_score = _estimate_confidence(device_requirements, capabilities, parse_warnings, blocking_gaps, devices, links)
683
+ return IntentPlan(
684
+ goal=goal,
685
+ prompt=prompt,
686
+ pkt_path=pkt_path,
687
+ capabilities=capabilities,
688
+ network_style=network_style,
689
+ device_requirements=device_requirements,
690
+ device_counts=device_counts,
691
+ department_groups=department_groups,
692
+ service_requirements=service_requirements,
693
+ topology_requirements=topology_requirements,
694
+ vlan_ids=vlan_ids,
695
+ uplink_intent=uplink_intent,
696
+ host_link_intent=host_link_intent,
697
+ host_vlan_assignment=host_vlan_assignment,
698
+ assumptions_used=assumptions_used,
699
+ confidence_score=confidence_score,
700
+ parse_warnings=parse_warnings,
701
+ blocking_gaps=blocking_gaps,
702
+ edit_operations=edit_operations,
703
+ devices=devices,
704
+ links=links,
705
+ switch_ops=switch_ops,
706
+ router_ops=router_ops,
707
+ server_ops=server_ops,
708
+ wireless_ops=wireless_ops,
709
+ end_device_ops=end_device_ops,
710
+ management_ops=management_ops,
711
+ verification_ops=verification_ops,
712
+ )