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
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""Bedrock translator — converts Intent objects into AWS CLI commands via Claude 3.5 Sonnet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import random
|
|
8
|
+
import time
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import boto3
|
|
13
|
+
from botocore.exceptions import ClientError, ReadTimeoutError
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from cloudshellgpt.intent import Intent
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Error types (defined before BedrockTranslator so they can be referenced)
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BedrockErrorType(StrEnum):
|
|
27
|
+
"""Categories of Bedrock errors for user-facing messaging."""
|
|
28
|
+
|
|
29
|
+
CREDENTIALS = "credentials"
|
|
30
|
+
THROTTLING = "throttling"
|
|
31
|
+
TIMEOUT = "timeout"
|
|
32
|
+
MODEL_NOT_AVAILABLE = "model_not_available"
|
|
33
|
+
INVALID_RESPONSE = "invalid_response"
|
|
34
|
+
GENERIC = "generic"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BedrockError(Exception):
|
|
38
|
+
"""Raised when Bedrock translation fails.
|
|
39
|
+
|
|
40
|
+
Provides structured error information with user-facing messages
|
|
41
|
+
and actionable suggestions.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
error_type: Category of the error for classification.
|
|
45
|
+
user_message: Human-readable message explaining what went wrong.
|
|
46
|
+
suggestion: Actionable advice on how to resolve the issue.
|
|
47
|
+
technical_detail: Raw error message for debugging (optional).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
user_message: str,
|
|
53
|
+
*,
|
|
54
|
+
error_type: BedrockErrorType = BedrockErrorType.GENERIC,
|
|
55
|
+
suggestion: str = "",
|
|
56
|
+
technical_detail: str = "",
|
|
57
|
+
) -> None:
|
|
58
|
+
self.error_type = error_type
|
|
59
|
+
self.user_message = user_message
|
|
60
|
+
self.suggestion = suggestion
|
|
61
|
+
self.technical_detail = technical_detail
|
|
62
|
+
super().__init__(user_message)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_retryable(self) -> bool:
|
|
66
|
+
"""Whether this error is transient and safe to retry.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the error type is throttling or timeout.
|
|
70
|
+
"""
|
|
71
|
+
return self.error_type in _RETRYABLE_ERROR_TYPES
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Tipos de error que son transitorios y seguros para reintentar
|
|
75
|
+
_RETRYABLE_ERROR_TYPES: frozenset[BedrockErrorType] = frozenset(
|
|
76
|
+
{
|
|
77
|
+
BedrockErrorType.THROTTLING,
|
|
78
|
+
BedrockErrorType.TIMEOUT,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Translation model
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Translation(BaseModel):
|
|
89
|
+
"""A translated AWS CLI command with metadata."""
|
|
90
|
+
|
|
91
|
+
command: str
|
|
92
|
+
explanation: str
|
|
93
|
+
detailed_explanation: str
|
|
94
|
+
risk_level: str = "low"
|
|
95
|
+
estimated_cost: str = "$0.00"
|
|
96
|
+
requires_dry_run: bool = False
|
|
97
|
+
affected_resources: list[str] = Field(default_factory=list)
|
|
98
|
+
flags_used: dict[str, str] = Field(default_factory=dict)
|
|
99
|
+
tip: str | None = None
|
|
100
|
+
related_commands: list[dict[str, str]] = Field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Translator
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class BedrockTranslator:
|
|
109
|
+
"""Translates natural language intents to AWS CLI commands using Amazon Bedrock.
|
|
110
|
+
|
|
111
|
+
Uses Claude 3.5 Sonnet via the Bedrock Converse API. Implements a
|
|
112
|
+
few-shot system prompt with best practices for AWS CLI generation.
|
|
113
|
+
Retries transient errors (throttling, timeouts) with exponential backoff.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
MODEL_ID = "us.anthropic.claude-sonnet-4-6"
|
|
117
|
+
REGION = "us-east-1"
|
|
118
|
+
|
|
119
|
+
# --- Retry configuration constants ---
|
|
120
|
+
MAX_RETRIES = 3
|
|
121
|
+
BASE_DELAY = 1.0 # seconds
|
|
122
|
+
MAX_JITTER = 0.5 # seconds
|
|
123
|
+
|
|
124
|
+
# --- Inference configuration constants ---
|
|
125
|
+
# Temperature por tipo de intención (menor = más preciso)
|
|
126
|
+
TRANSLATION_TEMPERATURE = 0.2
|
|
127
|
+
EXPLANATION_TEMPERATURE = 0.3
|
|
128
|
+
CODE_GENERATION_TEMPERATURE = 0.2
|
|
129
|
+
ARCHITECTURE_REVIEW_TEMPERATURE = 0.2
|
|
130
|
+
DEFAULT_TEMPERATURE = 0.2
|
|
131
|
+
|
|
132
|
+
# Max tokens por tipo de intención
|
|
133
|
+
TRANSLATION_MAX_TOKENS = 2048
|
|
134
|
+
EXPLANATION_MAX_TOKENS = 1024
|
|
135
|
+
CODE_GENERATION_MAX_TOKENS = 4096
|
|
136
|
+
ARCHITECTURE_REVIEW_MAX_TOKENS = 4096
|
|
137
|
+
DEFAULT_MAX_TOKENS = 4096
|
|
138
|
+
|
|
139
|
+
# Top-P compartido para todas las intenciones
|
|
140
|
+
TOP_P = 0.9
|
|
141
|
+
|
|
142
|
+
SYSTEM_PROMPT = """Eres un experto en AWS con 15 años de experiencia.
|
|
143
|
+
Tu trabajo es traducir intenciones en lenguaje natural a comandos AWS CLI exactos, seguros y eficientes.
|
|
144
|
+
|
|
145
|
+
REGLAS CRÍTICAS:
|
|
146
|
+
1. SIEMPRE devuelve JSON válido con esta estructura:
|
|
147
|
+
{
|
|
148
|
+
"command": "aws <service> <action> ...flags",
|
|
149
|
+
"explanation": "Resumen de 1 línea en el mismo idioma del input",
|
|
150
|
+
"detailed_explanation": "Explicación de 3-5 líneas de qué hace cada flag importante",
|
|
151
|
+
"risk_level": "low|medium|high|critical",
|
|
152
|
+
"estimated_cost": "$X.XX per month o per request",
|
|
153
|
+
"requires_dry_run": boolean,
|
|
154
|
+
"affected_resources": ["lista de recursos afectados"],
|
|
155
|
+
"flags_used": {"flag_name": "explicación breve"}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
2. Si la intención es ambigua, devuelve:
|
|
159
|
+
{
|
|
160
|
+
"clarification_needed": true,
|
|
161
|
+
"clarification_question": "Pregunta específica para clarificar"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
3. Riesgo:
|
|
165
|
+
- low: SIEMPRE para read-only (list, describe, get, head, wait, show, ls)
|
|
166
|
+
- medium: create/update reversible
|
|
167
|
+
- high: delete de UN recurso específico, terminate
|
|
168
|
+
- critical: delete recursivo, force sin confirmación, o que afecta múltiples recursos/producción
|
|
169
|
+
IMPORTANTE: los comandos describe-* y list-* SIEMPRE son "low", sin importar los filtros o queries.
|
|
170
|
+
|
|
171
|
+
4. Comandos destructivos DEBEN marcar requires_dry_run: true
|
|
172
|
+
|
|
173
|
+
5. NUNCA generes comandos con shell operators: |, &&, ;, xargs, $(), backticks.
|
|
174
|
+
Solo genera UN ÚNICO comando `aws` puro. Si la operación requiere múltiples IDs,
|
|
175
|
+
pásalos como argumentos separados por espacio (ej: --instance-ids id1 id2 id3).
|
|
176
|
+
Si la operación NO se puede hacer con un solo comando, usa "clarification_needed" y
|
|
177
|
+
explica qué pasos seguir manualmente.
|
|
178
|
+
EJEMPLO INCORRECTO: aws iam generate-credential-report && aws iam get-credential-report
|
|
179
|
+
EJEMPLO INCORRECTO: aws ec2 describe-instances | xargs aws ec2 terminate-instances
|
|
180
|
+
EJEMPLO INCORRECTO: aws s3 ls $(aws sts get-caller-identity --query Account --output text)
|
|
181
|
+
EJEMPLO CORRECTO: aws iam get-credential-report --output json
|
|
182
|
+
EJEMPLO CORRECTO: aws ec2 terminate-instances --instance-ids i-123 i-456 i-789
|
|
183
|
+
|
|
184
|
+
Si el usuario pide una operación que necesita múltiples pasos (ej: "elimina todas las instancias activas"),
|
|
185
|
+
responde con clarification_needed: true y explica los pasos que debe seguir manualmente.
|
|
186
|
+
|
|
187
|
+
6. Usa flags modernos:
|
|
188
|
+
- --output json por defecto (el usuario quiere parseable)
|
|
189
|
+
- --output table cuando el usuario pide listar/mostrar (más legible)
|
|
190
|
+
- --no-paginate cuando el resultado es chico
|
|
191
|
+
- --query para filtrar server-side (IMPORTANTE: los alias/keys en --query SIEMPRE en ASCII, nunca usar caracteres Unicode. Ej: {Name:Name,Created:CreationDate} NO {名称:Name})
|
|
192
|
+
- --filters en lugar de client-side
|
|
193
|
+
|
|
194
|
+
7. Idioma: TODAS las cadenas de texto dirigidas al usuario (explanation, detailed_explanation,
|
|
195
|
+
estimated_cost, tip, related_commands descriptions) deben estar en el MISMO idioma del input.
|
|
196
|
+
Los comandos AWS y nombres de flags permanecen en inglés.
|
|
197
|
+
|
|
198
|
+
8. Incluye siempre estos campos adicionales en tu respuesta JSON:
|
|
199
|
+
"tip": "Un consejo educativo breve relacionado al comando (en el idioma del input)",
|
|
200
|
+
"related_commands": [{"command": "aws ...", "description": "breve descripción en el idioma del input"}]
|
|
201
|
+
|
|
202
|
+
EJEMPLOS:
|
|
203
|
+
|
|
204
|
+
Input: "lista los buckets de S3"
|
|
205
|
+
Output: {
|
|
206
|
+
"command": "aws s3api list-buckets --query 'Buckets[].{Name:Name,Created:CreationDate}' --output table",
|
|
207
|
+
"explanation": "Lista todos los buckets de S3 con nombre y fecha de creación",
|
|
208
|
+
"detailed_explanation": "Usa s3api (API directa) en lugar de s3 (alto nivel) para mejor control. --query filtra server-side y --output table da formato legible.",
|
|
209
|
+
"risk_level": "low",
|
|
210
|
+
"estimated_cost": "$0.00",
|
|
211
|
+
"requires_dry_run": false,
|
|
212
|
+
"affected_resources": [],
|
|
213
|
+
"flags_used": {"--query": "JMESPath filter", "--output": "table format"}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Input: "borra todos los objetos del bucket de logs"
|
|
217
|
+
Output: {
|
|
218
|
+
"command": "aws s3 rm s3://logs-bucket-name/ --recursive",
|
|
219
|
+
"explanation": "Elimina TODOS los objetos del bucket de logs recursivamente",
|
|
220
|
+
"detailed_explanation": "⚠️ ESTA ACCIÓN ES IRREVERSIBLE. Borra todos los objetos pero NO el bucket. Para borrar el bucket también usa s3api delete-bucket. Recomendamos primero: aws s3 ls s3://logs-bucket-name/ --recursive | wc -l para contar archivos.",
|
|
221
|
+
"risk_level": "critical",
|
|
222
|
+
"estimated_cost": "$0.00",
|
|
223
|
+
"requires_dry_run": true,
|
|
224
|
+
"affected_resources": ["s3://logs-bucket-name/*"],
|
|
225
|
+
"flags_used": {"--recursive": "Borra todos los objetos, no solo el primero"}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Input: "muéstrame las lambdas que fallaron ayer"
|
|
229
|
+
Output: {
|
|
230
|
+
"command": "aws lambda list-functions --query 'Functions[?State==`Failed`].{Name:FunctionName,Runtime:Runtime,LastModified:LastModified}' --output table",
|
|
231
|
+
"explanation": "Lista funciones Lambda en estado Failed",
|
|
232
|
+
"detailed_explanation": "Filtra server-side usando --query. Para más detalles de un fallo específico: aws logs filter-log-events --log-group-name /aws/lambda/FUNCTION_NAME",
|
|
233
|
+
"risk_level": "low",
|
|
234
|
+
"estimated_cost": "$0.00",
|
|
235
|
+
"requires_dry_run": false,
|
|
236
|
+
"affected_resources": [],
|
|
237
|
+
"flags_used": {"--query": "Filter by state", "--output": "table"}
|
|
238
|
+
}
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
region: str = REGION,
|
|
244
|
+
*,
|
|
245
|
+
model_id: str = MODEL_ID,
|
|
246
|
+
max_retries: int = MAX_RETRIES,
|
|
247
|
+
base_delay: float = BASE_DELAY,
|
|
248
|
+
) -> None:
|
|
249
|
+
self.client = boto3.client("bedrock-runtime", region_name=region)
|
|
250
|
+
self.region = region
|
|
251
|
+
self.model_id = model_id
|
|
252
|
+
self.max_retries = max_retries
|
|
253
|
+
self.base_delay = base_delay
|
|
254
|
+
|
|
255
|
+
def translate(self, intent: Intent) -> Translation:
|
|
256
|
+
"""Translate an Intent into an AWS CLI command.
|
|
257
|
+
|
|
258
|
+
Retries transient errors (throttling, timeouts) with exponential
|
|
259
|
+
backoff and jitter. Non-transient errors fail immediately.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
intent: The parsed user intent
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Translation object with the command and metadata
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
BedrockError: If translation fails after all retries (for transient)
|
|
269
|
+
or immediately (for non-transient errors).
|
|
270
|
+
"""
|
|
271
|
+
user_message = self._build_user_message(intent)
|
|
272
|
+
response = self._call_converse_with_retry(user_message)
|
|
273
|
+
return self._parse_response(response)
|
|
274
|
+
|
|
275
|
+
def _call_converse_with_retry(self, user_message: str) -> dict[str, Any]:
|
|
276
|
+
"""Call Bedrock Converse API with exponential backoff for transient errors.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
user_message: The formatted user message to send.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The raw Bedrock Converse API response dict.
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
BedrockError: If a non-retryable error occurs, or after exhausting retries.
|
|
286
|
+
"""
|
|
287
|
+
last_error: BedrockError | None = None
|
|
288
|
+
|
|
289
|
+
for attempt in range(self.max_retries + 1):
|
|
290
|
+
try:
|
|
291
|
+
return self.client.converse( # type: ignore[no-any-return]
|
|
292
|
+
modelId=self.model_id,
|
|
293
|
+
messages=[
|
|
294
|
+
{
|
|
295
|
+
"role": "user",
|
|
296
|
+
"content": [{"text": user_message}],
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
system=[{"text": self.SYSTEM_PROMPT}],
|
|
300
|
+
inferenceConfig={
|
|
301
|
+
"maxTokens": self.TRANSLATION_MAX_TOKENS,
|
|
302
|
+
"temperature": self.TRANSLATION_TEMPERATURE,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
except ClientError as e:
|
|
306
|
+
error_code = e.response.get("Error", {}).get("Code", "")
|
|
307
|
+
bedrock_error = self._map_client_error(error_code, e)
|
|
308
|
+
last_error = bedrock_error
|
|
309
|
+
|
|
310
|
+
if not bedrock_error.is_retryable:
|
|
311
|
+
raise bedrock_error from e
|
|
312
|
+
|
|
313
|
+
if attempt < self.max_retries:
|
|
314
|
+
delay = self._calculate_backoff(attempt)
|
|
315
|
+
logger.warning(
|
|
316
|
+
"Transient error '%s' on attempt %d/%d, retrying in %.2fs",
|
|
317
|
+
error_code,
|
|
318
|
+
attempt + 1,
|
|
319
|
+
self.max_retries + 1,
|
|
320
|
+
delay,
|
|
321
|
+
)
|
|
322
|
+
time.sleep(delay)
|
|
323
|
+
else:
|
|
324
|
+
raise bedrock_error from e
|
|
325
|
+
|
|
326
|
+
except ReadTimeoutError as e:
|
|
327
|
+
last_error = BedrockError(
|
|
328
|
+
"Request to Bedrock timed out",
|
|
329
|
+
error_type=BedrockErrorType.TIMEOUT,
|
|
330
|
+
suggestion="Check your network connection and try again",
|
|
331
|
+
technical_detail=str(e),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if attempt < self.max_retries:
|
|
335
|
+
delay = self._calculate_backoff(attempt)
|
|
336
|
+
logger.warning(
|
|
337
|
+
"Read timeout on attempt %d/%d, retrying in %.2fs",
|
|
338
|
+
attempt + 1,
|
|
339
|
+
self.max_retries + 1,
|
|
340
|
+
delay,
|
|
341
|
+
)
|
|
342
|
+
time.sleep(delay)
|
|
343
|
+
else:
|
|
344
|
+
raise last_error from e
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
# Non-transient unexpected errors fail immediately
|
|
348
|
+
raise BedrockError(
|
|
349
|
+
"Unexpected error communicating with Bedrock",
|
|
350
|
+
error_type=BedrockErrorType.GENERIC,
|
|
351
|
+
suggestion="Check AWS connectivity and try again",
|
|
352
|
+
technical_detail=str(e),
|
|
353
|
+
) from e
|
|
354
|
+
|
|
355
|
+
# Nunca debería llegar aquí, pero por seguridad de tipos
|
|
356
|
+
assert last_error is not None # noqa: S101
|
|
357
|
+
raise last_error
|
|
358
|
+
|
|
359
|
+
def _calculate_backoff(self, attempt: int) -> float:
|
|
360
|
+
"""Calculate exponential backoff delay with jitter.
|
|
361
|
+
|
|
362
|
+
Formula: base_delay * 2^attempt + random_jitter
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
attempt: Zero-indexed attempt number.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Delay in seconds before the next retry.
|
|
369
|
+
"""
|
|
370
|
+
exponential_delay = self.base_delay * (2**attempt)
|
|
371
|
+
jitter = random.uniform(0, self.MAX_JITTER) # noqa: S311
|
|
372
|
+
return exponential_delay + jitter # type: ignore[no-any-return]
|
|
373
|
+
|
|
374
|
+
def _parse_response(self, response: dict[str, Any]) -> Translation:
|
|
375
|
+
"""Parse and validate the Bedrock Converse API response.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
response: Raw response from client.converse().
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
A Translation model with the parsed data.
|
|
382
|
+
|
|
383
|
+
Raises:
|
|
384
|
+
BedrockError: If response structure is invalid or JSON is malformed.
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
response_text = response["output"]["message"]["content"][0]["text"]
|
|
388
|
+
except (KeyError, IndexError) as e:
|
|
389
|
+
raise BedrockError(
|
|
390
|
+
"Bedrock returned an empty or malformed response",
|
|
391
|
+
error_type=BedrockErrorType.INVALID_RESPONSE,
|
|
392
|
+
suggestion="Try rephrasing your request or try again later",
|
|
393
|
+
technical_detail=str(e),
|
|
394
|
+
) from e
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
data = self._extract_json(response_text)
|
|
398
|
+
except json.JSONDecodeError as e:
|
|
399
|
+
raise BedrockError(
|
|
400
|
+
"Model returned invalid JSON — could not parse translation",
|
|
401
|
+
error_type=BedrockErrorType.INVALID_RESPONSE,
|
|
402
|
+
suggestion="Try rephrasing your request with more specific details",
|
|
403
|
+
technical_detail=f"JSON parse error: {e}",
|
|
404
|
+
) from e
|
|
405
|
+
|
|
406
|
+
# Handle clarification requests from the LLM
|
|
407
|
+
if data.get("clarification_needed"):
|
|
408
|
+
raise BedrockError(
|
|
409
|
+
data.get(
|
|
410
|
+
"clarification_question", "Please be more specific about what you want to do."
|
|
411
|
+
),
|
|
412
|
+
error_type=BedrockErrorType.INVALID_RESPONSE,
|
|
413
|
+
suggestion=data.get("clarification_question", "Try specifying exact resource IDs."),
|
|
414
|
+
technical_detail="LLM requested clarification instead of generating a command",
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return Translation(
|
|
418
|
+
command=data["command"],
|
|
419
|
+
explanation=data.get("explanation", ""),
|
|
420
|
+
detailed_explanation=data.get("detailed_explanation", ""),
|
|
421
|
+
risk_level=data.get("risk_level", "low"),
|
|
422
|
+
estimated_cost=data.get("estimated_cost", "$0.00"),
|
|
423
|
+
requires_dry_run=data.get("requires_dry_run", False),
|
|
424
|
+
affected_resources=data.get("affected_resources", []),
|
|
425
|
+
flags_used=data.get("flags_used", {}),
|
|
426
|
+
tip=data.get("tip"),
|
|
427
|
+
related_commands=data.get("related_commands", []),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
def _map_client_error(self, error_code: str, exc: ClientError) -> BedrockError:
|
|
431
|
+
"""Map a botocore ClientError code to a structured BedrockError."""
|
|
432
|
+
error_map: dict[str, tuple[BedrockErrorType, str, str]] = {
|
|
433
|
+
"AccessDeniedException": (
|
|
434
|
+
BedrockErrorType.CREDENTIALS,
|
|
435
|
+
"Access denied — insufficient permissions to invoke Bedrock",
|
|
436
|
+
"Check that your IAM role has bedrock:InvokeModel permission",
|
|
437
|
+
),
|
|
438
|
+
"UnrecognizedClientException": (
|
|
439
|
+
BedrockErrorType.CREDENTIALS,
|
|
440
|
+
"AWS credentials are invalid or expired",
|
|
441
|
+
"Run 'aws sts get-caller-identity' to verify your credentials",
|
|
442
|
+
),
|
|
443
|
+
"ExpiredTokenException": (
|
|
444
|
+
BedrockErrorType.CREDENTIALS,
|
|
445
|
+
"AWS session token has expired",
|
|
446
|
+
"Refresh your credentials (e.g., re-run 'aws sso login')",
|
|
447
|
+
),
|
|
448
|
+
"ThrottlingException": (
|
|
449
|
+
BedrockErrorType.THROTTLING,
|
|
450
|
+
"Request was throttled by Bedrock",
|
|
451
|
+
"Try again in a few seconds. If you hit the daily token limit, wait until tomorrow or request a quota increase in the AWS console (Service Quotas → Bedrock)",
|
|
452
|
+
),
|
|
453
|
+
"ServiceQuotaExceededException": (
|
|
454
|
+
BedrockErrorType.THROTTLING,
|
|
455
|
+
"Bedrock service quota exceeded",
|
|
456
|
+
"Wait a moment or request a quota increase in the AWS console",
|
|
457
|
+
),
|
|
458
|
+
"ModelTimeoutException": (
|
|
459
|
+
BedrockErrorType.TIMEOUT,
|
|
460
|
+
"Model took too long to respond",
|
|
461
|
+
"Try again — the model may be under heavy load",
|
|
462
|
+
),
|
|
463
|
+
"ModelNotReadyException": (
|
|
464
|
+
BedrockErrorType.MODEL_NOT_AVAILABLE,
|
|
465
|
+
"The model is not ready to serve requests",
|
|
466
|
+
"Wait a few minutes and try again",
|
|
467
|
+
),
|
|
468
|
+
"ResourceNotFoundException": (
|
|
469
|
+
BedrockErrorType.MODEL_NOT_AVAILABLE,
|
|
470
|
+
"Model not found — it may not be available in your region",
|
|
471
|
+
(f"Verify that model '{self.model_id}' is enabled in region '{self.region}'"),
|
|
472
|
+
),
|
|
473
|
+
"ValidationException": (
|
|
474
|
+
BedrockErrorType.INVALID_RESPONSE,
|
|
475
|
+
"Request validation failed",
|
|
476
|
+
"Try rephrasing your request with fewer special characters",
|
|
477
|
+
),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if error_code in error_map:
|
|
481
|
+
err_type, msg, suggestion = error_map[error_code]
|
|
482
|
+
return BedrockError(
|
|
483
|
+
msg,
|
|
484
|
+
error_type=err_type,
|
|
485
|
+
suggestion=suggestion,
|
|
486
|
+
technical_detail=str(exc),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return BedrockError(
|
|
490
|
+
f"AWS error: {error_code}",
|
|
491
|
+
error_type=BedrockErrorType.GENERIC,
|
|
492
|
+
suggestion="Check AWS service status and try again",
|
|
493
|
+
technical_detail=str(exc),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
def _build_user_message(self, intent: Intent) -> str:
|
|
497
|
+
"""Build the user message for Bedrock."""
|
|
498
|
+
from datetime import date, timedelta
|
|
499
|
+
|
|
500
|
+
today = date.today()
|
|
501
|
+
seven_days_ago = today - timedelta(days=7)
|
|
502
|
+
|
|
503
|
+
return f"""Input: "{intent.raw_input}"
|
|
504
|
+
|
|
505
|
+
Language detected: {intent.detected_language}
|
|
506
|
+
Service detected: {intent.service}
|
|
507
|
+
Action detected: {intent.action}
|
|
508
|
+
Region: {intent.region or "default (us-east-1)"}
|
|
509
|
+
Today's date: {today.isoformat()}
|
|
510
|
+
Seven days ago: {seven_days_ago.isoformat()}
|
|
511
|
+
Response language: ALL user-facing text MUST be in "{intent.detected_language}"
|
|
512
|
+
|
|
513
|
+
Translate this to an AWS CLI command. Return ONLY the JSON object."""
|
|
514
|
+
|
|
515
|
+
def _extract_json(self, text: str) -> dict[str, Any]:
|
|
516
|
+
"""Extract JSON from Claude's response (handles markdown code blocks)."""
|
|
517
|
+
text = text.strip()
|
|
518
|
+
|
|
519
|
+
# Strip markdown code fences if present
|
|
520
|
+
if text.startswith("```"):
|
|
521
|
+
lines = text.split("\n")
|
|
522
|
+
text = "\n".join(lines[1:-1]) if lines[-1].startswith("```") else "\n".join(lines[1:])
|
|
523
|
+
|
|
524
|
+
result: dict[str, Any] = json.loads(text)
|
|
525
|
+
return result
|