cloudshellgpt 1.0.0__py3-none-any.whl
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.
- cloudshellgpt/__init__.py +11 -0
- cloudshellgpt/audit.py +175 -0
- cloudshellgpt/bedrock_translator.py +525 -0
- cloudshellgpt/cli.py +612 -0
- cloudshellgpt/config.py +296 -0
- cloudshellgpt/cost.py +443 -0
- cloudshellgpt/executor.py +382 -0
- cloudshellgpt/formatter.py +328 -0
- cloudshellgpt/i18n.py +203 -0
- cloudshellgpt/intent.py +1080 -0
- cloudshellgpt/learning.py +969 -0
- cloudshellgpt/mcp_server.py +264 -0
- cloudshellgpt/safety.py +952 -0
- cloudshellgpt-1.0.0.dist-info/METADATA +426 -0
- cloudshellgpt-1.0.0.dist-info/RECORD +18 -0
- cloudshellgpt-1.0.0.dist-info/WHEEL +4 -0
- cloudshellgpt-1.0.0.dist-info/entry_points.txt +2 -0
- cloudshellgpt-1.0.0.dist-info/licenses/LICENSE +198 -0
cloudshellgpt/intent.py
ADDED
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
"""Intent parser — converts natural language into structured Intent objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Literal, get_args
|
|
7
|
+
|
|
8
|
+
import langdetect
|
|
9
|
+
import langdetect.detector_factory
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
# Seed para determinismo en langdetect
|
|
13
|
+
langdetect.detector_factory.LangDetectException = (
|
|
14
|
+
langdetect.lang_detect_exception.LangDetectException
|
|
15
|
+
)
|
|
16
|
+
langdetect.DetectorFactory.seed = 0
|
|
17
|
+
|
|
18
|
+
# --- Constants ---
|
|
19
|
+
|
|
20
|
+
SUPPORTED_LANGUAGES = ("es", "en", "pt", "fr", "de", "zh-cn", "zh-tw", "zh")
|
|
21
|
+
|
|
22
|
+
ActionType = Literal["list", "create", "delete", "update", "describe", "invoke", "unknown"]
|
|
23
|
+
|
|
24
|
+
# Keywords por servicio, multi-idioma: ES, EN, PT, FR, DE, ZH
|
|
25
|
+
SERVICE_KEYWORDS: dict[str, list[str]] = {
|
|
26
|
+
"s3": [
|
|
27
|
+
"s3",
|
|
28
|
+
"bucket",
|
|
29
|
+
"buckets",
|
|
30
|
+
# ES
|
|
31
|
+
"almacenamiento",
|
|
32
|
+
# PT
|
|
33
|
+
"armazenamento",
|
|
34
|
+
"balde",
|
|
35
|
+
# FR
|
|
36
|
+
"stockage",
|
|
37
|
+
"seau",
|
|
38
|
+
# DE
|
|
39
|
+
"speicher",
|
|
40
|
+
"eimer",
|
|
41
|
+
# ZH
|
|
42
|
+
"\u5b58\u50a8\u6876",
|
|
43
|
+
"\u5bf9\u8c61\u5b58\u50a8",
|
|
44
|
+
],
|
|
45
|
+
"ec2": [
|
|
46
|
+
"ec2",
|
|
47
|
+
"instance",
|
|
48
|
+
"instances",
|
|
49
|
+
# ES
|
|
50
|
+
"instancia",
|
|
51
|
+
"instancias",
|
|
52
|
+
"servidor",
|
|
53
|
+
"servidores",
|
|
54
|
+
# EN
|
|
55
|
+
"vm",
|
|
56
|
+
"virtual machine",
|
|
57
|
+
# PT
|
|
58
|
+
"inst\u00e2ncia",
|
|
59
|
+
"inst\u00e2ncias",
|
|
60
|
+
"servidor",
|
|
61
|
+
# FR
|
|
62
|
+
"serveur",
|
|
63
|
+
"machine virtuelle",
|
|
64
|
+
# DE
|
|
65
|
+
"instanz",
|
|
66
|
+
"instanzen",
|
|
67
|
+
# ZH
|
|
68
|
+
"\u5b9e\u4f8b",
|
|
69
|
+
"\u670d\u52a1\u5668",
|
|
70
|
+
"\u865a\u62df\u673a",
|
|
71
|
+
],
|
|
72
|
+
"lambda": [
|
|
73
|
+
"lambda",
|
|
74
|
+
"lamda",
|
|
75
|
+
# ES
|
|
76
|
+
"funci\u00f3n lambda",
|
|
77
|
+
"funcion lambda",
|
|
78
|
+
"funcion",
|
|
79
|
+
"funci\u00f3n",
|
|
80
|
+
"funciones",
|
|
81
|
+
# EN
|
|
82
|
+
"lambda function",
|
|
83
|
+
"function",
|
|
84
|
+
"functions",
|
|
85
|
+
# PT
|
|
86
|
+
"fun\u00e7\u00e3o lambda",
|
|
87
|
+
"fun\u00e7\u00e3o",
|
|
88
|
+
"fun\u00e7\u00f5es",
|
|
89
|
+
# FR
|
|
90
|
+
"fonction lambda",
|
|
91
|
+
"fonction",
|
|
92
|
+
"fonctions",
|
|
93
|
+
# DE
|
|
94
|
+
"lambda-funktion",
|
|
95
|
+
"funktion",
|
|
96
|
+
"funktionen",
|
|
97
|
+
# ZH
|
|
98
|
+
"lambda\u51fd\u6570",
|
|
99
|
+
"\u51fd\u6570",
|
|
100
|
+
],
|
|
101
|
+
"dynamodb": [
|
|
102
|
+
"dynamodb",
|
|
103
|
+
"dynamo",
|
|
104
|
+
# ES
|
|
105
|
+
"tabla dynamodb",
|
|
106
|
+
"tabla",
|
|
107
|
+
"tablas",
|
|
108
|
+
"base de datos nosql",
|
|
109
|
+
# EN
|
|
110
|
+
"dynamodb table",
|
|
111
|
+
"table",
|
|
112
|
+
"tables",
|
|
113
|
+
"nosql",
|
|
114
|
+
# PT
|
|
115
|
+
"tabela dynamodb",
|
|
116
|
+
"tabela",
|
|
117
|
+
"tabelas",
|
|
118
|
+
# FR
|
|
119
|
+
"tableau dynamodb",
|
|
120
|
+
"tableau",
|
|
121
|
+
# DE
|
|
122
|
+
"dynamodb-tabelle",
|
|
123
|
+
"dynamodb tabelle",
|
|
124
|
+
"tabelle",
|
|
125
|
+
"tabellen",
|
|
126
|
+
# ZH
|
|
127
|
+
"dynamodb\u8868",
|
|
128
|
+
"\u8868",
|
|
129
|
+
],
|
|
130
|
+
"iam": [
|
|
131
|
+
"iam",
|
|
132
|
+
# ES
|
|
133
|
+
"usuario",
|
|
134
|
+
"usuarios",
|
|
135
|
+
"rol",
|
|
136
|
+
"roles",
|
|
137
|
+
"permisos",
|
|
138
|
+
"permiso",
|
|
139
|
+
# EN
|
|
140
|
+
"user",
|
|
141
|
+
"users",
|
|
142
|
+
"role",
|
|
143
|
+
"permission",
|
|
144
|
+
"permissions",
|
|
145
|
+
"policy",
|
|
146
|
+
"policies",
|
|
147
|
+
# PT
|
|
148
|
+
"usu\u00e1rio",
|
|
149
|
+
"usu\u00e1rios",
|
|
150
|
+
"permiss\u00e3o",
|
|
151
|
+
"permiss\u00f5es",
|
|
152
|
+
# FR
|
|
153
|
+
"utilisateur iam",
|
|
154
|
+
"utilisateur",
|
|
155
|
+
"r\u00f4le iam",
|
|
156
|
+
"r\u00f4le",
|
|
157
|
+
# DE
|
|
158
|
+
"berechtigung",
|
|
159
|
+
"berechtigungen",
|
|
160
|
+
"rolle",
|
|
161
|
+
"rollen",
|
|
162
|
+
# ZH
|
|
163
|
+
"iam\u7528\u6237",
|
|
164
|
+
"iam\u89d2\u8272",
|
|
165
|
+
"\u6743\u9650",
|
|
166
|
+
],
|
|
167
|
+
"rds": [
|
|
168
|
+
"rds",
|
|
169
|
+
# ES
|
|
170
|
+
"base de datos",
|
|
171
|
+
"bases de datos",
|
|
172
|
+
"postgres",
|
|
173
|
+
"mysql",
|
|
174
|
+
"aurora",
|
|
175
|
+
# EN
|
|
176
|
+
"database",
|
|
177
|
+
"databases",
|
|
178
|
+
# PT
|
|
179
|
+
"banco de dados",
|
|
180
|
+
"bancos de dados",
|
|
181
|
+
# FR
|
|
182
|
+
"base de donn\u00e9es",
|
|
183
|
+
"bases de donn\u00e9es",
|
|
184
|
+
# DE
|
|
185
|
+
"datenbank",
|
|
186
|
+
"datenbanken",
|
|
187
|
+
# ZH
|
|
188
|
+
"\u6570\u636e\u5e93",
|
|
189
|
+
"\u5173\u7cfb\u578b\u6570\u636e\u5e93",
|
|
190
|
+
],
|
|
191
|
+
"vpc": [
|
|
192
|
+
"vpc",
|
|
193
|
+
# ES
|
|
194
|
+
"red",
|
|
195
|
+
"redes",
|
|
196
|
+
"subred",
|
|
197
|
+
"subredes",
|
|
198
|
+
# EN
|
|
199
|
+
"network",
|
|
200
|
+
"subnet",
|
|
201
|
+
"subnets",
|
|
202
|
+
# PT
|
|
203
|
+
"rede",
|
|
204
|
+
"redes",
|
|
205
|
+
"sub-rede",
|
|
206
|
+
# FR
|
|
207
|
+
"sous-r\u00e9seau",
|
|
208
|
+
"r\u00e9seau",
|
|
209
|
+
# DE
|
|
210
|
+
"netzwerk",
|
|
211
|
+
"netzwerke",
|
|
212
|
+
"subnetz",
|
|
213
|
+
# ZH
|
|
214
|
+
"\u5b50\u7f51",
|
|
215
|
+
"\u7f51\u7edc",
|
|
216
|
+
],
|
|
217
|
+
"cloudfront": [
|
|
218
|
+
"cloudfront",
|
|
219
|
+
# ES
|
|
220
|
+
"cdn",
|
|
221
|
+
"distribuci\u00f3n",
|
|
222
|
+
"distribucion",
|
|
223
|
+
# EN
|
|
224
|
+
"cdn",
|
|
225
|
+
"distribution",
|
|
226
|
+
# PT
|
|
227
|
+
"distribui\u00e7\u00e3o cdn",
|
|
228
|
+
"distribui\u00e7\u00e3o",
|
|
229
|
+
"distribui\u00e7\u00f5es",
|
|
230
|
+
# FR
|
|
231
|
+
"distribution cloudfront",
|
|
232
|
+
# DE
|
|
233
|
+
"cloudfront-distribution",
|
|
234
|
+
"verteilung",
|
|
235
|
+
# ZH
|
|
236
|
+
"\u5185\u5bb9\u5206\u53d1",
|
|
237
|
+
"\u5206\u53d1",
|
|
238
|
+
],
|
|
239
|
+
"sns": [
|
|
240
|
+
"sns",
|
|
241
|
+
# ES
|
|
242
|
+
"notificacion",
|
|
243
|
+
"notificaci\u00f3n",
|
|
244
|
+
"notificaciones",
|
|
245
|
+
# EN
|
|
246
|
+
"notification",
|
|
247
|
+
"notifications",
|
|
248
|
+
# PT
|
|
249
|
+
"notifica\u00e7\u00e3o",
|
|
250
|
+
"notifica\u00e7\u00f5es",
|
|
251
|
+
"t\u00f3pico",
|
|
252
|
+
# FR
|
|
253
|
+
"notification sns",
|
|
254
|
+
"sujet sns",
|
|
255
|
+
# DE
|
|
256
|
+
"sns-thema",
|
|
257
|
+
"benachrichtigung",
|
|
258
|
+
"benachrichtigungen",
|
|
259
|
+
# ZH
|
|
260
|
+
"sns\u4e3b\u9898",
|
|
261
|
+
"\u901a\u77e5",
|
|
262
|
+
],
|
|
263
|
+
"sqs": [
|
|
264
|
+
"sqs",
|
|
265
|
+
# ES
|
|
266
|
+
"cola",
|
|
267
|
+
"colas",
|
|
268
|
+
# EN
|
|
269
|
+
"queue",
|
|
270
|
+
"queues",
|
|
271
|
+
# PT
|
|
272
|
+
"fila",
|
|
273
|
+
"filas",
|
|
274
|
+
# FR
|
|
275
|
+
"file d'attente",
|
|
276
|
+
"files d'attente",
|
|
277
|
+
# DE
|
|
278
|
+
"warteschlange",
|
|
279
|
+
"warteschlangen",
|
|
280
|
+
# ZH
|
|
281
|
+
"\u961f\u5217",
|
|
282
|
+
"\u6d88\u606f\u961f\u5217",
|
|
283
|
+
],
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Keywords por acción, multi-idioma: ES, EN, PT, FR, DE, ZH
|
|
287
|
+
ACTION_KEYWORDS: dict[str, list[str]] = {
|
|
288
|
+
"list": [
|
|
289
|
+
# ES
|
|
290
|
+
"lista",
|
|
291
|
+
"listar",
|
|
292
|
+
"listes",
|
|
293
|
+
"muestra",
|
|
294
|
+
"mu\u00e9strame",
|
|
295
|
+
"mostrar",
|
|
296
|
+
"ver",
|
|
297
|
+
"dame",
|
|
298
|
+
"ens\u00e9\u00f1ame",
|
|
299
|
+
# EN
|
|
300
|
+
"list",
|
|
301
|
+
"show",
|
|
302
|
+
"display",
|
|
303
|
+
"get all",
|
|
304
|
+
"view",
|
|
305
|
+
# PT
|
|
306
|
+
"listar",
|
|
307
|
+
"exibir",
|
|
308
|
+
"mostre",
|
|
309
|
+
# FR
|
|
310
|
+
"lister",
|
|
311
|
+
"afficher",
|
|
312
|
+
"montrer",
|
|
313
|
+
"montrez",
|
|
314
|
+
# DE
|
|
315
|
+
"auflisten",
|
|
316
|
+
"anzeigen",
|
|
317
|
+
"zeige",
|
|
318
|
+
"zeigen",
|
|
319
|
+
# ZH
|
|
320
|
+
"\u5217\u51fa",
|
|
321
|
+
"\u5217\u8868",
|
|
322
|
+
"\u67e5\u770b",
|
|
323
|
+
"\u663e\u793a",
|
|
324
|
+
],
|
|
325
|
+
"create": [
|
|
326
|
+
# ES
|
|
327
|
+
"crea",
|
|
328
|
+
"crear",
|
|
329
|
+
"genera",
|
|
330
|
+
"generar",
|
|
331
|
+
"nuevo",
|
|
332
|
+
"nueva",
|
|
333
|
+
"provisiona",
|
|
334
|
+
# EN
|
|
335
|
+
"create",
|
|
336
|
+
"make",
|
|
337
|
+
"new",
|
|
338
|
+
"provision",
|
|
339
|
+
"generate",
|
|
340
|
+
"launch",
|
|
341
|
+
# PT
|
|
342
|
+
"criar",
|
|
343
|
+
"crie",
|
|
344
|
+
"gerar",
|
|
345
|
+
"novo",
|
|
346
|
+
"nova",
|
|
347
|
+
# FR
|
|
348
|
+
"cr\u00e9er",
|
|
349
|
+
"cr\u00e9ez",
|
|
350
|
+
"g\u00e9n\u00e9rer",
|
|
351
|
+
"nouveau",
|
|
352
|
+
"nouvelle",
|
|
353
|
+
# DE
|
|
354
|
+
"erstellen",
|
|
355
|
+
"erstelle",
|
|
356
|
+
"erzeugen",
|
|
357
|
+
"neu",
|
|
358
|
+
"starte",
|
|
359
|
+
"starten",
|
|
360
|
+
# ZH
|
|
361
|
+
"\u521b\u5efa",
|
|
362
|
+
"\u65b0\u5efa",
|
|
363
|
+
"\u751f\u6210",
|
|
364
|
+
"\u542f\u52a8",
|
|
365
|
+
],
|
|
366
|
+
"delete": [
|
|
367
|
+
# ES
|
|
368
|
+
"borra",
|
|
369
|
+
"borrar",
|
|
370
|
+
"borrame",
|
|
371
|
+
"elimina",
|
|
372
|
+
"eliminar",
|
|
373
|
+
"quita",
|
|
374
|
+
"quitar",
|
|
375
|
+
"vacia",
|
|
376
|
+
"vaciar",
|
|
377
|
+
# EN
|
|
378
|
+
"delete",
|
|
379
|
+
"remove",
|
|
380
|
+
"destroy",
|
|
381
|
+
"terminate",
|
|
382
|
+
"drop",
|
|
383
|
+
# PT
|
|
384
|
+
"excluir",
|
|
385
|
+
"deletar",
|
|
386
|
+
"remover",
|
|
387
|
+
"apagar",
|
|
388
|
+
"terminar",
|
|
389
|
+
# FR
|
|
390
|
+
"supprimer",
|
|
391
|
+
"supprimez",
|
|
392
|
+
"effacer",
|
|
393
|
+
"vider",
|
|
394
|
+
# DE
|
|
395
|
+
"l\u00f6schen",
|
|
396
|
+
"l\u00f6sche",
|
|
397
|
+
"entfernen",
|
|
398
|
+
"entferne",
|
|
399
|
+
# ZH
|
|
400
|
+
"\u5220\u9664",
|
|
401
|
+
"\u79fb\u9664",
|
|
402
|
+
"\u4e22\u5f03",
|
|
403
|
+
"\u7ec8\u6b62",
|
|
404
|
+
],
|
|
405
|
+
"update": [
|
|
406
|
+
# ES
|
|
407
|
+
"actualiza",
|
|
408
|
+
"actualizar",
|
|
409
|
+
"modifica",
|
|
410
|
+
"modificar",
|
|
411
|
+
"cambia",
|
|
412
|
+
"cambiar",
|
|
413
|
+
# EN
|
|
414
|
+
"update",
|
|
415
|
+
"modify",
|
|
416
|
+
"change",
|
|
417
|
+
"edit",
|
|
418
|
+
"alter",
|
|
419
|
+
# PT
|
|
420
|
+
"atualizar",
|
|
421
|
+
"atualize",
|
|
422
|
+
"modificar",
|
|
423
|
+
"alterar",
|
|
424
|
+
# FR
|
|
425
|
+
"mettre \u00e0 jour",
|
|
426
|
+
"modifier",
|
|
427
|
+
"modifiez",
|
|
428
|
+
"changer",
|
|
429
|
+
# DE
|
|
430
|
+
"\u00e4ndern",
|
|
431
|
+
"\u00e4ndere",
|
|
432
|
+
"aktualisieren",
|
|
433
|
+
"bearbeiten",
|
|
434
|
+
# ZH
|
|
435
|
+
"\u66f4\u65b0",
|
|
436
|
+
"\u4fee\u6539",
|
|
437
|
+
"\u53d8\u66f4",
|
|
438
|
+
],
|
|
439
|
+
"describe": [
|
|
440
|
+
# ES
|
|
441
|
+
"describe",
|
|
442
|
+
"describir",
|
|
443
|
+
"info",
|
|
444
|
+
"informacion",
|
|
445
|
+
"informaci\u00f3n",
|
|
446
|
+
"detalles",
|
|
447
|
+
"detalle",
|
|
448
|
+
# EN
|
|
449
|
+
"describe",
|
|
450
|
+
"detail",
|
|
451
|
+
"details",
|
|
452
|
+
"info",
|
|
453
|
+
"information",
|
|
454
|
+
# PT
|
|
455
|
+
"descrever",
|
|
456
|
+
"descreva",
|
|
457
|
+
"detalhes",
|
|
458
|
+
"informa\u00e7\u00e3o",
|
|
459
|
+
# FR
|
|
460
|
+
"d\u00e9crire",
|
|
461
|
+
"d\u00e9crivez",
|
|
462
|
+
"d\u00e9tails",
|
|
463
|
+
"informations",
|
|
464
|
+
# DE
|
|
465
|
+
"beschreiben",
|
|
466
|
+
"details",
|
|
467
|
+
"informationen",
|
|
468
|
+
# ZH
|
|
469
|
+
"\u63cf\u8ff0",
|
|
470
|
+
"\u8be6\u60c5",
|
|
471
|
+
"\u4fe1\u606f",
|
|
472
|
+
],
|
|
473
|
+
"invoke": [
|
|
474
|
+
# ES
|
|
475
|
+
"invoca",
|
|
476
|
+
"invocar",
|
|
477
|
+
"ejecuta",
|
|
478
|
+
"ejecutar",
|
|
479
|
+
"llama",
|
|
480
|
+
"llamar",
|
|
481
|
+
# EN
|
|
482
|
+
"invoke",
|
|
483
|
+
"execute",
|
|
484
|
+
"run",
|
|
485
|
+
"call",
|
|
486
|
+
"trigger",
|
|
487
|
+
# PT
|
|
488
|
+
"invocar",
|
|
489
|
+
"executar",
|
|
490
|
+
"chamar",
|
|
491
|
+
# FR
|
|
492
|
+
"invoquer",
|
|
493
|
+
"ex\u00e9cuter",
|
|
494
|
+
"appeler",
|
|
495
|
+
# DE
|
|
496
|
+
"aufrufen",
|
|
497
|
+
"ausf\u00fchren",
|
|
498
|
+
"f\u00fchre",
|
|
499
|
+
# ZH
|
|
500
|
+
"\u8c03\u7528",
|
|
501
|
+
"\u6267\u884c",
|
|
502
|
+
"\u89e6\u53d1",
|
|
503
|
+
],
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
# Keywords que implican "describe" con mayor prioridad si aparecen junto a un verbo de "list"
|
|
507
|
+
DESCRIBE_PRIORITY_KEYWORDS: list[str] = [
|
|
508
|
+
"detalles",
|
|
509
|
+
"detalle",
|
|
510
|
+
"details",
|
|
511
|
+
"detail",
|
|
512
|
+
"detalhes",
|
|
513
|
+
"d\u00e9tails",
|
|
514
|
+
"informaci\u00f3n",
|
|
515
|
+
"informacion",
|
|
516
|
+
"information",
|
|
517
|
+
"informa\u00e7\u00e3o",
|
|
518
|
+
"informations",
|
|
519
|
+
"informationen",
|
|
520
|
+
"\u8be6\u60c5",
|
|
521
|
+
"\u4fe1\u606f",
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
# Longitud máxima de input que procesamos
|
|
525
|
+
MAX_INPUT_LENGTH = 500
|
|
526
|
+
|
|
527
|
+
# Regex para detectar CJK characters
|
|
528
|
+
_CJK_PATTERN = re.compile(r"[\u4e00-\u9fff\u3400-\u4dbf]")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# --- Pydantic Models ---
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class Intent(BaseModel):
|
|
535
|
+
"""Structured representation of user's natural language request."""
|
|
536
|
+
|
|
537
|
+
action: ActionType
|
|
538
|
+
service: str = Field(..., description="AWS service short name (s3, ec2, lambda, etc.)")
|
|
539
|
+
resource_type: str | None = None
|
|
540
|
+
filters: dict[str, Any] = Field(default_factory=dict)
|
|
541
|
+
region: str | None = None
|
|
542
|
+
confidence: float = Field(ge=0.0, le=1.0, default=0.0)
|
|
543
|
+
raw_input: str
|
|
544
|
+
detected_language: str = "en"
|
|
545
|
+
suggestion: str | None = None
|
|
546
|
+
clarification_needed: bool = False
|
|
547
|
+
clarification_question: str | None = None
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# --- Classes ---
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class IntentParser:
|
|
554
|
+
"""Parses natural language into structured Intent objects.
|
|
555
|
+
|
|
556
|
+
Uses a hybrid approach combining rule-based keyword detection with
|
|
557
|
+
language identification via langdetect. For high-confidence cases,
|
|
558
|
+
rule-based matching is sufficient. Ambiguous inputs are flagged for
|
|
559
|
+
potential Bedrock fallback.
|
|
560
|
+
|
|
561
|
+
Supports 6 languages: ES, EN, PT, FR, DE, ZH.
|
|
562
|
+
Supports 10 AWS services and 6 action types.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
def parse(self, text: str, region: str | None = None) -> Intent:
|
|
566
|
+
"""Parse natural language text into an Intent.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
text: The natural language input.
|
|
570
|
+
region: Optional default AWS region.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Intent object with detected action, service, and metadata.
|
|
574
|
+
"""
|
|
575
|
+
# Edge case: empty or whitespace-only input
|
|
576
|
+
if not text or not text.strip():
|
|
577
|
+
return Intent(
|
|
578
|
+
action="unknown",
|
|
579
|
+
service="unknown",
|
|
580
|
+
confidence=0.0,
|
|
581
|
+
raw_input=text or "",
|
|
582
|
+
detected_language="en",
|
|
583
|
+
region=region,
|
|
584
|
+
clarification_needed=True,
|
|
585
|
+
clarification_question=(
|
|
586
|
+
"No input provided. Please describe what AWS operation you'd like to perform."
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Truncate very long input
|
|
591
|
+
original_text = text
|
|
592
|
+
if len(text) > MAX_INPUT_LENGTH:
|
|
593
|
+
text = text[:MAX_INPUT_LENGTH]
|
|
594
|
+
|
|
595
|
+
text_lower = text.lower().strip()
|
|
596
|
+
|
|
597
|
+
# Detect language with langdetect (seeded for determinism)
|
|
598
|
+
detected_lang = _detect_language(text)
|
|
599
|
+
|
|
600
|
+
# Detect service and action via keyword matching
|
|
601
|
+
service = _detect_service(text_lower)
|
|
602
|
+
action = _detect_action(text_lower)
|
|
603
|
+
|
|
604
|
+
# Calculate confidence based on detection results
|
|
605
|
+
confidence = _calculate_confidence(service, action)
|
|
606
|
+
|
|
607
|
+
# Build suggestion if low confidence
|
|
608
|
+
suggestion: str | None = None
|
|
609
|
+
if confidence < 0.5:
|
|
610
|
+
suggestion = _build_suggestion()
|
|
611
|
+
|
|
612
|
+
# AC-1.3: clarification needed when confidence < 0.7
|
|
613
|
+
clarification_needed = confidence < 0.7
|
|
614
|
+
clarification_question: str | None = None
|
|
615
|
+
if clarification_needed:
|
|
616
|
+
clarification_question = _build_clarification_question(service, action)
|
|
617
|
+
|
|
618
|
+
return Intent(
|
|
619
|
+
action=action,
|
|
620
|
+
service=service or "unknown",
|
|
621
|
+
confidence=confidence,
|
|
622
|
+
raw_input=original_text,
|
|
623
|
+
detected_language=detected_lang,
|
|
624
|
+
region=region,
|
|
625
|
+
suggestion=suggestion,
|
|
626
|
+
clarification_needed=clarification_needed,
|
|
627
|
+
clarification_question=clarification_question,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
# --- Private helpers ---
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _detect_language(text: str) -> str:
|
|
635
|
+
"""Detect language of the input text using langdetect with keyword-based fallback.
|
|
636
|
+
|
|
637
|
+
Pre-processes text by removing AWS identifiers and technical tokens
|
|
638
|
+
that confuse langdetect on short inputs. For texts that contain CJK
|
|
639
|
+
characters, returns 'zh-cn' directly. Uses keyword-based heuristic
|
|
640
|
+
as fallback for very short cleaned texts.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
text: Input text to analyze.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
ISO 639-1 language code (e.g., 'es', 'en', 'pt', 'fr', 'de', 'zh-cn').
|
|
647
|
+
"""
|
|
648
|
+
# If text contains CJK characters, it's Chinese
|
|
649
|
+
if _CJK_PATTERN.search(text):
|
|
650
|
+
return "zh-cn"
|
|
651
|
+
|
|
652
|
+
# Remove technical tokens that confuse langdetect:
|
|
653
|
+
cleaned = text
|
|
654
|
+
# Remove ARNs
|
|
655
|
+
cleaned = re.sub(r"arn:aws:\S+", " ", cleaned)
|
|
656
|
+
# Remove s3:// URIs
|
|
657
|
+
cleaned = re.sub(r"s3://\S+", " ", cleaned)
|
|
658
|
+
# Remove AWS resource IDs (i-xxxx, vpc-xxxx, etc.)
|
|
659
|
+
cleaned = re.sub(r"\b[a-z]+-[0-9a-f]{6,}\b", " ", cleaned)
|
|
660
|
+
# Remove instance types (t3.micro, db.t3.micro, db.r5.large)
|
|
661
|
+
cleaned = re.sub(r"\b(?:db\.)?[a-z]\d+\.\w+\b", " ", cleaned)
|
|
662
|
+
# Remove alphanumeric IDs that look like AWS distribution IDs (E1XYZ2ABC3)
|
|
663
|
+
cleaned = re.sub(r"\bE[0-9A-Z]{8,}\b", " ", cleaned)
|
|
664
|
+
# Remove CLI flags
|
|
665
|
+
cleaned = re.sub(r"--\w+", " ", cleaned)
|
|
666
|
+
# Remove AWS service abbreviations that don't help language detection
|
|
667
|
+
cleaned = re.sub(
|
|
668
|
+
r"\b(?:S3|EC2|RDS|VPC|IAM|SQS|SNS|Lambda|DynamoDB|CloudFront|CIDR)\b",
|
|
669
|
+
" ",
|
|
670
|
+
cleaned,
|
|
671
|
+
flags=re.IGNORECASE,
|
|
672
|
+
)
|
|
673
|
+
# Remove IP/CIDR ranges
|
|
674
|
+
cleaned = re.sub(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?:/\d{1,2})?", " ", cleaned)
|
|
675
|
+
# Remove JSON payloads
|
|
676
|
+
cleaned = re.sub(r"\{[^}]*\}", " ", cleaned)
|
|
677
|
+
# Remove remaining identifiers (like resource names with hyphens: temp-data, legacy-app-db)
|
|
678
|
+
cleaned = re.sub(r"\b[a-z]+-[a-z]+-[a-z0-9-]+\b", " ", cleaned)
|
|
679
|
+
cleaned = re.sub(r"\b[a-z]+-[a-z]+\b", " ", cleaned)
|
|
680
|
+
# Clean up multiple spaces
|
|
681
|
+
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
|
682
|
+
|
|
683
|
+
# If cleaned text is too short, use keyword-based detection
|
|
684
|
+
if len(cleaned) <= 15:
|
|
685
|
+
kw_lang = _detect_language_by_keywords(text.lower())
|
|
686
|
+
if kw_lang:
|
|
687
|
+
return kw_lang
|
|
688
|
+
|
|
689
|
+
try:
|
|
690
|
+
detected = str(langdetect.detect(cleaned if len(cleaned) > 5 else text))
|
|
691
|
+
# Always run keyword-based detection for cross-validation
|
|
692
|
+
kw_lang = _detect_language_by_keywords(text.lower())
|
|
693
|
+
|
|
694
|
+
# If keyword detection has a clear answer that differs from langdetect,
|
|
695
|
+
# trust the keyword detection — langdetect is unreliable for short technical texts
|
|
696
|
+
if kw_lang and kw_lang != detected:
|
|
697
|
+
# Only override if langdetect returned something wrong
|
|
698
|
+
# Common misdetections by langdetect on short technical texts:
|
|
699
|
+
# - Catalan (ca) for Spanish (es) or Portuguese (pt)
|
|
700
|
+
# - Italian (it) for French (fr)
|
|
701
|
+
# - Romanian (ro) for Portuguese (pt)
|
|
702
|
+
# - Spanish (es) for Portuguese (pt)
|
|
703
|
+
# - Danish/Swedish/Dutch/French for English (en)
|
|
704
|
+
# - French (fr) for Spanish (es) — short texts
|
|
705
|
+
if detected == "ca" and kw_lang == "pt":
|
|
706
|
+
return "pt"
|
|
707
|
+
if detected in ("it", "ca", "es") and kw_lang == "fr":
|
|
708
|
+
return "fr"
|
|
709
|
+
if detected in ("ro", "es", "ca") and kw_lang == "pt":
|
|
710
|
+
return "pt"
|
|
711
|
+
if detected in ("da", "sv", "nl", "no", "af", "fr") and kw_lang == "en":
|
|
712
|
+
return "en"
|
|
713
|
+
if detected in ("fr", "it", "ca") and kw_lang == "es":
|
|
714
|
+
return "es"
|
|
715
|
+
if detected == "it" and kw_lang != "it":
|
|
716
|
+
return kw_lang
|
|
717
|
+
# If langdetect returns unexpected language, trust keywords
|
|
718
|
+
if detected not in ("es", "en", "pt", "fr", "de"):
|
|
719
|
+
return kw_lang
|
|
720
|
+
# If langdetect returns es but keywords strongly say en, trust keywords
|
|
721
|
+
# This handles cases where short English texts after AWS-term removal
|
|
722
|
+
# get misclassified as Spanish
|
|
723
|
+
if detected == "es" and kw_lang == "en":
|
|
724
|
+
en_scores = _get_keyword_scores(text.lower())
|
|
725
|
+
if en_scores.get("en", 0) > en_scores.get("es", 0):
|
|
726
|
+
return "en"
|
|
727
|
+
|
|
728
|
+
# Simple corrections without keyword evidence
|
|
729
|
+
if detected == "ca":
|
|
730
|
+
return "es"
|
|
731
|
+
return detected
|
|
732
|
+
except langdetect.lang_detect_exception.LangDetectException:
|
|
733
|
+
return "en"
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
# Language-specific keywords for heuristic detection
|
|
737
|
+
_LANG_HINT_KEYWORDS: dict[str, list[str]] = {
|
|
738
|
+
"es": [
|
|
739
|
+
"lista",
|
|
740
|
+
"crea",
|
|
741
|
+
"crear",
|
|
742
|
+
"elimina",
|
|
743
|
+
"eliminar",
|
|
744
|
+
"borra",
|
|
745
|
+
"borrar",
|
|
746
|
+
"muestra",
|
|
747
|
+
"mu\u00e9strame",
|
|
748
|
+
"dame",
|
|
749
|
+
"ens\u00e9\u00f1ame",
|
|
750
|
+
"actualiza",
|
|
751
|
+
"modifica",
|
|
752
|
+
"quiero",
|
|
753
|
+
"necesito",
|
|
754
|
+
"por favor",
|
|
755
|
+
"todos los",
|
|
756
|
+
"todas las",
|
|
757
|
+
"del",
|
|
758
|
+
"los",
|
|
759
|
+
"las",
|
|
760
|
+
"hacer",
|
|
761
|
+
"cu\u00e1l",
|
|
762
|
+
"ejecuta",
|
|
763
|
+
"ejecutar",
|
|
764
|
+
"haz",
|
|
765
|
+
"algo",
|
|
766
|
+
"con",
|
|
767
|
+
"funci\u00f3n",
|
|
768
|
+
"base de datos",
|
|
769
|
+
],
|
|
770
|
+
"en": [
|
|
771
|
+
"list",
|
|
772
|
+
"create",
|
|
773
|
+
"delete",
|
|
774
|
+
"remove",
|
|
775
|
+
"show",
|
|
776
|
+
"display",
|
|
777
|
+
"get",
|
|
778
|
+
"the",
|
|
779
|
+
"all",
|
|
780
|
+
"please",
|
|
781
|
+
"every",
|
|
782
|
+
"everything",
|
|
783
|
+
"drop",
|
|
784
|
+
"terminate",
|
|
785
|
+
"launch",
|
|
786
|
+
"run",
|
|
787
|
+
"with",
|
|
788
|
+
"from",
|
|
789
|
+
"permanently",
|
|
790
|
+
"immediately",
|
|
791
|
+
"named",
|
|
792
|
+
"queue",
|
|
793
|
+
"instance",
|
|
794
|
+
"bucket",
|
|
795
|
+
],
|
|
796
|
+
"pt": [
|
|
797
|
+
"listar",
|
|
798
|
+
"criar",
|
|
799
|
+
"excluir",
|
|
800
|
+
"deletar",
|
|
801
|
+
"remover",
|
|
802
|
+
"terminar",
|
|
803
|
+
"todos os",
|
|
804
|
+
"todas as",
|
|
805
|
+
"mostre",
|
|
806
|
+
"da conta",
|
|
807
|
+
"dos",
|
|
808
|
+
"do",
|
|
809
|
+
"inst\u00e2ncia",
|
|
810
|
+
"inst\u00e2ncias",
|
|
811
|
+
"tabela",
|
|
812
|
+
"associadas",
|
|
813
|
+
"da",
|
|
814
|
+
"excluir a",
|
|
815
|
+
"deletar a",
|
|
816
|
+
],
|
|
817
|
+
"fr": [
|
|
818
|
+
"lister",
|
|
819
|
+
"cr\u00e9er",
|
|
820
|
+
"cr\u00e9ez",
|
|
821
|
+
"supprimer",
|
|
822
|
+
"afficher",
|
|
823
|
+
"montrer",
|
|
824
|
+
"terminer",
|
|
825
|
+
"vider",
|
|
826
|
+
"tous les",
|
|
827
|
+
"toutes les",
|
|
828
|
+
"nomm\u00e9",
|
|
829
|
+
"nouveau",
|
|
830
|
+
"nouvelle",
|
|
831
|
+
"r\u00f4le",
|
|
832
|
+
"ancien",
|
|
833
|
+
"ancienne",
|
|
834
|
+
"fichier",
|
|
835
|
+
"serveur",
|
|
836
|
+
"r\u00e9seau",
|
|
837
|
+
"obtenir",
|
|
838
|
+
"ajouter",
|
|
839
|
+
"configurer",
|
|
840
|
+
"r\u00e9cursivement",
|
|
841
|
+
"l'instance",
|
|
842
|
+
"donn\u00e9es",
|
|
843
|
+
],
|
|
844
|
+
"de": [
|
|
845
|
+
"erstellen",
|
|
846
|
+
"l\u00f6schen",
|
|
847
|
+
"l\u00f6sche",
|
|
848
|
+
"anzeigen",
|
|
849
|
+
"zeige",
|
|
850
|
+
"entferne",
|
|
851
|
+
"entfernen",
|
|
852
|
+
"starte",
|
|
853
|
+
"f\u00fchre",
|
|
854
|
+
"alle",
|
|
855
|
+
"der",
|
|
856
|
+
"die",
|
|
857
|
+
"das",
|
|
858
|
+
"den",
|
|
859
|
+
"dem",
|
|
860
|
+
"eine",
|
|
861
|
+
"einen",
|
|
862
|
+
"mit",
|
|
863
|
+
"auf",
|
|
864
|
+
"vom",
|
|
865
|
+
"und",
|
|
866
|
+
],
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _detect_language_by_keywords(text: str) -> str | None:
|
|
871
|
+
"""Detect language by counting keyword matches from each language.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
text: Lowercased text to analyze.
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Language code or None if no clear winner.
|
|
878
|
+
"""
|
|
879
|
+
scores = _get_keyword_scores(text)
|
|
880
|
+
|
|
881
|
+
if not scores:
|
|
882
|
+
return None
|
|
883
|
+
|
|
884
|
+
# Return language with highest score, but only if it clearly wins
|
|
885
|
+
best_lang = max(scores, key=scores.get) # type: ignore[arg-type]
|
|
886
|
+
return best_lang
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _get_keyword_scores(text: str) -> dict[str, int]:
|
|
890
|
+
"""Get keyword match scores for each language.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
text: Lowercased text to analyze.
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
Dictionary mapping language codes to their keyword match scores.
|
|
897
|
+
"""
|
|
898
|
+
scores: dict[str, int] = {}
|
|
899
|
+
for lang, keywords in _LANG_HINT_KEYWORDS.items():
|
|
900
|
+
score = 0
|
|
901
|
+
for kw in keywords:
|
|
902
|
+
if " " in kw:
|
|
903
|
+
if kw in text:
|
|
904
|
+
score += 2
|
|
905
|
+
else:
|
|
906
|
+
# Word-presence check (simple substring for common short words)
|
|
907
|
+
pattern = rf"(?:^|[\s,;:.!?\(\)\"'\-/]){re.escape(kw)}(?:[\s,;:.!?\(\)\"'\-/]|$)"
|
|
908
|
+
if re.search(pattern, text):
|
|
909
|
+
score += 1
|
|
910
|
+
if score > 0:
|
|
911
|
+
scores[lang] = score
|
|
912
|
+
|
|
913
|
+
return scores
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _contains_cjk(text: str) -> bool:
|
|
917
|
+
"""Check if text contains CJK characters.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
text: Text to check.
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
True if CJK characters are present.
|
|
924
|
+
"""
|
|
925
|
+
return bool(_CJK_PATTERN.search(text))
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _keyword_matches(keyword: str, text: str) -> bool:
|
|
929
|
+
"""Check if a keyword matches in the text, with appropriate boundary handling.
|
|
930
|
+
|
|
931
|
+
For multi-word keywords or keywords containing CJK characters, uses substring match.
|
|
932
|
+
For single-word ASCII keywords in text with CJK characters, also uses substring match.
|
|
933
|
+
For single-word ASCII keywords in pure ASCII text, uses word boundary regex.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
keyword: The keyword to search for.
|
|
937
|
+
text: The lowercased input text.
|
|
938
|
+
|
|
939
|
+
Returns:
|
|
940
|
+
True if the keyword matches.
|
|
941
|
+
"""
|
|
942
|
+
# Multi-word keywords or CJK keywords: use substring match
|
|
943
|
+
if " " in keyword or _contains_cjk(keyword):
|
|
944
|
+
return keyword in text
|
|
945
|
+
|
|
946
|
+
# If the text contains CJK characters, use substring match for ASCII keywords too
|
|
947
|
+
# (CJK text doesn't have word boundaries between chars and embedded ASCII words)
|
|
948
|
+
if _contains_cjk(text):
|
|
949
|
+
return keyword in text
|
|
950
|
+
|
|
951
|
+
# Single-word keywords in non-CJK text: use word boundary matching
|
|
952
|
+
pattern = rf"(?:^|[\s,;:.!?\(\)\"'\-/]){re.escape(keyword)}(?:[\s,;:.!?\(\)\"'\-/]|$)"
|
|
953
|
+
return bool(re.search(pattern, text))
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _detect_service(text: str) -> str | None:
|
|
957
|
+
"""Detect AWS service from text via keyword matching.
|
|
958
|
+
|
|
959
|
+
First checks for exact AWS service names (highest priority), then
|
|
960
|
+
falls back to keyword matching. Uses word boundary awareness for
|
|
961
|
+
single-word ASCII keywords and substring matching for CJK keywords.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
text: Lowercased input text.
|
|
965
|
+
|
|
966
|
+
Returns:
|
|
967
|
+
Service identifier string or None if not detected.
|
|
968
|
+
"""
|
|
969
|
+
# Priority 1: Exact service name matches (case-insensitive, already lowercase)
|
|
970
|
+
# These are unambiguous identifiers that should always win
|
|
971
|
+
# Order: longer/more specific first; IAM before lambda to avoid role name conflicts
|
|
972
|
+
exact_services = [
|
|
973
|
+
"cloudfront",
|
|
974
|
+
"dynamodb",
|
|
975
|
+
"iam",
|
|
976
|
+
"lambda",
|
|
977
|
+
"lamda",
|
|
978
|
+
"vpc",
|
|
979
|
+
"sqs",
|
|
980
|
+
"sns",
|
|
981
|
+
"rds",
|
|
982
|
+
"ec2",
|
|
983
|
+
"s3",
|
|
984
|
+
]
|
|
985
|
+
for svc_name in exact_services:
|
|
986
|
+
# Use keyword matching which handles CJK and word boundaries
|
|
987
|
+
if _keyword_matches(svc_name, text):
|
|
988
|
+
# Map to canonical service key
|
|
989
|
+
if svc_name in ("lambda", "lamda"):
|
|
990
|
+
return "lambda"
|
|
991
|
+
return svc_name
|
|
992
|
+
|
|
993
|
+
# Priority 2: Keyword-based detection for when service name isn't mentioned
|
|
994
|
+
for service, keywords in SERVICE_KEYWORDS.items():
|
|
995
|
+
for kw in keywords:
|
|
996
|
+
if _keyword_matches(kw, text):
|
|
997
|
+
return service
|
|
998
|
+
return None
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _detect_action(text: str) -> ActionType:
|
|
1002
|
+
"""Detect intended action from text via keyword matching.
|
|
1003
|
+
|
|
1004
|
+
If both a "list" keyword and a "describe" priority keyword are found,
|
|
1005
|
+
returns "describe" to handle cases like "muéstrame los detalles" or
|
|
1006
|
+
"mostre informação".
|
|
1007
|
+
|
|
1008
|
+
Args:
|
|
1009
|
+
text: Lowercased input text.
|
|
1010
|
+
|
|
1011
|
+
Returns:
|
|
1012
|
+
One of the valid ActionType values.
|
|
1013
|
+
"""
|
|
1014
|
+
detected_action: ActionType = "unknown"
|
|
1015
|
+
|
|
1016
|
+
for action, keywords in ACTION_KEYWORDS.items():
|
|
1017
|
+
for kw in keywords:
|
|
1018
|
+
if _keyword_matches(kw, text):
|
|
1019
|
+
if action in get_args(ActionType):
|
|
1020
|
+
detected_action = action # type: ignore[assignment]
|
|
1021
|
+
break
|
|
1022
|
+
if detected_action != "unknown":
|
|
1023
|
+
break
|
|
1024
|
+
|
|
1025
|
+
# If detected "list", check if describe-priority keywords are present
|
|
1026
|
+
# (handles "muéstrame los detalles", "mostre informação", "zeige mir die Details")
|
|
1027
|
+
if detected_action == "list":
|
|
1028
|
+
for desc_kw in DESCRIBE_PRIORITY_KEYWORDS:
|
|
1029
|
+
if desc_kw in text:
|
|
1030
|
+
return "describe"
|
|
1031
|
+
|
|
1032
|
+
return detected_action
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def _calculate_confidence(service: str | None, action: ActionType) -> float:
|
|
1036
|
+
"""Calculate confidence score based on detection results.
|
|
1037
|
+
|
|
1038
|
+
Args:
|
|
1039
|
+
service: Detected service or None.
|
|
1040
|
+
action: Detected action type.
|
|
1041
|
+
|
|
1042
|
+
Returns:
|
|
1043
|
+
Confidence float between 0.0 and 1.0.
|
|
1044
|
+
"""
|
|
1045
|
+
if service and action != "unknown":
|
|
1046
|
+
return 0.9
|
|
1047
|
+
if service or action != "unknown":
|
|
1048
|
+
return 0.5
|
|
1049
|
+
return 0.2
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
def _build_clarification_question(service: str | None, action: str) -> str:
|
|
1053
|
+
"""Generate a clarification question when confidence is low.
|
|
1054
|
+
|
|
1055
|
+
Args:
|
|
1056
|
+
service: Detected AWS service or None.
|
|
1057
|
+
action: Detected action type.
|
|
1058
|
+
|
|
1059
|
+
Returns:
|
|
1060
|
+
A specific clarification question for the user.
|
|
1061
|
+
"""
|
|
1062
|
+
if service is None and action == "unknown":
|
|
1063
|
+
return "Could you specify which AWS service and what operation you'd like to perform?"
|
|
1064
|
+
if service is None:
|
|
1065
|
+
return f"Which AWS service would you like to {action}?"
|
|
1066
|
+
if action == "unknown":
|
|
1067
|
+
return f"What would you like to do with {service}? (list, create, delete, describe)"
|
|
1068
|
+
return "Could you provide more details about what you'd like to do?"
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _build_suggestion() -> str:
|
|
1072
|
+
"""Generate a helpful suggestion when intent is unclear.
|
|
1073
|
+
|
|
1074
|
+
Returns:
|
|
1075
|
+
Suggestion string with usage examples.
|
|
1076
|
+
"""
|
|
1077
|
+
return (
|
|
1078
|
+
"Try being more specific. Example: 'list the S3 buckets' "
|
|
1079
|
+
"or 'create a new EC2 instance t3.micro'"
|
|
1080
|
+
)
|