File size: 15,615 Bytes
da4f611
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
"""
Prompt Preprocessor for Terraform Code Generation
===================================================
Uses TF-KB to enhance raw prompts before sending to LLM.

Pipeline:
  1. Entity Extraction: NL β†’ resource types (via TF-KB)
  2. Knowledge Enrichment: inject required args, dependencies
  3. Prompt Augmentation: format enhanced prompt

Usage:
    from prompt_preprocessor import PromptPreprocessor
    pp = PromptPreprocessor()
    enhanced = pp.enhance_prompt("Create an S3 bucket with versioning")
"""

import re
from tf_knowledge_base import TerraformKB


class PromptPreprocessor:
    def __init__(self, confidence_threshold: float = 0.65):
        self.kb = TerraformKB()
        self.confidence_threshold = confidence_threshold
    
    def extract_resource_types(self, prompt: str, 
                                ground_truth_resources: list[str] = None) -> list[str]:
        """
        Extract resource types from prompt.
        If ground_truth_resources provided (from IaC-Eval dataset), use those directly.
        Otherwise, use NL extraction from TF-KB.
        """
        if ground_truth_resources:
            # Validate against KB β€” catch hallucinated types
            validated = []
            for rt in ground_truth_resources:
                rt = rt.strip()
                if self.kb.resource_exists(rt):
                    validated.append(rt)
                else:
                    # Try to find the correct resource type
                    similar = self.kb.find_similar_resources(rt, top_k=1)
                    if similar and similar[0][1] > 0.8:
                        validated.append(similar[0][0])
            return list(dict.fromkeys(validated))  # deduplicate, preserve order
        
        # NL extraction
        candidates = self.kb.extract_resources(prompt)
        return [c['resource_type'] for c in candidates 
                if c['confidence'] >= self.confidence_threshold]
    
    def build_resource_info(self, resource_types: list[str]) -> str:
        """Build structured resource info block for prompt injection."""
        # Resolve dependencies (depth=1 only β€” direct deps, no transitive)
        all_resources = set(resource_types)
        for rt in resource_types:
            for dep in self.kb.get_dependencies(rt):
                all_resources.add(dep)
        
        self._last_all_resources = all_resources
        
        lines = []
        idx = 1
        for rt in sorted(all_resources):
            schema = self.kb.get_resource_schema(rt)
            if not schema:
                continue
            
            required = [a["name"] for a in schema.get("required", [])]
            # Include type info for required args
            required_with_types = []
            for a in schema.get("required", []):
                atype = a.get("type", "string")
                required_with_types.append(f"{a['name']} ({atype})")
            
            nested = [n for n in schema.get("nested_blocks", {}).keys() 
                      if n != "timeouts"]
            
            # Mark which nested blocks are required (min_items > 0)
            required_blocks = []
            optional_blocks = []
            for bname, binfo in schema.get("nested_blocks", {}).items():
                if bname == "timeouts":
                    continue
                if binfo.get("min_items", 0) > 0:
                    required_blocks.append(bname)
                else:
                    optional_blocks.append(bname)
            
            is_dependency = rt not in resource_types
            prefix = f"{idx}. {rt}"
            if is_dependency:
                prefix += " (auto-added dependency)"
            lines.append(prefix)
            
            if required:
                lines.append(f"   Required arguments: {', '.join(required_with_types)}")
            if required_blocks:
                lines.append(f"   Required blocks: {', '.join(required_blocks)}")
            if optional_blocks:
                # Only show relevant optional blocks (max 5)
                shown = optional_blocks[:5]
                lines.append(f"   Optional blocks: {', '.join(shown)}")
            
            # Show nested block details (required args inside blocks + sub-blocks)
            for bname, binfo in schema.get("nested_blocks", {}).items():
                if bname == "timeouts":
                    continue
                block_required = [f"{a['name']} ({a.get('type', 'string')})" 
                                  for a in binfo.get("required", [])]
                block_optional = [a['name'] for a in binfo.get("optional", [])]
                # Check required sub-blocks (min_items > 0)
                required_sub = [sb for sb, si in binfo.get("nested_blocks", {}).items()
                                if si.get("min_items", 0) > 0]
                if block_required or block_optional or required_sub:
                    detail = f"   Block '{bname}':"
                    if block_required:
                        detail += f" required=[{', '.join(block_required)}]"
                    if required_sub:
                        detail += f" must contain: [{', '.join(required_sub)}]"
                    if block_optional:
                        detail += f" optional=[{', '.join(block_optional[:4])}]"
                    lines.append(detail)
            
            # Show key optional args for common resources (helps LLM pick correct args)
            key_optional = self._get_key_optional_args(rt, schema)
            if key_optional:
                lines.append(f"   Key optional args: {', '.join(key_optional)}")
            
            idx += 1
        
        # Dependencies
        dep_lines = []
        for rt in sorted(all_resources):
            deps = self.kb.get_dependencies(rt)
            deps_in_scope = [d for d in deps if d in all_resources]
            if deps_in_scope:
                for dep in deps_in_scope:
                    # Find the linking argument
                    link_arg = self._find_link_arg(rt, dep)
                    if link_arg:
                        dep_lines.append(f"   {rt}.{link_arg} β†’ {dep}")
                    else:
                        dep_lines.append(f"   {rt} β†’ depends on {dep}")
        
        if dep_lines:
            lines.append("")
            lines.append("Resource dependencies:")
            lines.extend(dep_lines)
        
        return "\n".join(lines)
    
    def _get_key_optional_args(self, resource_type: str, schema: dict) -> list[str]:
        """Return key optional arguments that LLMs commonly need for a resource."""
        # Curated map of important optional args per resource
        KEY_ARGS = {
            "aws_instance": ["ami", "instance_type", "subnet_id", "vpc_security_group_ids", "tags"],
            "aws_s3_bucket": ["bucket", "tags"],
            "aws_security_group": ["name", "description", "ingress", "egress", "tags"],
            "aws_vpc": ["cidr_block", "enable_dns_support", "enable_dns_hostnames", "tags"],
            "aws_subnet": ["vpc_id", "cidr_block", "availability_zone", "map_public_ip_on_launch", "tags"],
            "aws_db_instance": ["engine", "engine_version", "instance_class", "allocated_storage", "username", "password", "db_subnet_group_name", "skip_final_snapshot"],
            "aws_lambda_function": ["function_name", "runtime", "handler", "role", "filename", "source_code_hash"],
            "aws_iam_role": ["name", "assume_role_policy", "tags"],
            "aws_iam_policy": ["name", "policy", "description"],
            "aws_lb": ["name", "internal", "load_balancer_type", "subnets", "security_groups"],
            "aws_lb_target_group": ["name", "port", "protocol", "vpc_id", "target_type"],
            "aws_lb_listener": ["load_balancer_arn", "port", "protocol", "default_action"],
            "aws_ecs_cluster": ["name"],
            "aws_ecs_task_definition": ["family", "container_definitions", "network_mode", "requires_compatibilities", "cpu", "memory"],
            "aws_ecs_service": ["name", "cluster", "task_definition", "desired_count", "launch_type"],
            "aws_cloudwatch_log_group": ["name", "retention_in_days"],
            "aws_route53_zone": ["name"],
            "aws_route53_record": ["zone_id", "name", "type", "ttl", "records"],
            "aws_elastic_beanstalk_application": ["name", "description"],
            "aws_elastic_beanstalk_environment": ["name", "application", "solution_stack_name"],
            "aws_launch_template": ["name_prefix", "image_id", "instance_type"],
            "aws_autoscaling_group": ["min_size", "max_size", "desired_capacity", "vpc_zone_identifier"],
            "aws_sns_topic": ["name"],
            "aws_sqs_queue": ["name"],
            "aws_dynamodb_table": ["name", "billing_mode", "hash_key"],
            "aws_eip": ["domain"],
            "aws_nat_gateway": ["allocation_id", "subnet_id"],
            "aws_internet_gateway": ["vpc_id", "tags"],
            "aws_route_table": ["vpc_id", "tags"],
            "aws_cloudfront_distribution": ["enabled", "origin", "default_cache_behavior"],
            "aws_acm_certificate": ["domain_name", "validation_method"],
        }
        return KEY_ARGS.get(resource_type, [])
    
    def _find_link_arg(self, resource_type: str, dependency_type: str) -> str | None:
        """Guess the linking argument between resource and its dependency."""
        # Common patterns
        dep_short = dependency_type.replace("aws_", "")
        schema = self.kb.get_resource_schema(resource_type)
        if not schema:
            return None
        
        all_args = [a["name"] for a in schema.get("required", [])] + \
                   [a["name"] for a in schema.get("optional", [])]
        
        # Look for *_id patterns
        for arg in all_args:
            if dep_short.endswith(arg.replace("_id", "").replace("_arn", "")):
                return arg
            if arg in (f"{dep_short}_id", f"{dep_short}_arn", 
                       f"{dep_short.split('_')[-1]}_id"):
                return arg
        
        # Common specific mappings
        specific = {
            ("aws_route53_record", "aws_route53_zone"): "zone_id",
            ("aws_subnet", "aws_vpc"): "vpc_id",
            ("aws_security_group", "aws_vpc"): "vpc_id",
            ("aws_internet_gateway", "aws_vpc"): "vpc_id",
            ("aws_db_instance", "aws_db_subnet_group"): "db_subnet_group_name",
            ("aws_s3_bucket_versioning", "aws_s3_bucket"): "bucket",
            ("aws_s3_bucket_policy", "aws_s3_bucket"): "bucket",
            ("aws_s3_object", "aws_s3_bucket"): "bucket",
            ("aws_iam_instance_profile", "aws_iam_role"): "role",
            ("aws_iam_role_policy_attachment", "aws_iam_role"): "role",
            ("aws_iam_role_policy_attachment", "aws_iam_policy"): "policy_arn",
            ("aws_lambda_function", "aws_iam_role"): "role",
            ("aws_elastic_beanstalk_environment", "aws_elastic_beanstalk_application"): "application",
            ("aws_lb_listener", "aws_lb"): "load_balancer_arn",
            ("aws_lb_target_group", "aws_vpc"): "vpc_id",
            ("aws_nat_gateway", "aws_subnet"): "subnet_id",
            ("aws_nat_gateway", "aws_eip"): "allocation_id",
            ("aws_route_table_association", "aws_route_table"): "route_table_id",
            ("aws_route_table_association", "aws_subnet"): "subnet_id",
            ("aws_ecs_service", "aws_ecs_cluster"): "cluster",
            ("aws_ecs_service", "aws_ecs_task_definition"): "task_definition",
        }
        return specific.get((resource_type, dependency_type))
    
    def enhance_prompt(self, prompt: str, 
                       ground_truth_resources: list[str] = None) -> str:
        """
        Main entry point: enhance a raw prompt with TF-KB knowledge.
        
        Args:
            prompt: Raw user prompt
            ground_truth_resources: Optional list of resource types from dataset
            
        Returns:
            Enhanced prompt with resource schemas and dependencies
        """
        resource_types = self.extract_resource_types(prompt, ground_truth_resources)
        
        if not resource_types:
            # Can't extract any resources β€” return original prompt with generic guidance
            return (
                "Generate valid Terraform HCL code for AWS infrastructure. "
                "Use only real AWS provider resource types (e.g., aws_instance, aws_s3_bucket). "
                "Include all required arguments for each resource.\n\n"
                f"Task: {prompt}"
            )
        
        resource_info = self.build_resource_info(resource_types)
        
        # Add example snippets for detected resources
        from tf_examples import get_examples_for_resources
        examples = get_examples_for_resources(list(self._last_all_resources) if hasattr(self, '_last_all_resources') else resource_types)
        
        # Add composition templates for resource combinations
        from tf_compositions import get_composition_template
        compositions = get_composition_template(list(self._last_all_resources) if hasattr(self, '_last_all_resources') else resource_types)
        
        enhanced = (
            f"Generate Terraform HCL code for the following AWS infrastructure.\n"
            f"The resource schemas and examples below are for REFERENCE β€” use them as guidance "
            f"but include any additional resources needed for a complete, working configuration.\n\n"
            f"{resource_info}\n\n"
        )
        
        if compositions:
            enhanced += f"IMPORTANT β€” Follow this composition pattern for a complete setup:\n\n{compositions}\n\n"
        
        if examples:
            enhanced += f"Individual resource reference (for syntax only):\n\n{examples}\n\n"
        
        enhanced += (
            f"User request: {prompt}\n\n"
            f"Important:\n"
            f"- The schemas above are reference β€” freely add related resources (IAM roles, security groups, gateways, etc.) as needed\n"
            f"- Include all required arguments for each resource you create\n"
            f"- Connect resources using proper references (e.g., aws_vpc.main.id)\n"
            f"- Do NOT include provider or terraform blocks\n"
            f"- Use variables (var.xxx) for sensitive values like passwords, not hardcoded strings\n"
            f"- Include security best practices: encryption, security groups, least-privilege IAM\n"
            f"- Output ONLY valid HCL code, no explanations"
        )
        
        return enhanced


if __name__ == "__main__":
    pp = PromptPreprocessor()
    
    test_cases = [
        {
            "prompt": "Create an S3 bucket with versioning enabled",
            "resources": None  # NL extraction
        },
        {
            "prompt": "Create a Route53 zone and add a DNS A record pointing to an ELB",
            "resources": ["aws_route53_zone", "aws_route53_record", "aws_elb"]
        },
        {
            "prompt": "Deploy an Elastic Beanstalk application with IAM role",
            "resources": ["aws_elastic_beanstalk_application", "aws_elastic_beanstalk_environment",
                         "aws_iam_role", "aws_iam_instance_profile", "aws_iam_role_policy_attachment"]
        },
    ]
    
    for tc in test_cases:
        print("=" * 70)
        print(f"RAW PROMPT: {tc['prompt']}")
        print("-" * 70)
        enhanced = pp.enhance_prompt(tc['prompt'], tc.get('resources'))
        print(enhanced)
        print()