mcp-souschef 2.8.0__py3-none-any.whl → 3.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.
@@ -98,6 +98,7 @@ def generate_playbook_from_recipe_with_ai(
98
98
  max_tokens: int = 4000,
99
99
  project_id: str = "",
100
100
  base_url: str = "",
101
+ project_recommendations: dict | None = None,
101
102
  ) -> str:
102
103
  """
103
104
  Generate an AI-enhanced Ansible playbook from a Chef recipe.
@@ -116,6 +117,8 @@ def generate_playbook_from_recipe_with_ai(
116
117
  max_tokens: Maximum tokens to generate.
117
118
  project_id: Project ID for IBM Watsonx (required for watson provider).
118
119
  base_url: Custom base URL for the AI provider.
120
+ project_recommendations: Dictionary containing project-level analysis
121
+ and recommendations from cookbook assessment.
119
122
 
120
123
  Returns:
121
124
  AI-generated Ansible playbook in YAML format.
@@ -146,6 +149,7 @@ def generate_playbook_from_recipe_with_ai(
146
149
  max_tokens,
147
150
  project_id,
148
151
  base_url,
152
+ project_recommendations,
149
153
  )
150
154
 
151
155
  return ai_playbook
@@ -165,6 +169,7 @@ def _generate_playbook_with_ai(
165
169
  max_tokens: int,
166
170
  project_id: str = "",
167
171
  base_url: str = "",
172
+ project_recommendations: dict | None = None,
168
173
  ) -> str:
169
174
  """Generate Ansible playbook using AI for intelligent conversion."""
170
175
  try:
@@ -174,7 +179,9 @@ def _generate_playbook_with_ai(
174
179
  return client
175
180
 
176
181
  # Create the AI prompt
177
- prompt = _create_ai_conversion_prompt(raw_content, parsed_content, recipe_name)
182
+ prompt = _create_ai_conversion_prompt(
183
+ raw_content, parsed_content, recipe_name, project_recommendations
184
+ )
178
185
 
179
186
  # Call the AI API and get response
180
187
  ai_response = _call_ai_api(
@@ -333,96 +340,257 @@ def _call_ai_api(
333
340
 
334
341
 
335
342
  def _create_ai_conversion_prompt(
336
- raw_content: str, parsed_content: str, recipe_name: str
343
+ raw_content: str,
344
+ parsed_content: str,
345
+ recipe_name: str,
346
+ project_recommendations: dict | None = None,
337
347
  ) -> str:
338
348
  """Create a comprehensive prompt for AI conversion."""
339
- return f"""You are an expert at converting Chef recipes to Ansible playbooks.
340
- Your task is to convert the following Chef recipe into a high-quality,
341
- production-ready Ansible playbook.
342
-
343
- CHEF RECIPE CONTENT:
344
- {raw_content}
345
-
346
- PARSED RECIPE ANALYSIS:
347
- {parsed_content}
348
-
349
- RECIPE NAME: {recipe_name}
350
-
351
- CONVERSION REQUIREMENTS:
352
-
353
- 1. **Understand the Intent**: Analyze what this Chef recipe is trying to
354
- accomplish. Look at the resources, their properties, and the overall
355
- workflow.
356
-
357
- 2. **Best Practices**: Generate Ansible code that follows Ansible best
358
- practices:
359
- - Use appropriate modules (ansible.builtin.* when possible)
360
- - Include proper error handling and idempotency
361
- - Use meaningful variable names
362
- - Include comments explaining complex logic
363
- - Handle edge cases and failure scenarios
364
-
365
- 3. **Resource Mapping**: Convert Chef resources to appropriate Ansible
366
- modules:
367
- - package → ansible.builtin.package or specific package managers
368
- - service → ansible.builtin.service
369
- - file/directory → ansible.builtin.file
370
- - template → ansible.builtin.template
371
- - execute → ansible.builtin.command/shell
372
- - user/group → ansible.builtin.user/group
373
- - mount → ansible.builtin.mount
374
-
375
- 4. **Variables and Facts**: Convert Chef node attributes to Ansible
376
- variables/facts appropriately.
377
-
378
- 5. **Conditionals**: Convert Chef guards (only_if/not_if) to Ansible when
379
- conditions.
380
-
381
- 6. **Notifications**: Convert Chef notifications to Ansible handlers where
382
- appropriate.
383
-
384
- 7. **Idempotency**: Ensure the playbook is idempotent and can be run
385
- multiple times safely.
386
-
387
- 8. **Error Handling**: Include proper error handling and rollback
388
- considerations.
389
-
390
- 9. **Task Ordering**: CRITICAL: Ensure tasks are ordered logically.
391
- - Install packages BEFORE configuring them.
392
- - create users/groups BEFORE using them in file permissions.
393
- - Place configuration files BEFORE starting/restarting services.
394
- - Ensure directories exist BEFORE creating files in them.
395
-
396
- 10. **Handlers**: Verify that all notified handlers are actually defined
397
- in the handlers section.
398
-
399
- OUTPUT FORMAT:
400
- Return ONLY a valid YAML Ansible playbook. Do not include any explanation,
401
- markdown formatting, or code blocks. The output should be pure YAML that can
402
- be directly used as an Ansible playbook.
403
-
404
- The playbook should include:
405
- - A proper name
406
- - Appropriate hosts (default to 'all')
407
- - Variables section if needed
408
- - Tasks section with all converted resources
409
- - Handlers section if notifications are used
410
- - Any necessary pre_tasks or post_tasks
411
-
412
- Example structure:
413
- ---
414
- - name: Convert of {recipe_name}
415
- hosts: all
416
- become: true
417
- vars:
418
- # Variables here
419
- tasks:
420
- # Tasks here
421
- handlers:
422
- # Handlers here
423
-
424
- Focus on creating a functional, well-structured Ansible playbook that achieves
425
- the same outcome as the Chef recipe."""
349
+ prompt_parts = _build_base_prompt_parts(raw_content, parsed_content, recipe_name)
350
+
351
+ # Add project context if available
352
+ if project_recommendations:
353
+ prompt_parts.extend(
354
+ _build_project_context_parts(project_recommendations, recipe_name)
355
+ )
356
+
357
+ prompt_parts.extend(_build_conversion_requirements_parts())
358
+
359
+ # Add project-specific guidance if available
360
+ if project_recommendations:
361
+ prompt_parts.extend(_build_project_guidance_parts(project_recommendations))
362
+
363
+ prompt_parts.extend(_build_output_format_parts())
364
+
365
+ return "\n".join(prompt_parts)
366
+
367
+
368
+ def _build_base_prompt_parts(
369
+ raw_content: str, parsed_content: str, recipe_name: str
370
+ ) -> list[str]:
371
+ """Build the base prompt parts."""
372
+ return [
373
+ "You are an expert at converting Chef recipes to Ansible playbooks.",
374
+ "Your task is to convert the following Chef recipe into a high-quality,",
375
+ "production-ready Ansible playbook.",
376
+ "",
377
+ "CHEF RECIPE CONTENT:",
378
+ raw_content,
379
+ "",
380
+ "PARSED RECIPE ANALYSIS:",
381
+ parsed_content,
382
+ "",
383
+ f"RECIPE NAME: {recipe_name}",
384
+ ]
385
+
386
+
387
+ def _build_project_context_parts(
388
+ project_recommendations: dict, recipe_name: str
389
+ ) -> list[str]:
390
+ """Build project context parts."""
391
+ # Extract values to shorten f-strings
392
+ complexity = project_recommendations.get("project_complexity", "Unknown")
393
+ strategy = project_recommendations.get("migration_strategy", "Unknown")
394
+ effort_days = project_recommendations.get("project_effort_days", 0)
395
+ density = project_recommendations.get("dependency_density", 0)
396
+
397
+ parts = [
398
+ "",
399
+ "PROJECT CONTEXT:",
400
+ f"Project Complexity: {complexity}",
401
+ f"Migration Strategy: {strategy}",
402
+ f"Total Project Effort: {effort_days:.1f} days",
403
+ f"Dependency Density: {density:.2f}",
404
+ ]
405
+
406
+ # Add migration recommendations
407
+ recommendations = project_recommendations.get("recommendations", [])
408
+ if recommendations:
409
+ parts.extend(
410
+ [
411
+ "",
412
+ "PROJECT MIGRATION RECOMMENDATIONS:",
413
+ ]
414
+ )
415
+ for rec in recommendations[:5]: # Limit to first 5 recommendations
416
+ parts.append(f"- {rec}")
417
+
418
+ # Add dependency information
419
+ migration_order = project_recommendations.get("migration_order", [])
420
+ if migration_order:
421
+ recipe_position = _find_recipe_position_in_migration_order(
422
+ migration_order, recipe_name
423
+ )
424
+ if recipe_position:
425
+ dependencies = ", ".join(recipe_position.get("dependencies", [])) or "None"
426
+ parts.extend(
427
+ [
428
+ "",
429
+ "MIGRATION CONTEXT FOR THIS RECIPE:",
430
+ f"Phase: {recipe_position.get('phase', 'Unknown')}",
431
+ f"Complexity: {recipe_position.get('complexity', 'Unknown')}",
432
+ f"Dependencies: {dependencies}",
433
+ f"Migration Reason: {recipe_position.get('reason', 'Unknown')}",
434
+ ]
435
+ )
436
+
437
+ return parts
438
+
439
+
440
+ def _find_recipe_position_in_migration_order(
441
+ migration_order: list[dict[str, Any]], recipe_name: str
442
+ ) -> dict[str, Any] | None:
443
+ """Find this recipe's position in migration order."""
444
+ for item in migration_order:
445
+ cookbook_name = recipe_name.replace(".rb", "").replace("recipes/", "")
446
+ if item.get("cookbook") == cookbook_name:
447
+ return item
448
+ return None
449
+
450
+
451
+ def _build_conversion_requirements_parts() -> list[str]:
452
+ """Build conversion requirements parts."""
453
+ return [
454
+ "",
455
+ "CONVERSION REQUIREMENTS:",
456
+ "",
457
+ "1. **Understand the Intent**: Analyze what this Chef recipe is trying to",
458
+ " accomplish. Look at the resources, their properties, and the overall",
459
+ " workflow.",
460
+ "",
461
+ "2. **Best Practices**: Generate Ansible code that follows Ansible best",
462
+ " practices:",
463
+ " - Use appropriate modules (ansible.builtin.* when possible)",
464
+ " - Include proper error handling and idempotency",
465
+ " - Use meaningful variable names",
466
+ " - Include comments explaining complex logic",
467
+ " - Handle edge cases and failure scenarios",
468
+ "",
469
+ "3. **Resource Mapping**: Convert Chef resources to appropriate Ansible",
470
+ " modules:",
471
+ " - package → ansible.builtin.package or specific package managers",
472
+ " - service → ansible.builtin.service",
473
+ " - file/directory → ansible.builtin.file",
474
+ " - template → ansible.builtin.template (CHANGE .erb to .j2 extension)",
475
+ " - execute → ansible.builtin.command/shell",
476
+ " - user/group → ansible.builtin.user/group",
477
+ " - mount → ansible.builtin.mount",
478
+ " - include_recipe → ansible.builtin.include_role (for unknown cookbooks)",
479
+ " - include_recipe → specific role imports (for known cookbooks)",
480
+ "",
481
+ "4. **Template Conversions**: CRITICAL for template resources:",
482
+ " - Change file extension from .erb to .j2 in the 'src' parameter",
483
+ " - Example: 'config.erb' becomes 'config.j2'",
484
+ " - Add note: Templates need manual ERB→Jinja2 conversion",
485
+ " - ERB syntax: <%= variable %> → Jinja2: {{ variable }}",
486
+ " - ERB blocks: <% code %> → Jinja2: {% code %}",
487
+ "",
488
+ "5. **Chef Data Bags to Ansible Vault**: Convert data bag lookups:",
489
+ " - Chef::EncryptedDataBagItem.load('bag', 'item') →",
490
+ " - Ansible: Use group_vars/ or host_vars/ with ansible-vault",
491
+ " - Store sensitive data in encrypted YAML files, not inline lookups",
492
+ " - Example: Define ssh_key in group_vars/all/vault.yml (encrypted)",
493
+ "",
494
+ "6. **Variables and Facts**: Convert Chef node attributes to Ansible",
495
+ " variables/facts appropriately:",
496
+ " - node['attribute'] → {{ ansible_facts.attribute }} or vars",
497
+ " - node['platform'] → {{ ansible_distribution }}",
498
+ " - node['platform_version'] → {{ ansible_distribution_version }}",
499
+ "",
500
+ "7. **Conditionals**: Convert Chef guards (only_if/not_if) to Ansible when",
501
+ " conditions.",
502
+ "",
503
+ "8. **Notifications**: Convert Chef notifications to Ansible handlers",
504
+ " where appropriate.",
505
+ "",
506
+ "9. **Idempotency**: Ensure the playbook is idempotent and can be run",
507
+ " multiple times safely.",
508
+ "",
509
+ "10. **Error Handling**: Include proper error handling and rollback",
510
+ " considerations.",
511
+ "",
512
+ "11. **Task Ordering**: CRITICAL: Ensure tasks are ordered logically.",
513
+ " - Install packages BEFORE configuring them.",
514
+ " - create users/groups BEFORE using them in file permissions.",
515
+ " - Place configuration files BEFORE starting/restarting services.",
516
+ " - Ensure directories exist BEFORE creating files in them.",
517
+ "",
518
+ "12. **Handlers**: Verify that all notified handlers are actually defined",
519
+ " in the handlers section.",
520
+ ]
521
+
522
+
523
+ def _build_project_guidance_parts(project_recommendations: dict) -> list[str]:
524
+ """Build project-specific guidance parts."""
525
+ strategy = project_recommendations.get("migration_strategy", "").lower()
526
+ parts = []
527
+
528
+ if "parallel" in strategy:
529
+ parallel_tracks = project_recommendations.get("parallel_tracks", 2)
530
+ parts.extend(
531
+ [
532
+ "",
533
+ "11. **Parallel Migration Context**: This recipe is part of a",
534
+ f" parallel migration with {parallel_tracks} tracks.",
535
+ " Ensure this playbook can run independently without",
536
+ " dependencies on other cookbooks in the project.",
537
+ ]
538
+ )
539
+ elif "phased" in strategy:
540
+ parts.extend(
541
+ [
542
+ "",
543
+ "11. **Phased Migration Context**: This recipe is part of a phased",
544
+ " migration approach. Consider dependencies and ensure proper",
545
+ " ordering within the broader project migration plan.",
546
+ ]
547
+ )
548
+
549
+ return parts
550
+
551
+
552
+ def _build_output_format_parts() -> list[str]:
553
+ """Build output format parts."""
554
+ return [
555
+ "",
556
+ "OUTPUT FORMAT:",
557
+ "Return ONLY a valid YAML Ansible playbook. Do not include any",
558
+ " explanation, markdown formatting, or code blocks. The output should",
559
+ " be pure YAML that can be directly used as an Ansible playbook.",
560
+ "",
561
+ "CRITICAL YAML SYNTAX RULES:",
562
+ "- Use block mapping style (one key per line) NOT flow mapping style",
563
+ "- NEVER use { } for mappings with conditionals or complex expressions",
564
+ "- Correct: Use multi-line format:",
565
+ " - name: Include role conditionally",
566
+ " ansible.builtin.import_role:",
567
+ " name: my_role",
568
+ " when: condition_here",
569
+ "- WRONG: { role: my_role, when: condition } # This is INVALID",
570
+ "",
571
+ "The playbook should include:",
572
+ "- A proper name",
573
+ "- Appropriate hosts (default to 'all')",
574
+ "- Variables section if needed",
575
+ "- Tasks section with all converted resources",
576
+ "- Handlers section if notifications are used",
577
+ "- Any necessary pre_tasks or post_tasks",
578
+ "",
579
+ "Example structure:",
580
+ "---",
581
+ "- name: Convert of <recipe_name>",
582
+ " hosts: all",
583
+ " become: true",
584
+ " vars:",
585
+ " # Variables here",
586
+ " tasks:",
587
+ " # Tasks here",
588
+ " handlers:",
589
+ " # Handlers here",
590
+ "",
591
+ "Focus on creating a functional, well-structured Ansible playbook that",
592
+ "achieves the same outcome as the Chef recipe.",
593
+ ]
426
594
 
