healing-agent 0.2.4__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.
@@ -0,0 +1,15 @@
1
+ from .healing_agent import healing_agent as _healing_agent
2
+
3
+ # Make the module callable by implementing __call__
4
+ class HealingAgentModule:
5
+ def __init__(self):
6
+ self.healing_agent = _healing_agent
7
+
8
+ def __call__(self, *args, **kwargs):
9
+ return self.healing_agent(*args, **kwargs)
10
+
11
+ # Replace the module with our callable instance
12
+ import sys
13
+ sys.modules[__name__] = HealingAgentModule()
14
+
15
+ __all__ = ['healing_agent']
@@ -0,0 +1,19 @@
1
+ # Experimental and not used.
2
+
3
+ import os
4
+
5
+ def get_environment_variables() -> dict:
6
+ """
7
+ Get all environment variables and return them as a dictionary.
8
+
9
+ Returns:
10
+ dict: Dictionary containing all environment variables as key-value pairs
11
+ """
12
+ env_vars = {}
13
+ for key, value in os.environ.items():
14
+ env_vars[key] = value
15
+ return env_vars
16
+
17
+ if __name__ == "__main__":
18
+ print(get_environment_variables())
19
+
@@ -0,0 +1,80 @@
1
+ # Experimental and not used.
2
+
3
+ import json
4
+ from typing import Any, Dict
5
+ from ..ai_code_fixer import get_ai_response
6
+
7
+ def fix_json_ai(context: Dict[str, Any], config: Dict[str, Any]) -> Any:
8
+ """
9
+ Attempt to fix JSON data using AI.
10
+ """
11
+ try:
12
+ json_data = context['error']['json_details']['response_text']
13
+
14
+ # Extract the first 1000 characters of the JSON data
15
+ json_snippet = json_data[:1000]
16
+
17
+ # Prepare the prompt for AI
18
+ prompt = f"""
19
+ Fix the following JSON data:
20
+
21
+ {json_snippet}
22
+
23
+ Return only the fixed JSON data without any explanations or markdown formatting.
24
+ """
25
+
26
+ # Get the fix from AI
27
+ fixed_json_str = get_ai_response(prompt, config)
28
+
29
+ # Parse the fixed JSON string
30
+ fixed_data = json.loads(fixed_json_str)
31
+
32
+ return fixed_data
33
+ except Exception as e:
34
+ print(f"♣ JSON fixing with AI failed: {str(e)}")
35
+ return None
36
+
37
+ def fix_json_lint(context: Dict[str, Any], config: Dict[str, Any]) -> Any:
38
+ """
39
+ Attempt to fix JSON data using a JSON linter.
40
+ """
41
+ try:
42
+ json_data = context['error']['json_details']['response_text']
43
+ # Use a JSON linter library to identify and fix syntax errors
44
+ # Example using jsonlint:
45
+ # fixed_data = jsonlint.parse(json_data)
46
+ # return fixed_data
47
+ pass
48
+ except Exception as e:
49
+ print(f"♣ JSON linting failed: {str(e)}")
50
+ return None
51
+
52
+ def fix_json_fallback(context: Dict[str, Any], config: Dict[str, Any]) -> Any:
53
+ """
54
+ Attempt to fix JSON data using fallback parsing options.
55
+ """
56
+ try:
57
+ json_data = context['error']['json_details']['response_text']
58
+ # Use fallback parsing options, e.g., allow trailing commas
59
+ fixed_data = json.loads(json_data, strict=False)
60
+ return fixed_data
61
+ except json.JSONDecodeError as e:
62
+ print(f"♣ Fallback JSON parsing failed: {str(e)}")
63
+ return None
64
+
65
+ def fix_json(context: Dict[str, Any], config: Dict[str, Any]) -> Any:
66
+ """
67
+ Attempt to fix JSON data using the selected strategy.
68
+ """
69
+ strategy = config.get('JSON_FIX_STRATEGY', 'ai')
70
+
71
+ if strategy == 'ai':
72
+ return fix_json_ai(context, config)
73
+ elif strategy == 'lint':
74
+ return fix_json_lint(context, config)
75
+ elif strategy == 'fallback':
76
+ return fix_json_fallback(context, config)
77
+ else:
78
+ print(f"♣ Unknown JSON fix strategy: {strategy}")
79
+ return None
80
+
@@ -0,0 +1,116 @@
1
+ # Experimental and not used.
2
+
3
+ import os
4
+ import ast
5
+ import json
6
+ from typing import Dict, List, Optional, Union
7
+
8
+ def get_function_details(file_path: str) -> List[Dict[str, str]]:
9
+ """
10
+ Extract function names and docstrings from a Python file.
11
+
12
+ Args:
13
+ file_path: Path to the Python file
14
+
15
+ Returns:
16
+ List of dictionaries containing function names and their docstrings
17
+ """
18
+ functions = []
19
+ try:
20
+ with open(file_path, 'r', encoding='utf-8') as f:
21
+ tree = ast.parse(f.read())
22
+
23
+ for node in ast.walk(tree):
24
+ if isinstance(node, ast.FunctionDef):
25
+ docstring = ast.get_docstring(node)
26
+ functions.append({
27
+ "name": node.name,
28
+ "docstring": docstring if docstring else "No description available"
29
+ })
30
+ except Exception as e:
31
+ print(f"Error parsing {file_path}: {str(e)}")
32
+ return []
33
+
34
+ return functions
35
+
36
+ def list_directory_contents(
37
+ directory: str,
38
+ recursive: bool = False,
39
+ include_functions: bool = False
40
+ ) -> Dict[str, Union[List[str], Dict[str, List[Dict[str, str]]]]]:
41
+ """
42
+ List all files in a directory with optional recursive search and Python function details.
43
+
44
+ Args:
45
+ directory: Path to the directory to scan
46
+ recursive: Whether to scan subdirectories recursively
47
+ include_functions: Whether to extract function details from Python files
48
+
49
+ Returns:
50
+ Dictionary containing:
51
+ - files: List of all files
52
+ - python_files: Dictionary of Python files and their function details (if include_functions=True)
53
+ """
54
+
55
+ if not os.path.exists(directory):
56
+ raise ValueError(f"Directory {directory} does not exist")
57
+
58
+ result = {
59
+ "files": [],
60
+ "python_files": {}
61
+ }
62
+
63
+ def scan_directory(path: str):
64
+ for entry in os.scandir(path):
65
+ if entry.is_file():
66
+ file_path = os.path.relpath(entry.path, directory)
67
+ result["files"].append(file_path)
68
+
69
+ if include_functions and file_path.endswith('.py'):
70
+ functions = get_function_details(entry.path)
71
+ if functions:
72
+ result["python_files"][file_path] = functions
73
+
74
+ elif entry.is_dir() and recursive:
75
+ scan_directory(entry.path)
76
+
77
+ scan_directory(directory)
78
+
79
+ # Sort files for consistent output
80
+ result["files"].sort()
81
+
82
+ return result
83
+
84
+ def get_directory_info(
85
+ directory: str = ".",
86
+ recursive: bool = False,
87
+ include_functions: bool = False,
88
+ pretty_print: bool = False
89
+ ) -> str:
90
+ """
91
+ Get directory information in JSON format.
92
+
93
+ Args:
94
+ directory: Path to the directory to scan (defaults to current directory)
95
+ recursive: Whether to scan subdirectories recursively
96
+ include_functions: Whether to extract function details from Python files
97
+ pretty_print: Whether to format the JSON output with indentation
98
+
99
+ Returns:
100
+ JSON string containing directory information
101
+ """
102
+ try:
103
+ contents = list_directory_contents(
104
+ directory,
105
+ recursive=recursive,
106
+ include_functions=include_functions
107
+ )
108
+
109
+ return json.dumps(contents, indent=2 if pretty_print else None)
110
+
111
+ except Exception as e:
112
+ error_response = {
113
+ "error": str(e),
114
+ "directory": directory
115
+ }
116
+ return json.dumps(error_response, indent=2 if pretty_print else None)
@@ -0,0 +1,202 @@
1
+ from typing import Dict
2
+ import time
3
+ import httpx
4
+ import requests
5
+ import openai
6
+ from functools import wraps
7
+
8
+ def handle_connection_errors(provider_name: str):
9
+ """Simple decorator to handle connection errors with basic logging"""
10
+ def decorator(func):
11
+ @wraps(func)
12
+ def wrapper(*args, **kwargs):
13
+ try:
14
+ return func(*args, **kwargs)
15
+ except (
16
+ httpx.ConnectError,
17
+ requests.exceptions.ConnectionError,
18
+ requests.exceptions.Timeout,
19
+ ConnectionError,
20
+ TimeoutError
21
+ ) as e:
22
+ print(f"♣ Connection error in {provider_name}: {str(e)}")
23
+ # Wait briefly before retrying
24
+ time.sleep(2)
25
+ try:
26
+ return func(*args, **kwargs)
27
+ except Exception as retry_error:
28
+ print(f"♣ Retry failed for {provider_name}: {str(retry_error)}")
29
+ raise
30
+ except openai.APIConnectionError as e:
31
+ if 'OpenAI' in provider_name or 'Azure' in provider_name:
32
+ print(f"♣ Connection error in {provider_name}: {str(e)}")
33
+ # Wait briefly before retrying
34
+ time.sleep(2)
35
+ try:
36
+ return func(*args, **kwargs)
37
+ except Exception as retry_error:
38
+ print(f"♣ Retry failed for {provider_name}: {str(retry_error)}")
39
+ raise
40
+ else:
41
+ raise
42
+ except Exception as e:
43
+ print(f"♣ Unexpected error in {provider_name}: {str(e)}")
44
+ raise
45
+ return wrapper
46
+ return decorator
47
+
48
+ @handle_connection_errors("Azure")
49
+ def _get_azure_response(prompt: str, config: Dict, system_prompt: str) -> str:
50
+ """Handle Azure OpenAI API requests"""
51
+ import openai
52
+ client = openai.AzureOpenAI(
53
+ api_key=config['api_key'],
54
+ api_version=config['api_version'],
55
+ azure_endpoint=config['endpoint']
56
+ )
57
+
58
+ try:
59
+ response = client.chat.completions.create(
60
+ model=config['deployment_name'],
61
+ messages=[
62
+ {"role": "system", "content": system_prompt},
63
+ {"role": "user", "content": prompt}
64
+ ],
65
+ timeout=config.get('timeout', 30)
66
+ )
67
+ return response.choices[0].message.content.strip()
68
+ except openai.APIError as e:
69
+ print(f"♣ Azure API error: {str(e)}")
70
+ raise
71
+
72
+ @handle_connection_errors("OpenAI")
73
+ def _get_openai_response(prompt: str, config: Dict, system_prompt: str) -> str:
74
+ """Handle OpenAI direct API requests"""
75
+ import openai
76
+ client = openai.OpenAI(
77
+ api_key=config['api_key'],
78
+ organization=config.get('organization_id')
79
+ )
80
+
81
+ try:
82
+ response = client.chat.completions.create(
83
+ model=config['model'],
84
+ messages=[
85
+ {"role": "system", "content": system_prompt},
86
+ {"role": "user", "content": prompt}
87
+ ],
88
+ timeout=config.get('timeout', 30)
89
+ )
90
+ return response.choices[0].message.content.strip()
91
+ except openai.APIError as e:
92
+ print(f"♣ OpenAI API error: {str(e)}")
93
+ raise
94
+
95
+ @handle_connection_errors("Anthropic")
96
+ def _get_anthropic_response(prompt: str, config: Dict) -> str:
97
+ """Handle Anthropic API requests"""
98
+ import anthropic
99
+ client = anthropic.Anthropic(api_key=config['api_key'])
100
+
101
+ try:
102
+ response = client.messages.create(
103
+ model=config['model'],
104
+ max_tokens=1000,
105
+ messages=[{
106
+ "role": "user",
107
+ "content": prompt
108
+ }],
109
+ timeout=config.get('timeout', 30)
110
+ )
111
+ return response.content[0].text
112
+ except Exception as e:
113
+ print(f"♣ Anthropic API error: {str(e)}")
114
+ raise
115
+
116
+ @handle_connection_errors("Ollama")
117
+ def _get_ollama_response(prompt: str, config: Dict) -> str:
118
+ """Handle Ollama API requests"""
119
+ try:
120
+ response = requests.post(
121
+ f"{config['host']}/api/generate",
122
+ json={
123
+ "model": config['model'],
124
+ "prompt": prompt,
125
+ "stream": False
126
+ },
127
+ timeout=config.get('timeout', 120)
128
+ )
129
+ response.raise_for_status()
130
+ return response.json()['response']
131
+ except requests.exceptions.RequestException as e:
132
+ print(f"♣ Ollama API error: {str(e)}")
133
+ raise
134
+
135
+ @handle_connection_errors("LiteLLM")
136
+ def _get_litellm_response(prompt: str, config: Dict, system_prompt: str) -> str:
137
+ """Handle LiteLLM API requests"""
138
+ import litellm
139
+ if config.get('api_base'):
140
+ litellm.api_base = config['api_base']
141
+
142
+ try:
143
+ response = litellm.completion(
144
+ model=config['model'],
145
+ messages=[
146
+ {"role": "system", "content": system_prompt},
147
+ {"role": "user", "content": prompt}
148
+ ],
149
+ api_key=config['api_key'],
150
+ timeout=config.get('timeout', 30)
151
+ )
152
+ if not response or not response.choices:
153
+ raise ValueError("Invalid response from LiteLLM API - no choices returned")
154
+
155
+ if not response.choices[0].message or not response.choices[0].message.content:
156
+ raise ValueError("Invalid response format - missing message content")
157
+
158
+ return response.choices[0].message.content.strip()
159
+
160
+ except Exception as e:
161
+ print(f"♣ LiteLLM API error: {str(e)}")
162
+ raise
163
+
164
+ def get_ai_response(prompt: str, config: Dict, system_role: str = "code_fixer") -> str:
165
+ """
166
+ Get response from configured AI provider.
167
+
168
+ Args:
169
+ prompt (str): The prompt to send to the AI
170
+ config (Dict): Configuration dictionary
171
+ system_role (str): Role for system prompt - "code_fixer", "analyzer", or "report"
172
+
173
+ Returns:
174
+ str: The AI generated response
175
+ """
176
+ system_prompts = {
177
+ "code_fixer": "You are a Python code fixing assistant. Provide only the corrected code without explanations.",
178
+ "analyzer": "You are a Python error analysis assistant. Provide clear and concise explanation of the error and suggestions to fix it.",
179
+ "report": "You are a Python error reporting assistant. Provide a detailed report of the error, its cause, and the applied fix."
180
+ }
181
+
182
+ system_prompt = system_prompts.get(system_role, system_prompts["code_fixer"])
183
+
184
+ try:
185
+ provider = config.get('AI_PROVIDER', 'azure').lower()
186
+
187
+ if provider == 'azure':
188
+ return _get_azure_response(prompt, config['AZURE'], system_prompt)
189
+ elif provider == 'openai':
190
+ return _get_openai_response(prompt, config['OPENAI'], system_prompt)
191
+ elif provider == 'anthropic':
192
+ return _get_anthropic_response(prompt, config['ANTHROPIC'])
193
+ elif provider == 'ollama':
194
+ return _get_ollama_response(prompt, config['OLLAMA'])
195
+ elif provider == 'litellm':
196
+ return _get_litellm_response(prompt, config['LITELLM'], system_prompt)
197
+ else:
198
+ raise ValueError(f"Unsupported AI provider: {provider}")
199
+
200
+ except Exception as e:
201
+ print(f"♣ Error getting AI response: {str(e)}")
202
+ raise
@@ -0,0 +1,174 @@
1
+ import re
2
+ from typing import Dict, Any
3
+ from .ai_broker import get_ai_response
4
+
5
+ def ensure_healing_agent_decorator(code: str) -> str:
6
+ """
7
+ Ensures the code has the @healing_agent decorator, adds it if missing.
8
+
9
+ Args:
10
+ code (str): The code to check/modify
11
+
12
+ Returns:
13
+ str: Code with @healing_agent decorator
14
+ """
15
+ # Check the first 5 lines for the @healing_agent decorator
16
+ lines = code.split('\n')[:5]
17
+ for line in lines:
18
+ if line.strip() == '@healing_agent':
19
+ return code # Decorator already present
20
+
21
+ # If not found, add the decorator at the top
22
+ first_line = code.split('\n')[0]
23
+ indent = len(first_line) - len(first_line.lstrip())
24
+ decorator = ' ' * indent + '@healing_agent\n'
25
+ return decorator + code
26
+
27
+
28
+ def prepare_fix_prompt(context: Dict[str, Any]) -> str:
29
+ """
30
+ Prepare the prompt for AI based on the context.
31
+
32
+ Args:
33
+ context (Dict[str, Any]): The error context
34
+
35
+ Returns:
36
+ str: Formatted prompt for the AI
37
+ """
38
+ # Extract function info if available
39
+ function_info = context.get('function_info', {})
40
+ function_args = context.get('function_arguments', {})
41
+ error_info = context.get('error', {})
42
+
43
+ # Build argument info string
44
+ arg_info = ""
45
+ if function_args:
46
+ arg_info = "\nFunction was called with arguments:\n"
47
+ for arg_name, arg_data in function_args.items():
48
+ arg_info += f"{arg_name}: {arg_data.get('value')} (type: {arg_data.get('type')})\n"
49
+
50
+ # Build function info string
51
+ func_info = ""
52
+ if function_info:
53
+ func_info = f"""
54
+ Function Name: {function_info.get('name')}
55
+ Function Signature: {function_info.get('signature')}
56
+ Module: {function_info.get('module')}
57
+ """
58
+
59
+ # Build error details string
60
+ error_details = ""
61
+ if error_info.get('exception_attrs'):
62
+ error_details = "\nDetailed Error Information:\n"
63
+ for attr, value in error_info['exception_attrs'].items():
64
+ error_details += f"{attr}: {value}\n"
65
+
66
+ # Add traceback frames for context
67
+ traceback_info = ""
68
+ if error_info.get('traceback_frames'):
69
+ traceback_info = "\nTraceback Frames:\n"
70
+ for frame in error_info['traceback_frames']:
71
+ traceback_info += f"File: {frame['filename']}, Line {frame['line_number']}, in {frame['function']}\n"
72
+ traceback_info += f"Code: {frame['code']}\n"
73
+
74
+ # Include AI hint if available
75
+ ai_hint = ""
76
+ if context.get('ai_hint'):
77
+ ai_hint = f"\nAI Analysis:\n{context['ai_hint']}"
78
+
79
+ return f"""
80
+ Fix the following Python code that produced an error, or at least handle the exceptions, add more info that could help debugging next time:
81
+
82
+ Original Code:
83
+ {context['function_info']['source_code']}
84
+
85
+ Error Type: {context['error']['type']}
86
+ Error Message: {context['error']['message']}
87
+ Error Line Number: {context['error'].get('line_number')}
88
+ Error Line: {context['error'].get('error_line')}
89
+
90
+ {error_details}
91
+ {traceback_info}
92
+ {func_info}{arg_info}{ai_hint}
93
+
94
+ Return only the fixed code without any explanations or markdown formatting.
95
+ Ensure the fixed code maintains the same function name and signature.
96
+ Add appropriate error handling where necessary.
97
+ """
98
+
99
+ def validate_fixed_code(fixed_code: str) -> bool:
100
+ """
101
+ Validate the fixed code is syntactically correct.
102
+
103
+ Args:
104
+ fixed_code (str): The code to validate
105
+
106
+ Returns:
107
+ bool: True if valid, False otherwise
108
+ """
109
+ try:
110
+ # Check if the code is syntactically valid Python
111
+ compile(fixed_code, '<string>', 'exec')
112
+
113
+ # Basic checks for common issues
114
+ if not fixed_code.strip():
115
+ print("♣ Generated code is empty")
116
+ return False
117
+
118
+ if "def " not in fixed_code:
119
+ print("♣ Generated code doesn't contain function definition")
120
+ return False
121
+
122
+ return True
123
+
124
+ except SyntaxError as e:
125
+ print(f"♣ Syntax error in generated code: {str(e)}")
126
+ return False
127
+ except Exception as e:
128
+ print(f"♣ Validation error: {str(e)}")
129
+ return False
130
+
131
+ def fix(context: Dict[str, Any], config: Dict[str, Any]) -> str:
132
+ """
133
+ Fix buggy code using AI based on the provided context.
134
+
135
+ Args:
136
+ context (Dict[str, Any]): Dictionary containing:
137
+ - original_code (str): The original buggy code
138
+ - error_info (Dict): Error details including type, message, traceback
139
+ - stack_trace (str): Full stack trace of the error
140
+ - function_name (str): Name of the function that caused the error
141
+ - additional_context (Dict): Any additional context that might help
142
+
143
+ Returns:
144
+ str: The fixed version of the code
145
+ """
146
+ try:
147
+ # Prepare the prompt for AI
148
+ prompt = prepare_fix_prompt(context)
149
+
150
+ # Get the fix from AI with code_fixer role
151
+ fixed_code = get_ai_response(prompt, config, "code_fixer")
152
+
153
+ # Remove markdown code block formatting if present
154
+ fixed_code = re.sub(r'^```python\n|^```\n|```$', '', fixed_code, flags=re.MULTILINE)
155
+
156
+ # Ensure healing_agent decorator is present
157
+ fixed_code = ensure_healing_agent_decorator(fixed_code)
158
+
159
+ # Validate the fixed code
160
+ if validate_fixed_code(fixed_code):
161
+
162
+ return fixed_code
163
+ else:
164
+ print("♣ Generated fix failed validation")
165
+ return
166
+
167
+ except Exception as e:
168
+ print(f"♣ Error during code fixing: {str(e)}")
169
+ print(f"♣ Error type: {type(e).__name__}")
170
+ print(f"♣ Error details: {repr(e)}")
171
+ print(f"♣ Error traceback:")
172
+ import traceback
173
+ traceback.print_exc()
174
+ return
@@ -0,0 +1,89 @@
1
+ from typing import Dict, Any
2
+ from .ai_broker import get_ai_response
3
+
4
+ def generate_hint(context: Dict[str, Any], config: Dict[str, Any]) -> str:
5
+ """
6
+ Generate an AI-powered hint based on the exception context.
7
+
8
+ Args:
9
+ context (Dict[str, Any]): The exception context
10
+ config (Dict[str, Any]): The configuration dictionary
11
+
12
+ Returns:
13
+ str: The generated AI hint
14
+ """
15
+ # Extract error information
16
+ error = context['error']
17
+ error_type = error['type']
18
+ error_message = error['message']
19
+ function_name = error['function_name']
20
+ traceback = error['traceback']
21
+ traceback_frames = error['traceback_frames']
22
+ exception_attrs = error['exception_attrs']
23
+
24
+ # Extract any additional error details if present
25
+ error_details = ""
26
+ for detail_type in ['json_details', 'connection_details', 'timeout_details',
27
+ 'http_details', 'value_details', 'key_details',
28
+ 'type_details', 'file_details']:
29
+ if detail_type in error:
30
+ error_details += f"\n{detail_type}:\n{error[detail_type]}"
31
+
32
+ # Get function info
33
+ function_info = context.get('function_info', {})
34
+ function_source = function_info.get('source_code', '')
35
+ function_module = function_info.get('module', '')
36
+
37
+ # Get function arguments
38
+ function_arguments = context.get('function_arguments', {})
39
+
40
+ # Get environment info
41
+ python_version = context.get('python_version', '')
42
+ platform = context.get('platform', '')
43
+
44
+ # Prepare the prompt for AI
45
+ prompt = f"""
46
+ An exception occurred in a Python program:
47
+
48
+ ENVIRONMENT:
49
+ Python Version: {python_version}
50
+ Platform: {platform}
51
+
52
+ ERROR DETAILS:
53
+ Error Type: {error_type}
54
+ Error Message: {error_message}
55
+ Function Name: {function_name}
56
+ Module: {function_module}
57
+
58
+ Source Code:
59
+ {function_source}
60
+
61
+ Function Arguments:
62
+ {function_arguments}
63
+
64
+ Exception Attributes:
65
+ {exception_attrs}
66
+
67
+ Traceback:
68
+ {traceback}
69
+
70
+ Detailed Traceback Frames:
71
+ {traceback_frames}
72
+
73
+ Additional Error Details:
74
+ {error_details}
75
+
76
+ Based on all the provided context, generate a helpful hint or suggestion for resolving the issue. Consider:
77
+ 1. The exact error type and message
78
+ 2. The function's source code
79
+ 3. The values of arguments passed to the function
80
+ 4. Any additional error-specific details
81
+ 5. The full execution context from the traceback
82
+
83
+ Provide the hint in a concise and clear manner, avoiding any code snippets or markdown formatting.
84
+ """
85
+
86
+ # Get the AI-generated hint with analyzer role
87
+ hint = get_ai_response(prompt, config, system_role="analyzer")
88
+
89
+ return hint