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.
- package/LICENSE +21 -0
- package/LICENSES/LICENSE.Twofish-BSD-3-Clause.txt +29 -0
- package/README.md +687 -0
- package/SKILL.md +221 -0
- package/bin/packet-tracer-skill.js +635 -0
- package/examples/blueprint_minimal.json +46 -0
- package/package.json +42 -0
- package/references/packettracer-sample-catalog.json +21410 -0
- package/references/packettracer-sample-catalog.md +1124 -0
- package/references/pkt-format.md +57 -0
- package/references/xml-skeleton-notes.md +44 -0
- package/requirements-dev.txt +1 -0
- package/requirements.txt +6 -0
- package/scripts/build_sample_catalog.py +65 -0
- package/scripts/donor_diagnostics.py +35 -0
- package/scripts/generate_pkt.py +1264 -0
- package/scripts/install_skill.py +71 -0
- package/scripts/intent_parser.py +712 -0
- package/scripts/packet_tracer_env.py +278 -0
- package/scripts/pkt_builder.py +15 -0
- package/scripts/pkt_codec.py +181 -0
- package/scripts/pkt_editor.py +752 -0
- package/scripts/pkt_transformer.py +541 -0
- package/scripts/sample_catalog.py +385 -0
- package/scripts/sample_selector.py +156 -0
- package/scripts/setup.ps1 +26 -0
- package/scripts/twofish_diagnostics.py +91 -0
- package/scripts/vendor/README.md +46 -0
- package/scripts/vendor/twofish.py +81 -0
- package/scripts/workspace_repair.py +441 -0
- package/templates/pt900/base_empty.xml +21 -0
- package/templates/pt900/device_library/pc.xml +20 -0
- package/templates/pt900/device_library/printer.xml +432 -0
- package/templates/pt900/device_library/router.xml +16 -0
- 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
|
+
)
|