427
595
 
428
596
  def _clean_ai_playbook_response(ai_response: str) -> str:
@@ -507,6 +675,7 @@ def _run_ansible_lint(playbook_content: str) -> str | None:
507
675
  if shutil.which("ansible-lint") is None:
508
676
  return None
509
677
 
678
+ tmp_path = None
510
679
  try:
511
680
  with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False) as tmp:
512
681
  tmp.write(playbook_content)
@@ -528,7 +697,7 @@ def _run_ansible_lint(playbook_content: str) -> str | None:
528
697
  except Exception:
529
698
  return None
530
699
  finally:
531
- if "tmp_path" in locals() and Path(tmp_path).exists():
700
+ if tmp_path is not None and Path(tmp_path).exists():
532
701
  Path(tmp_path).unlink()
533
702
 
534
703
 
@@ -30,20 +30,14 @@ def _parse_properties(properties_str: str) -> dict[str, Any]:
30
30
  if not properties_str:
31
31
  return {}
32
32
  try:
33
- # Try ast.literal_eval first for safety
33
+ # Use ast.literal_eval for safe parsing of Python literals
34
34
  result = ast.literal_eval(properties_str)
35
35
  if isinstance(result, dict):
36
36
  return result
37
37
  return {}
38
38
  except (ValueError, SyntaxError):
