Skip to content

Commit 7510944

Browse files
authored
updated to support Claude sonnet 4.0 and inference profiles (#32)
* updated to support Claude sonnet 4.0 and inference profiles * fix: AMI version prompt * fix: added impact analysis * fix: version bump, docs update
1 parent d338375 commit 7510944

File tree

5 files changed

+96
-99
lines changed

5 files changed

+96
-99
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Changelog
2+
3+
## [Unreleased]
4+
5+
## [1.0.0] - 2025-10-03
6+
7+
### Added
8+
- Enhanced AI analysis with structured impact assessment including security concerns, configuration issues, operational impact, and recommendations
9+
- Improved error handling and JSON parsing for Bedrock responses
10+
- Better AMI analysis with direct technical output
11+
12+
### Changed
13+
- **BREAKING**: Updated default Bedrock model from `anthropic.claude-3-sonnet-20240229-v1:0` to `global.anthropic.claude-sonnet-4-20250514-v1:0` (supports cross-region inference profiles)
14+
- Increased default Lambda timeout from 120 seconds to 300 seconds for better performance with Claude 4.0
15+
- Restructured AI prompts for more focused and technical analysis output
16+
- Improved system prompts to reduce conversational language in responses
17+
- Enhanced JSON response parsing with better error handling and fallback mechanisms

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,14 @@ Enhance your Terraform workflows with AI-powered insights while maintaining secu
199199
|------|-------------|------|---------|:--------:|
200200
| <a name="input_aws_region"></a> [aws\_region](#input\_aws\_region) | The region from which this module will be executed. | `string` | n/a | yes |
201201
| <a name="input_hcp_tf_org"></a> [hcp\_tf\_org](#input\_hcp\_tf\_org) | HCP Terraform Organization name | `string` | n/a | yes |
202-
| <a name="input_bedrock_llm_model"></a> [bedrock\_llm\_model](#input\_bedrock\_llm\_model) | Bedrock LLM model to use | `string` | `"anthropic.claude-3-sonnet-20240229-v1:0"` | no |
202+
| <a name="input_bedrock_llm_model"></a> [bedrock\_llm\_model](#input\_bedrock\_llm\_model) | Bedrock LLM model to use (supports cross-region inference profiles) | `string` | `"global.anthropic.claude-sonnet-4-20250514-v1:0"` | no |
203203
| <a name="input_cloudwatch_log_group_name"></a> [cloudwatch\_log\_group\_name](#input\_cloudwatch\_log\_group\_name) | RunTask CloudWatch log group name | `string` | `"/hashicorp/terraform/runtask/"` | no |
204204
| <a name="input_cloudwatch_log_group_retention"></a> [cloudwatch\_log\_group\_retention](#input\_cloudwatch\_log\_group\_retention) | Lambda CloudWatch log group retention period | `string` | `"365"` | no |
205205
| <a name="input_deploy_waf"></a> [deploy\_waf](#input\_deploy\_waf) | Set to true to deploy CloudFront and WAF in front of the Lambda function URL | `string` | `false` | no |
206206
| <a name="input_event_bus_name"></a> [event\_bus\_name](#input\_event\_bus\_name) | EventBridge event bus name | `string` | `"default"` | no |
207207
| <a name="input_event_source"></a> [event\_source](#input\_event\_source) | EventBridge source name | `string` | `"app.terraform.io"` | no |
208208
| <a name="input_lambda_architecture"></a> [lambda\_architecture](#input\_lambda\_architecture) | Lambda architecture (arm64 or x86\_64) | `string` | `"x86_64"` | no |
209-
| <a name="input_lambda_default_timeout"></a> [lambda\_default\_timeout](#input\_lambda\_default\_timeout) | Lambda default timeout in seconds | `number` | `120` | no |
209+
| <a name="input_lambda_default_timeout"></a> [lambda\_default\_timeout](#input\_lambda\_default\_timeout) | Lambda default timeout in seconds | `number` | `300` | no |
210210
| <a name="input_lambda_python_runtime"></a> [lambda\_python\_runtime](#input\_lambda\_python\_runtime) | Lambda Python runtime | `string` | `"python3.11"` | no |
211211
| <a name="input_lambda_reserved_concurrency"></a> [lambda\_reserved\_concurrency](#input\_lambda\_reserved\_concurrency) | Maximum Lambda reserved concurrency, make sure your AWS quota is sufficient | `number` | `10` | no |
212212
| <a name="input_name_prefix"></a> [name\_prefix](#input\_name\_prefix) | Name to be used on all the resources as identifier. | `string` | `"runtask-tf-plan-analyzer"` | no |

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.2.0
1+
v1.0.0

lambda/runtask_fulfillment/ai.py

Lines changed: 73 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -34,40 +34,16 @@ def eval(tf_plan_json):
3434

3535
logger.info("##### Evaluating Terraform plan output #####")
3636
prompt = """
37-
List the resources that will be created, modified or deleted in the following terraform plan using the following rules:
38-
1. Think step by step using the "thinking" json field
39-
2. For AMI changes, include the old and new AMI ID
40-
3. Use the following schema. Skip the preamble:
41-
<schema>
42-
{
43-
"$id": "https://example.com/arrays.schema.json",
44-
"$schema": "https://json-schema.org/draft/2020-12/schema",
45-
"type": "object",
46-
"properties": {
47-
"thinking": {
48-
"type": "string",
49-
"description": "Think step by step"
50-
},
51-
"resources": {
52-
"type": "string",
53-
"description": "A list of resources that will be created, modified or deleted"
54-
}
55-
}
56-
}
57-
</schema>
58-
Here is an example of the output:
59-
<example>
60-
{
61-
"thinking": "To list the resources that will be created, modified or deleted, I will go through the terraform plan and look for the 'actions' field in each resource change. If the actions include 'create', 'update', or 'delete', I will add that resource to the list. For AMI changes, I will include the old and new AMI ID.",
62-
"resources": "The following resources will be modified: RESOURCES"
63-
}
64-
</example>
65-
Now, list the resources that will be created, modified or deleted in the following terraform plan"""
37+
You must respond with ONLY a JSON object. Do not include any explanatory text, conversation, or markdown formatting.
38+
39+
Analyze the terraform plan and return this exact JSON structure:
40+
{"thinking": "brief analysis", "resources": "list of resources being created, modified, or deleted", "impact_analysis": "assessment formatted as markdown with sections: ## 🔍 Impact Analysis\n\n### 🚨 Security Concerns\n- **Critical/High/Medium**: Description\n- **Risk Level**: Assessment\n\n### ⚠️ Configuration Issues\n- **Issue Type**: Description\n- **Impact**: Consequence\n\n### 📊 Operational Impact\n- **Infrastructure**: What's being deployed\n- **Cost**: Cost implications\n\n### 💡 Recommendations\n- **Priority 1**: Most critical fix\n- **Priority 2**: Secondary concerns\n- **Warning**: Important warnings"}
41+
42+
Terraform plan:
43+
"""
6644

6745
prompt += f"""
68-
<terraform_plan>
6946
{tf_plan_json["resource_changes"]}
70-
</terraform_plan>
7147
"""
7248

7349
messages = [
@@ -89,54 +65,34 @@ def eval(tf_plan_json):
8965

9066
logger.debug("Analysis response: {}".format(analysis_response))
9167

92-
analysis_response_text= clean_response(analysis_response["content"][0]["text"])["resources"]
68+
try:
69+
parsed_response = clean_response(analysis_response["content"][0]["text"])
70+
analysis_response_text = parsed_response["resources"]
71+
impact_analysis_text = parsed_response.get("impact_analysis", "No impact analysis available")
72+
except Exception as e:
73+
logger.error(f"Error parsing analysis response: {e}")
74+
analysis_response_text = "Error: Could not parse Terraform plan analysis"
75+
impact_analysis_text = "Error: Could not parse impact analysis"
9376

9477
logger.debug("Analysis response Text: {}".format(analysis_response_text))
9578

9679
#####################################################################
9780
######## Secondly, evaluate AMIs per analysis ########
9881
#####################################################################
9982
logger.info("##### Evaluating AMI information #####")
100-
prompt = """
101-
Find additional details of infrastructure changes using the following rules
102-
1. For Amazon machine image (AMI or image_id) modifications, compare the old AMI information against the new AMI, including linux kernel, docker and ecs agent using the get_ami_releases function.
103-
2. Think step by step using "thinking" tags field
104-
3. Use the following schema. Skip the preamble:
105-
<output>
106-
<thinking>
107-
</thinking>
108-
<result>
109-
## Current AMI ID
110-
* AMI name:
111-
* OS Architecture:
112-
* OS Name:
113-
* kernel:
114-
* docker version:
115-
* ECS agent:
116-
117-
## New AMI ID
118-
* AMI name:
119-
* kernel:
120-
* OS Architecture:
121-
* OS Name:
122-
* docker version:
123-
* ECS agent:
124-
</result>
125-
<output>
126-
Now, given the following analysis, compare any old with new AMIs:
127-
"""
83+
prompt = f"""
84+
For any Amazon Machine Image (AMI) changes in this analysis, use the get_ami_releases function to compare old and new AMI details including kernel, docker, and ECS agent versions.
12885
129-
prompt += f"""
130-
<analysis>{analysis_response_text}</analysis>
131-
"""
86+
Analysis: {analysis_response_text}
87+
"""
13288

13389
messages = [{"role": "user", "content": [{"text": prompt}]}]
13490

13591
stop_reason, response = stream_messages(
13692
bedrock_client=bedrock_client,
13793
model_id=model_id,
13894
messages=messages,
139-
system_text=system_text,
95+
system_text="Provide direct, technical analysis of AMI changes without conversational language.",
14096
tool_config=tool_config,
14197
)
14298

@@ -173,53 +129,51 @@ def eval(tf_plan_json):
173129
bedrock_client=bedrock_client,
174130
model_id=model_id,
175131
messages=messages,
176-
system_text=system_text,
132+
system_text="Provide direct, technical analysis of AMI changes without conversational language.",
177133
tool_config=tool_config,
178-
stop_sequences=["</result>"],
179134
)
180135

181136
# Add response to message history
182137
messages.append(response)
183138

184-
# Try to parse output as XML and look for the <output> tag
185-
try:
186-
root = ET.fromstring(response["content"][0]["text"])
187-
result = root.find("result").text
188-
logger.info("Parsed : {}".format(result))
189-
except Exception as e:
139+
# Extract the actual response text from Bedrock
140+
if response and "content" in response and len(response["content"]) > 0:
190141
result = response["content"][0]["text"]
191-
logger.info("Non Parsed : {}".format(result))
142+
logger.debug("AMI analysis response: {}".format(result))
143+
else:
144+
result = "Error: No AMI analysis response received from Bedrock"
145+
logger.error("No AMI analysis content received from Bedrock")
192146

193147
#####################################################################
194148
######### Third, generate short summary #########
195149
#####################################################################
196150

197151
logger.info("##### Generating short summary #####")
198152
prompt = f"""
199-
List the resources that will be created, modified or deleted in the following terraform plan using the following rules:
200-
- Provide summary of the infrastructure changes
201-
- Highlight major components of the changes such as what Terraform modules is executed
202-
- Summarize what each Terraform modules does
203-
- Highlight any resources that being replaced or deleted
204-
- Highlight any outputs if available
205-
206-
<terraform_plan>
207-
{tf_plan_json["resource_changes"]}
208-
</terraform_plan>
209-
"""
153+
Provide a concise summary of these Terraform changes. Focus on what resources are being created, modified, or deleted:
154+
155+
{tf_plan_json["resource_changes"]}
156+
"""
210157
message_desc = [{"role": "user", "content": [{"text": prompt}]}]
211158
stop_reason, response = stream_messages(
212159
bedrock_client=bedrock_client,
213160
model_id=model_id,
214161
messages=message_desc,
215-
system_text=system_text,
216-
tool_config=tool_config,
217-
stop_sequences=["</result>"],
162+
system_text="Provide a direct, technical summary without conversational language.",
163+
tool_config=None,
218164
)
219-
description = response["content"][0]["text"]
165+
166+
# Extract the actual response text from Bedrock
167+
if response and "content" in response and len(response["content"]) > 0:
168+
description = response["content"][0]["text"]
169+
logger.debug("Full Bedrock response: {}".format(description))
170+
else:
171+
description = "Error: No response received from Bedrock"
172+
logger.error("No response content received from Bedrock")
220173

221174
logger.info("##### Report #####")
222175
logger.info("Analysis : {}".format(analysis_response_text))
176+
logger.info("Impact Analysis: {}".format(impact_analysis_text))
223177
logger.info("AMI summary: {}".format(result))
224178
logger.info("Terraform plan summary: {}".format(description))
225179

@@ -232,6 +186,12 @@ def eval(tf_plan_json):
232186
results.append(generate_runtask_result(outcome_id="Plan-Summary", description="Summary of Terraform plan", result="Output omitted due to : {}".format(guardrail_response)))
233187
description = "Bedrock guardrail triggered : {}".format(guardrail_response)
234188

189+
guardrail_status, guardrail_response = guardrail_inspection(str(impact_analysis_text))
190+
if guardrail_status:
191+
results.append(generate_runtask_result(outcome_id="Impact-Analysis", description="Security and operational impact assessment", result=impact_analysis_text[:9000]))
192+
else:
193+
results.append(generate_runtask_result(outcome_id="Impact-Analysis", description="Security and operational impact assessment", result="Output omitted due to : {}".format(guardrail_response)))
194+
235195
guardrail_status, guardrail_response = guardrail_inspection(str(result))
236196
if guardrail_status:
237197
results.append(generate_runtask_result(outcome_id="AMI-Summary", description="Summary of AMI changes", result=result[:700]))
@@ -279,8 +239,28 @@ def guardrail_inspection(input_text, input_mode = 'OUTPUT'):
279239
return True, "Guardrail inspection skipped"
280240

281241
def clean_response(json_str):
282-
# Remove any tags in the format <tag> or </tag>
283-
cleaned_str = re.sub(r'<\/?[\w\s]+>', '', json_str)
284-
last_brace_index = cleaned_str.rfind('}')
285-
cleaned_str = cleaned_str[:last_brace_index + 1]
286-
return json.loads(cleaned_str)
242+
try:
243+
# First try to parse as-is
244+
return json.loads(json_str)
245+
except json.JSONDecodeError:
246+
try:
247+
# Remove any tags in the format <tag> or </tag>
248+
cleaned_str = re.sub(r'<\/?[\w\s]+>', '', json_str)
249+
250+
# Find JSON content between braces
251+
start_brace = cleaned_str.find('{')
252+
last_brace = cleaned_str.rfind('}')
253+
254+
if start_brace != -1 and last_brace != -1 and last_brace > start_brace:
255+
json_content = cleaned_str[start_brace:last_brace + 1]
256+
return json.loads(json_content)
257+
else:
258+
logger.error(f"No valid JSON braces found in response: {json_str}")
259+
return {"resources": "Error: No valid JSON structure found"}
260+
261+
except json.JSONDecodeError as e:
262+
logger.error(f"JSON decode error after cleaning: {e}, Original string: {json_str}")
263+
return {"resources": "Error: Could not parse response as JSON"}
264+
except Exception as e:
265+
logger.error(f"Unexpected error in clean_response: {e}, Original string: {json_str}")
266+
return {"resources": "Error: Unexpected parsing error"}

variables.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ variable "lambda_reserved_concurrency" {
8989
variable "lambda_default_timeout" {
9090
description = "Lambda default timeout in seconds"
9191
type = number
92-
default = 120
92+
default = 300
9393
}
9494

9595
variable "lambda_architecture" {
@@ -148,7 +148,7 @@ variable "waf_managed_rule_set" {
148148
}
149149

150150
variable "bedrock_llm_model" {
151-
description = "Bedrock LLM model to use"
151+
description = "Bedrock LLM model to use (supports cross-region inference profiles)"
152152
type = string
153-
default = "anthropic.claude-3-sonnet-20240229-v1:0"
153+
default = "global.anthropic.claude-sonnet-4-20250514-v1:0"
154154
}

0 commit comments

Comments
 (0)