39
- # Fallback to eval if needed, but this is less safe
40
- try:
41
- result = eval(properties_str) # noqa: S307
42
- if isinstance(result, dict):
43
- return result
44
- return {}
45
- except Exception:
46
- return {}
39
+ # If parsing fails, return empty dict rather than using unsafe eval
40
+ return {}
47
41
 
48
42
 
49
43
  def _normalize_template_value(value: Any) -> Any:
@@ -245,13 +239,17 @@ def _get_include_recipe_params(
245
239
  Build parameters for include_recipe resources.
246
240
 
247
241
  Uses cookbook-specific configurations when available.
242
+ Falls back to include_role for unknown cookbooks.
248
243
  """
249
244
  cookbook_config = get_cookbook_package_config(resource_name)
250
245
  if cookbook_config:
251
246
  # Return a copy to prevent callers from mutating the shared mapping.
252
247
  return dict(cookbook_config["params"])
253
- # Default behavior for recipes without a specific mapping.
254
- return {"name": resource_name, "state": "present"}
248
+
249
+ # For unknown cookbooks, use include_role with the cookbook name
250
+ # Extract cookbook name from "cookbook::recipe" format
251
+ cookbook_name = resource_name.split("::")[0]
252
+ return {"name": cookbook_name}
255
253
 
256
254
 
257
255
  def _get_default_params(resource_name: str, action: str) -> dict[str, Any]:
@@ -303,6 +301,9 @@ def _convert_chef_resource_to_ansible(
303
301
  cookbook_config = get_cookbook_package_config(resource_name)
304
302
  if cookbook_config:
305
303
  ansible_module = cookbook_config["module"]
304
+ else:
305
+ # For include_recipe without specific mapping, use include_role
306
+ ansible_module = "ansible.builtin.include_role"
306
307
 
307
308
  # Handle unknown resource types
308
309
  if ansible_module is None:
@@ -0,0 +1,177 @@
1
+ """Chef ERB template to Jinja2 converter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from souschef.parsers.template import (
6
+ _convert_erb_to_jinja2,
7
+ _extract_template_variables,
8
+ )
9
+
10
+
11
+ def convert_template_file(erb_path: str) -> dict:
12
+ """
13
+ Convert an ERB template file to Jinja2 format.
14
+
15
+ Args:
16
+ erb_path: Path to the ERB template file.
17
+
18
+ Returns:
19
+ Dictionary containing:
20
+ - success: bool, whether conversion succeeded
21
+ - original_file: str, path to original ERB file
22
+ - jinja2_file: str, suggested path for .j2 file
23
+ - jinja2_content: str, converted Jinja2 template content
24
+ - variables: list, variables found in template
25
+ - error: str (optional), error message if conversion failed
26
+
27
+ """
28
+ try:
29
+ file_path = Path(erb_path).resolve()
30
+
31
+ if not file_path.exists():
32
+ return {
33
+ "success": False,
34
+ "error": f"File not found: {erb_path}",
35
+ "original_file": erb_path,
36
+ }
37
+
38
+ if not file_path.is_file():
39
+ return {
40
+ "success": False,
41
+ "error": f"Path is not a file: {erb_path}",
42
+ "original_file": erb_path,
43
+ }
44
+
45
+ # Read ERB template
46
+ try:
47
+ content = file_path.read_text(encoding="utf-8")
48
+ except UnicodeDecodeError:
49
+ return {
50
+ "success": False,
51
+ "error": f"Unable to decode {erb_path} as UTF-8 text",
52
+ "original_file": str(file_path),
53
+ }
54
+
55
+ # Extract variables
56
+ variables = _extract_template_variables(content)
57
+
58
+ # Convert ERB to Jinja2
59
+ jinja2_content = _convert_erb_to_jinja2(content)
60
+
61
+ # Determine output file name
62
+ jinja2_file = str(file_path).replace(".erb", ".j2")
63
+
64
+ return {
65
+ "success": True,
66
+ "original_file": str(file_path),
67
+ "jinja2_file": jinja2_file,
68
+ "jinja2_content": jinja2_content,
69
+ "variables": sorted(variables),
70
+ }
71
+
72
+ except Exception as e:
73
+ return {
74
+ "success": False,
75
+ "error": f"Conversion failed: {e}",
76
+ "original_file": erb_path,
77
+ }
78
+
79
+
80
+ def convert_cookbook_templates(cookbook_path: str) -> dict:
81
+ """
82
+ Convert all ERB templates in a cookbook to Jinja2.
83
+
84
+ Args:
85
+ cookbook_path: Path to the cookbook directory.
86
+
87
+ Returns:
88
+ Dictionary containing:
89
+ - success: bool, whether all conversions succeeded
90
+ - templates_converted: int, number of templates successfully converted
91
+ - templates_failed: int, number of templates that failed conversion
92
+ - results: list of dict, individual template conversion results
93
+ - error: str (optional), error message if cookbook not found
94
+
95
+ """
96
+ try:
97
+ cookbook_dir = Path(cookbook_path).resolve()
98
+
99
+ if not cookbook_dir.exists():
100
+ return {
101
+ "success": False,
102
+ "error": f"Cookbook directory not found: {cookbook_path}",
103
+ "templates_converted": 0,
104
+ "templates_failed": 0,
105
+ "results": [],
106
+ }
107
+
108
+ # Find all .erb files in the cookbook
109
+ erb_files = list(cookbook_dir.glob("**/*.erb"))
110
+
111
+ if not erb_files:
112
+ return {
113
+ "success": True,
114
+ "templates_converted": 0,
115
+ "templates_failed": 0,
116
+ "results": [],
117
+ "message": "No ERB templates found in cookbook",
118
+ }
119
+
120
+ results = []
121
+ templates_converted = 0
122
+ templates_failed = 0
123
+
124
+ for erb_file in erb_files:
125
+ result = convert_template_file(str(erb_file))
126
+ results.append(result)
127
+
128
+ if result["success"]:
129
+ templates_converted += 1
130
+ else:
131
+ templates_failed += 1
132
+
133
+ return {
134
+ "success": templates_failed == 0,
135
+ "templates_converted": templates_converted,
136
+ "templates_failed": templates_failed,
137
+ "results": results,
138
+ }
139
+
140
+ except Exception as e:
141
+ return {
142
+ "success": False,
143
+ "error": f"Failed to convert cookbook templates: {e}",
144
+ "templates_converted": 0,
145
+ "templates_failed": 0,
146
+ "results": [],
147
+ }
148
+
149
+
150
+ def convert_template_with_ai(erb_path: str, ai_service=None) -> dict:
151
+ """
152
+ Convert an ERB template to Jinja2 using AI assistance for complex conversions.
153
+
154
+ This function first attempts rule-based conversion, then optionally uses AI
155
+ for validation or complex Ruby logic that can't be automatically converted.
156
+
157
+ Args:
158
+ erb_path: Path to the ERB template file.
159
+ ai_service: Optional AI service instance for complex conversions.
160
+
161
+ Returns:
162
+ Dictionary with conversion results (same format as convert_template_file).
163
+
164
+ """
165
+ # Start with rule-based conversion
166
+ result = convert_template_file(erb_path)
167
+
168
+ # Add conversion method metadata
169
+ result["conversion_method"] = "rule-based"
170
+
171
+ # Future enhancement: Use AI service to validate/improve complex conversions
172
+ if ai_service is not None:
173
+ # AI validation/improvement logic deferred to future enhancement
174
+ # when AI integration becomes more critical to the template conversion process
175
+ pass
176
+
177
+ return result